view class doc
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780/// FOURJS_START_COPYRIGHT(D,2015)
/// Property of Four Js*
/// (c) Copyright Four Js 2015, 2024. All Rights Reserved.
/// * Trademark of Four Js Development Tools Europe Ltd
///   in the United States and elsewhere
///
/// This file can be modified by licensees according to the
/// product manual.
/// FOURJS_END_COPYRIGHT

'use strict';

modulum('ComboBoxWidget', ['ComboBoxWidgetBase', 'WidgetFactory'],
  function(context, cls) {

    /**
     * Combobox widget.
     * @class ComboBoxWidget
     * @memberOf classes
     * @extends classes.ComboBoxWidgetBase
     * @publicdoc Widgets
     */

    cls.ComboBoxWidget = context.oo.Class(cls.ComboBoxWidgetBase, function($super) {
      return /** @lends classes.ComboBoxWidget.prototype */ {
        __name: 'ComboBoxWidget',

        /** @type {string} */
        __dataContentPlaceholderSelector: '.gbc_dataContentPlaceholder',
        /** @type {classes.EditWidget}*/
        _editWidget: null,
        /** @type {classes.ListDropDownWidget} */
        _dropDown: null,
        /** @type {string} */
        _typedLetters: "",
        /** @function */
        _typedLettersCacheHandler: null,
        /** @function */
        _focusHandler: null,
        /** @function */
        _editFocusHandler: null,
        /** @function */
        _dropDownSelectHandler: null,
        /** @function */
        _visibilityChangeHandler: null,

        /** @type {HTMLElement} */
        _toggleIcon: null,

        /** @type {string} */
        _placeholderText: '',

        /** @type {string} */
        _currentValue: "",
        /** @type {string} */
        _lastVMValue: "",
        /** @type {boolean} */
        _allowMultipleValues: false,

        /**
         * @inheritDoc
         */
        _initLayout: function() {
          if (!this._ignoreLayout) {
            this._layoutInformation = new cls.LayoutInformation(this);
            this._layoutEngine = new cls.ComboBoxLayoutEngine(this);
            this._layoutInformation.setReservedDecorationSpace(2);
            this._layoutInformation.setSingleLineContentOnly(true);
          }
        },

        /**
         * Bind all events listeners on combobox and create the combobox dropdown
         * @inheritDoc
         */
        _initElement: function() {
          $super._initElement.call(this);

          this._toggleIcon = this.getElement().querySelector("i.toggle");

          this.setStyle('i.toggle', {
            'min-width': window.scrollBarSize + 'px',
          });

          this._editWidget = cls.WidgetFactory.createWidget('EditWidget', this.getBuildParameters());
          this._editWidget.setFocusable(false);
          this._editWidget.setParentWidget(this);
          this._element.prependChild(this._editWidget.getElement());

          this._dropDown = cls.WidgetFactory.createWidget('ListDropDown', this.getBuildParameters());
          this._dropDown.setParentWidget(this);
          this._dropDown.fallbackMaxHeight = 300;
          this._dropDown.hide();
          this._dropDownSelectHandler = this._dropDown.when(context.constants.widgetEvents.select, this._onSelectValue.bind(this));
          this._focusHandler = this.when(context.constants.widgetEvents.focus, this._onFocus.bind(this));
          this._dropDown.onClose(this._onFocus.bind(this));
          this._dropDown.onClose(this._onClose.bind(this));

          this._editWidget._inputElement.on('blur.ComboBoxWidget', this._onBlur.bind(this));
          this._editFocusHandler = this._editWidget.when(context.constants.widgetEvents.requestFocus,
            this._onEditRequestFocus.bind(this));

          this._visibilityChangeHandler = this._dropDown
            .when(context.constants.widgetEvents.visibilityChange, this._updateEditState.bind(this));

          this.setAriaAttribute("owns", this._dropDown.getRootClassName());
          this.setAriaAttribute("labelledby", this._editWidget.getRootClassName());
        },

        /**
         * @inheritDoc
         */
        destroy: function() {
          if (this._focusHandler) {
            this._focusHandler();
            this._focusHandler = null;
          }
          if (this._editFocusHandler) {
            this._editFocusHandler();
            this._editFocusHandler = null;
          }
          if (this._dropDownSelectHandler) {
            this._dropDownSelectHandler();
            this._dropDownSelectHandler = null;
          }
          if (this._visibilityChangeHandler) {
            this._visibilityChangeHandler();
            this._visibilityChangeHandler = null;
          }

          this._editWidget._inputElement.off('blur.ComboBoxWidget');

          if (this._dropDown) {
            this._dropDown.destroy();
            this._dropDown = null;
          }

          this._typedLettersCacheHandler = null;

          this._editWidget.destroy();
          this._editWidget = null;
          $super.destroy.call(this);
        },

        /**
         * Set widget mode. Useful when widget have peculiar behavior in certain mode
         * @param {string} mode the widget mode
         * @param {boolean} active the active state
         */
        setWidgetMode: function(mode, active) {
          this._allowMultipleValues = mode === "Construct";
          this._dropDown.allowMultipleChoices(this._allowMultipleValues);
          this._updateEditState();
          this._updateTextTransform();
        },

        /**
         * Returns whether or not the user should be able to input data freely
         * @return {boolean} true if user can input data
         */
        canInputText: function() {
          return $super.canInputText.call(this) && this._allowMultipleValues;
        },

        /**
         * format the value
         * @param {string} value value
         * @return {string} the formatted value
         * @private
         */
        _getFormattedValue: function(value) {
          var values = (value || "").split("|").map(function(itemValue) {
            var found = this._dropDown.findByValue(itemValue);
            return found && found.text || itemValue;
          }.bind(this));
          if (values[0] === "") {
            values.splice(0, 1);
          }
          return values.join("|");
        },

        /**
         * Handler when focus is requested
         * @param event
         * @param sender
         * @param domEvent
         */
        _onEditRequestFocus: function(event, sender, domEvent) {
          this.emit(context.constants.widgetEvents.requestFocus, domEvent);
        },

        /**
         * update the edit value from the widget value
         * @private
         */
        _updateEditValue: function() {
          this._editWidget.setValue(this._getFormattedValue(this._currentValue));
        },
        /**
         * update edit availability
         * @private
         */
        _updateEditState: function() {
          var readOnly = this._dropDown.isVisible() || !this.canInputText();
          this._editWidget.setReadOnly(readOnly);
        },

        /**
         * Handler once dropdown is closed
         * @private
         */
        _onClose: function() {
          this._toggleIcon.toggleClass("dd-open", this._dropDown.isVisible());
        },

        /**
         * @inheritDoc
         */
        manageMouseClick: function(domEvent) {
          this._onRequestFocus(domEvent); // request focus
          if (this.isEnabled() && !(this._isQueryEditable && domEvent.target.tagName === 'INPUT')) {
            this._dropDown.setCurrentValue(this.getValue());
            this._dropDown.show();
            this._toggleIcon.toggleClass("dd-open", this._dropDown.isVisible());
          }
          return true;
        },

        /**
         * Focus handler
         * @private
         */
        _onFocus: function() {
          if (this._editWidget && this.isEnabled()) {
            this._editWidget.getInputElement().domFocus();
          }
        },

        /**
         * Blur handler
         * @private
         */
        _onBlur: function() {
          if (!this._dropDown.isVisible()) {
            this.emit(context.constants.widgetEvents.blur);
          } else if (!this.hasFocus()) {
            this._dropDown.hide();
          }
        },
        /**
         * when value is selected in the dropdown
         * @param event
         * @param src
         * @param value
         * @private
         */
        _onSelectValue: function(event, src, value) {
          this.setEditing(true);
          this.setValue(value);
        },

        /**
         * @inheritDoc
         */
        managePriorityKeyDown: function(keyString, domKeyEvent, repeat) {
          var keyProcessed = false;
          if (this._dropDown.isVisible()) {
            keyProcessed = this._dropDown.managePriorityKeyDown(keyString, domKeyEvent, repeat);
          }

          if (keyProcessed) {
            return true;
          }
          return $super.managePriorityKeyDown.call(this, keyString, domKeyEvent, repeat);
        },

        /**
         * @inheritDoc
         */
        manageKeyDown: function(keyString, domKeyEvent, repeat) {
          var keyProcessed = false;

          if (this.isEnabled()) {
            if (keyString === "alt+up" || keyString === "alt+down") {
              //Initialize dropdown selected value before to show it
              this._dropDown.setCurrentValue(this.getValue());
            }

            // we call managePriorityKeyDown method on the closed combobox to select item using same navigation logic
            keyProcessed = this._dropDown.managePriorityKeyDown(keyString, domKeyEvent, repeat);

            if (!keyProcessed) {
              // auto item preselection by name
              keyProcessed = this._processKey(domKeyEvent, keyString);
            }
          }

          if (keyProcessed) {
            return true;
          }
          return $super.manageKeyDown.call(this, keyString, domKeyEvent, repeat);
        },

        /**
         * Process one key event
         * @param {Object} event
         * @param {string} keyString
         * @returns {boolean} true if key has been processed, false otherwise
         * @private
         */
        _processKey: function(event, keyString) {
          if (event.which <= 0) { // arrows key (for firefox)
            return false;
          }
          var key = event.gbcKey;

          if (key.length > 1) { // we only want single char
            return false;
          }
          if (this._typedLettersCacheHandler) {
            this._clearTimeout(this._typedLettersCacheHandler);
            this._typedLettersCacheHandler = 0;
          }
          if (!this._dropDown.isVisible()) {
            if (this.canInputText()) {
              return false;
            }
          }

          var lastChar = key.toLocaleLowerCase();
          this._typedLettersCacheHandler = this._registerTimeout(this._clearTypedLettersCache.bind(this), 400);
          this._typedLetters += lastChar;
          // looking for an item matching with combination of typed chars within 400ms time interval
          var found = this._dropDown.findStartingByText(this._typedLetters, this._typedLetters.length === 1);
          if (!found) {
            // if no item matched combination of chars we just display next item beginning with last typed char so user can scroll different values
            this._typedLetters = lastChar;
            found = this._dropDown.findStartingByText(this._typedLetters, true);
          }
          if (found) {
            this._dropDown.navigateToItem(found);
            if (!this._dropDown.isVisible()) {
              this.setEditing(this._oldValue !== found.value);
              this.setValue(found.value);
            }
            return true;
          }
          return false;
        },

        /**
         * Clear the cache of typed letters
         * @private
         */
        _clearTypedLettersCache: function() {
          this._typedLettersCacheHandler = 0;
          this._typedLetters = "";
        },

        /**
         * Get the value of the dropdown list at given position
         * @param {number} pos - position in the dropdown
         * @return {*} value at given position
         * @publicdoc
         */
        getValueAtPosition: function(pos) {
          return this._dropDown.getValueAtPosition(pos);
        },

        /**
         * get the available items
         * @return {Object[]}
         */
        getItems: function() {
          return this._dropDown.getItems();
        },

        /**
         * set combobox choice(s)
         * @param {ListDropDownWidgetItem|ListDropDownWidgetItem[]} choices - a single or a list of choices
         * @publicdoc
         */
        setChoices: function(choices) {
          var list = choices;
          if (!Array.isArray(choices)) {
            list = choices ? [choices] : [];
          }
          this._dropDown.setItems(list);
          if (this._dropDown.isVisible()) {
            this._dropDown.setSelectedValues(this.getValue());
            this._dropDown.setCurrentValue(this.getValue());
          }
          this._updateSelectedValue();
          if (this._layoutEngine) {
            this._layoutEngine.invalidateMeasure();
          }
        },
        /**
         * update edit value
         * @private
         */
        _updateSelectedValue: function() {
          var selectedValue = this.isEditing() ? this._editWidget.getValue() : this._lastVMValue;
          var foundInList = this._dropDown.findByText(selectedValue);
          if (foundInList) { // first try to find by text
            this._currentValue = foundInList.text;
          } else { // if not found by text, try by value
            foundInList = this._dropDown.findByValue(selectedValue);
            this._currentValue = foundInList && foundInList.value || "";
          }
          if (!foundInList) {
            this._editWidget.setValue('');
          } else {
            this._editWidget.setValue(foundInList.text);
          }
        },

        /**
         * Display the dropdown
         * @publicdoc
         */
        showDropDown: function() {
          this.emit(context.constants.widgetEvents.requestFocus, null);
          this._dropDown.setCurrentValue(this.getValue());
          this._dropDown.show();
        },

        toggleDropDown: function() {
          this._dropDown.setCurrentValue(this.getValue());
          this._dropDown.toggle();
        },

        /**
         * get the associate dropdown
         * @returns {classes.ListDropDownWidget}
         */
        getDropDown: function() {
          return this._dropDown;
        },

        /**
         * @inheritDoc
         */
        getValue: function() {
          var editValue = this.getEditValue(),
            formattedValue = this._getFormattedValue(this._currentValue);

          if (this._isQueryEditable && editValue && editValue !== formattedValue && editValue.trim().length > 0) {
            var itemList = editValue.split("|").map(function(t) {
              var item = this._dropDown.findByText(t, false);
              return item ? item.value : t;
            }.bind(this));

            if (this._editWidget && itemList.length > 0 && this.canInputText()) {
              this._currentValue = itemList.join("|");
              this._editWidget.setValue(this._getFormattedValue(this._currentValue));
            }

            return this._currentValue || "";
          }

          return this.canInputText() && !this._dropDown.isVisible() &&
            editValue !== formattedValue ? editValue : this._currentValue || "";
        },

        /**
         * @inheritDoc
         */
        getClipboardValue: function() {
          var value = this._dropDown.getCurrentValue();
          var item = this._dropDown.findByValue(value);
          return item ? item.text : value;
        },

        /**
         * get the edit value
         * @return {string}
         */
        getEditValue: function() {
          return this._editWidget && this._editWidget.getValue() || "";
        },

        /**
         * @inheritDoc
         */
        setValue: function(value, fromVM) {
          $super.setValue.call(this, value, fromVM);

          if (this.hasFocus()) {
            if (this._editWidget) {
              this._editWidget.getInputElement().setSelectionRange(0, 0);
            } else {
              this._element.setSelectionRange(0, 0);
            }
          }

          if (fromVM) {
            if (this._editWidget) {
              this._editWidget.setEditing(false);
            }
            this._lastVMValue = value;
          }

          this._setValue(value, fromVM);
        },

        /**
         * Internal setValue used to be inherited correctly by OtherWidget
         * @param {string} value - the value
         * @param {boolean} [fromVM] - is value come from VM ?
         * @private
         */
        _setValue: function(value, fromVM) {
          var currentValue, valueChanged = false;
          if (this._allowMultipleValues) {
            if (this._dropDown.isVisible()) {
              if (fromVM) {
                this._currentValue = value;
                this._updateEditValue();
              } else {
                var commandList = ["=", "<>", "!="];
                var hasConstructCommand = commandList.findIndex(function(v) {
                  return v === value;
                }) >= 0;

                //In construct commandList can't be combined
                var values = this._currentValue === "" ||
                  (hasConstructCommand && this.getDialogType() === 'Construct' && this._currentValue !== "=") ? [] : this._currentValue.split(
                    "|");
                var existingIndex = values.indexOf(value);
                if (existingIndex >= 0) {
                  values.splice(existingIndex, 1);
                } else {
                  values.push(value);
                }

                //Remove the '=' or '<>' if a regular value is selected
                commandList.forEach((k) => {
                  var nullIndex = values.indexOf(k);
                  if (nullIndex >= 0 && values.length > 1) {
                    values.splice(nullIndex, 1);
                  }
                });

                values = this._dropDown.sortValues(values);
                this._currentValue = values.join("|");
                valueChanged = true;
                this._updateEditValue();
              }
              this._dropDown.setSelectedValues(this._currentValue);
            } else {
              currentValue = this._currentValue || "";
              if (currentValue !== value) {
                this._currentValue = value;
                this._dropDown.setCurrentValue(value);
                this._updateEditValue();
                valueChanged = true;
              }
              if (!fromVM && valueChanged) {
                this.emit(context.constants.widgetEvents.change, false);
              }
            }
          } else {
            currentValue = this._currentValue || "";
            if (currentValue !== value) {
              var found = this._dropDown.findByValue(value);
              this._currentValue = found && found.value || "";
              this._dropDown.setCurrentValue(this._currentValue);
              this._editWidget.setValue(found ? found.text : "");
              valueChanged = true;
            }
          }
          if (!fromVM && valueChanged) {
            this.emit(context.constants.widgetEvents.change, false);
          }
        },

        /**
         * @inheritDoc
         */
        setFocus: function(fromMouse, stayOnSameWidget) {
          $super.setFocus.call(this, fromMouse);

          if (this._editWidget) {
            var inputElement = this._editWidget.getInputElement();

            inputElement.domFocus();

            if (!stayOnSameWidget && inputElement.selectionStart !== inputElement.selectionEnd) {
              inputElement.selectionStart = inputElement.selectionEnd = inputElement.value.length;
            }
          } else {
            this._element.domFocus();
          }

          if (!stayOnSameWidget) {
            //Reset the dropdown index position
            if (this._allowMultipleValues) {
              this._dropDown.clearCurrentIndex();
            } else {
              var currentValue = this._currentValue || "";
              this._dropDown.setCurrentPosition(this._dropDown.indexByValue(currentValue));
            }
          }
        },

        /**
         * @inheritDoc
         */
        setEnabled: function(enabled) {
          $super.setEnabled.call(this, enabled);
          this._editWidget.setEnabled(enabled);
          this._updateEditState();
          this._updateEditValue();
          this._dropDown.setEnabled(enabled);
        },

        /**
         * sets the combobox as query editable
         * @param {boolean} isQueryEditable
         */
        setQueryEditable: function(isQueryEditable) {
          this._isQueryEditable = isQueryEditable;
          this._dropDown.setQueryEditable(isQueryEditable);
          this._updateEditState();
          this._updateTextTransform();
        },

        /**
         * @inheritDoc
         */
        setColor: function(color) {
          $super.setColor.call(this, color);
          this._editWidget.setColor(color);
          if (this._dropDown) {
            this._dropDown.setColor(color);
          }
        },

        /**
         * @inheritDoc
         */
        getColorFromStyle: function() {
          return this._editWidget.getColorFromStyle();
        },

        /**
         * @inheritDoc
         */
        setBackgroundColor: function(color) {
          $super.setBackgroundColor.call(this, color);
          if (this._dropDown) {
            this._dropDown.setBackgroundColor(color);
          }
        },

        /**
         * @inheritDoc
         */
        setFontWeight: function(weight) {
          $super.setFontWeight.call(this, weight);
          this._editWidget.setFontWeight(weight);
          if (this._dropDown) {
            this._dropDown.setFontWeight(weight);
          }
        },

        /**
         * @inheritDoc
         */
        setFontFamily: function(fontFamily) {
          $super.setFontFamily.call(this, fontFamily);
          if (this._dropDown) {
            this._dropDown.setFontFamily(fontFamily);
          }
        },

        /**
         * @inheritDoc
         */
        setFontStyle: function(style) {
          $super.setFontStyle.call(this, style);
          this._editWidget.setFontStyle(style);
          if (this._dropDown) {
            this._dropDown.setFontStyle(style);
          }
        },

        /**
         * @inheritDoc
         */
        setFontSize: function(size) {
          $super.setFontSize.call(this, size);
          // apply to dropdown as well
          if (this._dropDown) {
            this._dropDown.setFontSize(size);
          }
        },

        /**
         * @inheritDoc
         */
        setTextAlign: function(align) {
          $super.setFontAlign.call(this, align);
          this._editWidget.setTextAlign(align);
          if (this._dropDown) {
            this._dropDown.setTextAlign(align);
          }
        },

        /**
         * @inheritDoc
         */
        removeTextTransform: function() {
          $super.removeTextTransform.call(this);
          this._editWidget.removeTextTransform();
        },
        /**
         * @inheritDoc
         */
        _updateTextTransform: function() {
          var wantedTextTransform = this.canInputText() ? this._textTransform : "none";
          if (wantedTextTransform !== this._editWidget.getTextTransform()) {
            this._editWidget.removeTextTransform();
            this._editWidget.setTextTransform(wantedTextTransform);
          }
        },

        /**
         * @inheritDoc
         */
        setTextDecoration: function(decoration) {
          $super.setTextDecoration.call(this, decoration);
          this._editWidget.setTextDecoration(decoration);
        },

        /**
         * Handle a null item if notNull is not specified
         * @param {boolean} notNull - combobox accept notNull value?
         * @publicdoc
         */
        setNotNull: function(notNull) {
          $super.setNotNull.call(this, notNull);
          this._dropDown.setNotNull(notNull);
        },

        /**
         * @inheritDoc
         */
        setPlaceHolder: function(placeholder) {
          $super.setPlaceHolder.call(this, placeholder);
          this._editWidget.setPlaceHolder(placeholder);
        },

        /**
         * @inheritDoc
         */
        setDialogType: function(dialogType) {
          $super.setDialogType.call(this, dialogType);

          this._dropDown.updateUIList();
        },

        /**
         * @inheritDoc
         */
        getClipboardAuthorizedAction: function() {
          return {
            paste: false,
            copy: true,
            cut: false
          };
        }

      };
    });
    cls.WidgetFactory.registerBuilder('ComboBox', cls.ComboBoxWidget);
    cls.WidgetFactory.registerBuilder('ComboBoxWidget', cls.ComboBoxWidget);
  });