private void buildHiddenSelectorWidget(
      SerialisationContext pSerialisationContext,
      HTMLSerialiser pSerialiser,
      EvaluatedNodeInfoItem pEvalNode,
      List<FieldSelectOption> pSelectOptions) {
    Map<String, Object> lTemplateVars =
        super.getGenericTemplateVars(pSerialisationContext, pSerialiser, pEvalNode);

    // Force the style to be off screen
    lTemplateVars.put("Style", "position: absolute; left: -999em;");

    if (pEvalNode.getSelectorMaxCardinality() > 1) {
      lTemplateVars.put("Multiple", true);
    }

    // Always set size greater than 2, otherwise all fields post back with first value
    lTemplateVars.put("Size", Math.max(2, pSelectOptions.size()));

    List<Map<String, Object>> lOptions = new ArrayList<>();
    OPTION_LOOP:
    for (FieldSelectOption lOption : pSelectOptions) {
      if (lOption.isHistorical() && !lOption.isSelected()) {
        // Skip un-selected historical items
        continue OPTION_LOOP;
      }
      Map<String, Object> lOptionVars = new HashMap<>(3);
      lOptionVars.put("Key", lOption.getDisplayKey());
      lOptionVars.put("Value", lOption.getExternalFieldValue());
      if (lOption.isSelected()) {
        lOptionVars.put("Selected", true);
      }
      if (lOption.isDisabled()) {
        lOptionVars.put("Disabled", true);
      }
      lOptions.add(lOptionVars);
    }
    lTemplateVars.put("Options", lOptions);

    MustacheFragmentBuilder.applyMapToTemplate(
        SELECTOR_MUSTACHE_TEMPLATE, lTemplateVars, pSerialiser.getWriter());
  }
  @Override
  public void buildWidgetInternal(
      SerialisationContext pSerialisationContext,
      HTMLSerialiser pSerialiser,
      EvaluatedNodeInfoItem pEvalNode) {

    FieldMgr lFieldMgr = pEvalNode.getFieldMgr();
    List<FieldSelectOption> lSelectOptions = filteredOptionList(pEvalNode);

    if (!pEvalNode.isPlusWidget() && lFieldMgr.getVisibility() == NodeVisibility.VIEW) {
      SelectorWidgetBuilder.outputReadOnlyOptions(pSerialiser, pEvalNode);
    } else {
      // Create a hidden select element similar to the selector widget
      buildHiddenSelectorWidget(pSerialisationContext, pSerialiser, pEvalNode, lSelectOptions);

      // Find image base URL (the static servlet path)
      RequestURIBuilder lURIBuilder = pSerialisationContext.createURIBuilder();

      // Mapset var name, could be cache key but then possible issues with itemrec
      String lMapsetJSONVariableName =
          lFieldMgr.getExternalFieldName()
              + pEvalNode
                  .getStringAttribute(NodeAttribute.MAPSET, "missing-map-set")
                  .replaceAll("[^a-zA-Z0-9]*", "");
      // Static servlet requires app mnem appended
      String lJSON =
          mapsetToJSON(
              pEvalNode,
              StaticServlet.getURIWithAppMnem(
                  lURIBuilder, pSerialisationContext.getApp().getAppMnem()));
      // Add the JSON to the page
      pSerialisationContext.addConditionalLoadJavascript(
          "var " + lMapsetJSONVariableName + " = " + lJSON + ";");

      // Add a placeholder element
      String lLoadingElementName = "loading" + lFieldMgr.getExternalFieldName();
      pSerialiser.append("<input type=\"text\" id=\"");
      pSerialiser.append(lLoadingElementName);
      pSerialiser.append("\" class=\"tagger tagger-loading\" />");

      // Use the field width as the tagger width if tight field is specified, otherwise tagger
      // should use 100% of the cell
      String lFieldWidth;
      if (pEvalNode.getBooleanAttribute(NodeAttribute.TIGHT_FIELD, false)) {
        lFieldWidth = "$('#" + lLoadingElementName + "').css('width')";
      } else {
        lFieldWidth = "'100%'";
      }

      // Config for the jqueryTagger widget
      JSONObject lTaggerConfig = new JSONObject();
      lTaggerConfig.put("baseURL", lURIBuilder.buildServletURI(StaticServlet.SERVLET_PATH));
      lTaggerConfig.put("imgDownArrow", "/img/tagger-dropdown.png");
      lTaggerConfig.put("imgRemove", "/img/tagger-remove.png");
      lTaggerConfig.put("imgSearch", "/img/tagger-search.png");
      lTaggerConfig.put("fieldWidth", lFieldWidth);

      // Add optinal values to pass to tagger
      lTaggerConfig.putAll(establishExtraParams(pEvalNode));

      // Variable is passed in as a raw variable name to be resolved at runtime
      lTaggerConfig.put("availableTags", new JSONNonEscapedValue(lMapsetJSONVariableName));

      // Pass through lSelectedIdList
      JSONArray lSelectedTags = new JSONArray();
      for (FieldSelectOption lOption : lSelectOptions) {
        if (lOption.isSelected()) {
          lSelectedTags.add(lOption.getExternalFieldValue());
        }
      }
      lTaggerConfig.put("preselectedTags", lSelectedTags);

      boolean lAJAXMapSet = pEvalNode.getMapSet() instanceof JITMapSet;
      if (lAJAXMapSet) {
        lTaggerConfig.put(
            "ajaxURL",
            MapSetWebService.AjaxSearchEndPoint.buildEndPointURI(
                pSerialisationContext.createURIBuilder(),
                pSerialisationContext.getThreadInfoProvider().getThreadId()));
        lTaggerConfig.put(
            "ajaxErrorFunction",
            new JSONNonEscapedValue(
                "function(self, data){self._showMessageSuggestion('The application has experienced an unexpected error, please try again or contact support. Error reference: <strong>' + data.responseJSON.errorDetails.reference + '</strong>', 'error');}"));
      }

      // Add in the JS to construct the tagger for the select
      pSerialisationContext.addConditionalLoadJavascript(
          "$(function(){\n"
              + "  $('#"
              + lFieldMgr.getExternalFieldName()
              + "').tagger("
              + lTaggerConfig.toJSONString()
              + ");\n"
              + "});");
    }
  }
  /**
   * Take in all the lists from the mapset and generate a JSON object array for each entry, to be
   * used by the search-selector widget
   *
   * @param pEvalNode Current ENI to get options from
   * @param pBaseURL Base URL of static servlet to replace %IMAGE_BASE% with in suggestion text
   * @return String of the JSON array to be set at the top of the page
   */
  private String mapsetToJSON(EvaluatedNodeInfoItem pEvalNode, String pBaseURL) {
    JSONArray lMapsetJSON = new JSONArray();
    JSONObject lMapsetObject = new JSONObject();
    JSONObject lJSONEntry;

    int i = 1;
    for (FieldSelectOption lItem : filteredOptionList(pEvalNode)) {
      FieldSelectOption lSearchableItem = lItem;

      String lHiddenSearchable = lItem.getAdditionalProperty(HIDDEN_SEARCHABLE_MS_PROPERTY);
      String lSuggestion = lItem.getAdditionalProperty(SUGGESTION_DISPLAY_MS_PROPERTY);
      String lLevel = lItem.getAdditionalProperty(LEVEL_MS_PROPERTY);

      if ((pEvalNode.getFieldMgr().getVisibility() == NodeVisibility.VIEW && !lItem.isSelected())) {
        // Skip putting it in the JSON if it's got null entries or in read only mode and not
        // selected
        continue;
      }

      lJSONEntry = new JSONObject();

      lJSONEntry.put("id", lSearchableItem.getExternalFieldValue());
      lJSONEntry.put(KEY_JSON_PROPERTY, lSearchableItem.getDisplayKey());
      lJSONEntry.put(SORT_JSON_PROPERTY, i++);

      // Add suggestion text (if none in mapset the JS will use the key safely escaped)
      if (!XFUtil.isNull(lSuggestion)) {
        lJSONEntry.put(
            SUGGESTION_DISPLAY_JSON_PROPERTY,
            StringEscapeUtils.escapeHtml4(lSuggestion.replaceAll("%IMAGE_BASE%", pBaseURL)));
      }

      lJSONEntry.put(HIDDEN_SEARCHABLE_JSON_PROPERTY, XFUtil.nvl(lHiddenSearchable, ""));

      // Add entry for hierarchical data
      if (lLevel != null) {
        lJSONEntry.put(LEVEL_JSON_PROPERTY, lLevel);
      }

      // If it was selected add that entry
      if (lSearchableItem.isSelected()) {
        lJSONEntry.put(SELECTED_JSON_PROPERTY, true);
      }

      // If it's a historical entry add that entry
      if (lSearchableItem.isHistorical()) {
        lJSONEntry.put(HISTORICAL_JSON_PROPERTY, true);
        lJSONEntry.put(SUGGESTABLE_JSON_PROPERTY, false);
      } else {
        lJSONEntry.put(SUGGESTABLE_JSON_PROPERTY, true);
      }

      if (lSearchableItem.getAdditionalProperty(OptionFieldMgr.FREE_TEXT_ADDITIONAL_PROPERTY)
          != null) {
        lJSONEntry.put(FREE_TEXT_JSON_PROPERTY, true);
      }

      lJSONEntry.put(DISABLED_JSON_PROPERTY, lSearchableItem.isDisabled());

      lMapsetJSON.add(lJSONEntry);
      lMapsetObject.put(lSearchableItem.getExternalFieldValue(), lJSONEntry);
    }
    return lMapsetObject.toJSONString();
  }