view class doc
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895/// FOURJS_START_COPYRIGHT(D,2017)
/// Property of Four Js*
/// (c) Copyright Four Js 2017, 2022. 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('FieldWidgetBase', ['TextWidgetBase', 'WidgetFactory'],
  function(context, cls) {

    /**
     * Base class for genero formfield widgets
     * @class FieldWidgetBase
     * @memberOf classes
     * @publicdoc Widgets
     * @extends classes.TextWidgetBase
     */
    cls.FieldWidgetBase = context.oo.Class(cls.TextWidgetBase, function($super) {
      return /** @lends classes.FieldWidgetBase.prototype */ {
        __name: "FieldWidgetBase",
        /**
         * Flag for augmentedFace
         * @type {boolean}
         */
        __virtual: true,

        /**
         * List of values through time
         * @type {Array}
         */
        _valueStack: null,

        /**
         * Flag to know if the placeholder contain the real one (it could be the comment)
         * @type {boolean}
         */
        _isFakePlaceholder: true,

        /**
         * true if widget has pending changes
         * @type {boolean}
         */
        _editing: false,

        /**
         * true if widget is readOnly and can't be edited nor focused
         * @type {boolean}
         * */
        _isReadOnly: false,

        /**
         * the input element
         * @protected
         * @type {HTMLElement}
         */
        _inputElement: null,

        /**
         * Indicates if key event handlers are bound
         */
        _keyEventsBound: false,

        /***
         * Time of the last widget modification
         * @type {number}
         */
        _editingTime: 0,

        /**
         * Position of the current value in the stack
         * @type {Number}
         */
        _valueStackCursor: -1,

        /**
         * Exact value from VM
         * @type {*}
         */
        _vmValue: null,

        /**
         * Old value, used by typeahead
         * @type {?string}
         */
        _oldValue: null,

        /**
         * true if widget should not be editable but navigation is possible
         * @type {boolean}
         */
        _notEditable: false,

        /**
         * true if widget requires a value
         * @type {boolean}
         */
        _required: false,

        /**
         * true if widget is set as not Null
         * @type {boolean}
         */
        _notNull: false,

        /**
         * List of possible values for the widget
         * @type {?Array}
         */
        _include: null,

        /**
         * Flag to check if the mouse button is currently pressed
         * @type {boolean}
         */
        _isMousePressed: false,

        /**
         * input element state
         * @type {cls.InputTextState}
         */
        _elementState: null,

        /**
         * Scroll attribute value
         * @type {boolean}
         */
        _scroll: null,

        /**
         * true if must ignore the scroll attribute (equivalent to scroll = 0)
         * @type {boolean}
         */
        _dataTypeWithNoScroll: false,

        /**
         * Maximum number of characters allowed. By default -1 indicates no limit.
         * @type {number}
         */
        _maxLength: -1,

        /**
         * widget VM width
         * @type {number}
         */
        _vmWidth: 0,

        /**
         * true if we are between a key down and key yp event
         * @type {boolean}
         */
        _processingKeyEvent: null,

        /**
         * true if a picture is define on this field
         * @type {boolean}
         */
        _pictureDefined: null,

        /**
         * @constructs
         * @inheritDoc
         */
        constructor: function(opts) {
          $super.constructor.call(this, opts);
          this.setEnabled(false, true);
        },

        /**
         * @inheritDoc
         */
        _initElement: function() {
          $super._initElement.call(this);
          this._valueStack = [];
          if (window.isMobile()) {
            var inputElement = this._element.getElementsByTagName('input')[0];
            if (inputElement) {
              // Track the focus and mouse down/up events on mobile devices to handle the virtual keybord's TAB key
              inputElement.on('focus.FieldWidgetBase', this._onMobileFocus.bind(this));
              inputElement.on('mousedown.FieldWidgetBase', this._onMobileMouseDown.bind(this));
              inputElement.on('mouseup.FieldWidgetBase', this._onMobileMouseUp.bind(this));
            }
          }

          this._elementState = new cls.InputTextState();
          this._processingKeyEvent = false;
          this._pictureDefined = false;
        },

        /**
         * @inheritDoc
         */
        destroy: function() {
          $super.destroy.call(this);

          this._valueStack = null;
          this._oldValue = null;

          this.unbindKeyEvents();
          this._keyEventsBound = false;

          if (this._inputElement && this.isNotEditable()) {
            this._inputElement.off('drop.FieldWidgetBase_notEditable');
          }

          if (this._inputElement && window.isMobile()) {
            this._inputElement.off('focus.FieldWidgetBase');
            this._inputElement.off('mousedown.FieldWidgetBase');
            this._inputElement.off('mouseup.FieldWidgetBase');
          }

          this._inputElement = null;
          this._elementState = null;
        },

        /**
         * @inheritDoc
         */
        _afterInitElement: function() {
          $super._afterInitElement.call(this);

          this.bindKeyEvents();
          this._keyEventsBound = true;
        },

        /**
         * Get the input part of the widget
         * @return {HTMLElement} the input part of the widget
         * @publicdoc
         */
        getInputElement: function() {
          return this._inputElement;
        },

        /**
         * Check if the widget has an input element
         * @return {boolean} true if widget has an input element
         * @publicdoc
         */
        hasInputElement: function() {
          return Boolean(this.getInputElement());
        },

        /**
         * Bind all keys events of the widget (done when the widget becomes active)
         * @protected
         */
        bindKeyEvents: function() {},
        /**
         * Unbind all keys events of the widget (done when the widget becomes inactive or in typeahead)
         * @protected
         */
        unbindKeyEvents: function() {},

        /**
         * Set the value of widget
         * @param {string|number} value - sets the value to display
         * @param {boolean} [fromVM] - true if value comes from the VM
         * @publicdoc
         */
        setValue: function(value, fromVM) {
          if (this.hasCursors() && !fromVM) { // only widgets with cursors manage undo/redo
            this._valueStack.push(value);
            this._valueStackCursor++;
          }

          if (fromVM) {
            this._vmValue = value;
            if (this.getValue() !== value) {
              this._valueStack = [value];
              this._valueStackCursor = 0;
            } else {
              this._valueStack.push(value);
              this._valueStackCursor++;
            }
            this._oldValue = value;
          }
          if (this._valueStack.length > 30) {
            this._valueStack.shift();
            this._valueStackCursor--;
          }
        },

        /**
         * Internal setValue to change value without any event emited
         * @param {string} value - the value
         * @private
         */
        _setValue: function(value) {
          if (this._inputElement) {
            this._inputElement.value = value;
          }
        },

        /**
         * Handle input event to manage
         *  - set editing
         *  - shift attribute (textTransform !== none)
         * @private
         */
        _onInput: function() {
          if (this.isNotEditable()) {
            // If not editable, rollback to old value (the initial one)
            this._inputElement.value = this._oldValue;
          } else {
            this._editingTime = Date.now();
            this.setEditing(this.isEditing() || this.getValue() !== this._oldValue);
            if (this.isEditing() && this._textTransform !== 'none' && this.hasInputElement()) {
              var start = this._inputElement.selectionStart;
              var end = this._inputElement.selectionEnd;
              this._inputElement.value = this.getValue();
              this._inputElement.setCursorPosition(start, end);
            }

          }
        },

        /**
         * Handle drop event
         * @param evt
         * @private
         */
        _onDrop: function(evt) {
          if (this.isNotEditable()) {
            evt.preventCancelableDefault();
          }
        },

        /**
         * NotEditable allows cursor moving, but not a value change
         * @param {boolean} notEditable - true to set the edit part as read-only
         */
        setNotEditable: function(notEditable) {
          this._notEditable = notEditable;
          if (this._inputElement) {
            if (notEditable) {
              this._inputElement.on('drop.FieldWidgetBase_notEditable', this._onDrop.bind(this));
            } else {
              this._inputElement.off('drop.FieldWidgetBase_notEditable');
            }
          }
        },

        /**
         * NotEditable allows cursor moving, but not a value change
         * @return {boolean} true if the edit part is not editable
         */
        isNotEditable: function() {
          return this._notEditable;
        },

        /**
         * Set the widget validation to 'required'
         * @param {boolean} required - true if a value is required
         */
        setRequired: function(required) {
          this._required = required;
          this.toggleClass("gbc_Required", required);
        },

        /**
         * Verify if the widget value is required
         * @return {boolean} true if a value is required
         */
        isRequired: function() {
          return this._required;
        },

        /**
         * Verify if the placeholder is the real one
         * @return {boolean} true if it is a fake placeholder
         */
        isFakePlaceholder: function() {
          return this._isFakePlaceholder;
        },

        /**
         * Set the widget validation to noNull
         * @param {boolean} notNull - false if the widget value can be null, true otherwise
         */
        setNotNull: function(notNull) {
          this._notNull = notNull;
          this.toggleClass("gbc_NotNull", notNull);
        },

        /**
         * Verify if the widget can be null
         * @return {boolean} false if the widget value can be null, true otherwise
         */
        isNotNull: function() {
          return this._notNull;
        },

        /**
         * Get the list of allowed values defined by INCLUDE list
         * @param {Array|null} include - list of allowed values or null if not defined
         */
        setAllowedValues: function(include) {
          this._include = include;
        },

        /**
         * Get the list of allowed values defined by INCLUDE list
         * @return {Array|null} list of allowed values or null if not defined
         */
        getAllowedValues: function() {
          return this._include;
        },

        /**
         * Prevent value change but allow navigation
         * @param {Event} evt the browser event
         * @param {string} keyString the string representation of the key sequence
         * @private
         */
        _preventEditAllowNavigation: function(evt, keyString) {
          var prevent = ["ctrl+x", "ctrl+v", "meta+x", "meta+v"].contains(keyString); // CTRL+X & CTRL+V forbidden
          prevent = prevent || (["tab", "home", "end", "left", "right", "up", "down", "shift+left", "shift+right", "ctrl+c",
            "ctrl+a",
            "meta+c", "meta+a"
          ].contains(
            keyString) === false);

          if (prevent) {
            evt.preventCancelableDefault();
            this.flash();
          }
        },

        /**
         * Get the value of the widget
         * @returns {?string|number} the value
         * @publicdoc
         */
        getValue: function() {
          return null;
        },

        /**
         * @inheritDoc
         */
        getClipboardValue: function() {
          return this.getValue();
        },

        /**
         * Define the widget as readonly or not
         * @param {boolean} readonly - true to set the widget as readonly without possibility of edition, false otherwise
         * @publicdoc
         */
        setReadOnly: function(readonly) {
          this._isReadOnly = readonly;
        },

        /**
         * Check if the widget is readonly or not
         * @returns {boolean} true if the widget is readonly, false otherwise
         * @publicdoc
         */
        isReadOnly: function() {
          return this._isReadOnly;
        },

        /**
         * @returns {number} time of the last widget modification
         */
        getEditingTime: function() {
          return this._editingTime;
        },

        /**
         * Check if widget is currently editing
         * @return {boolean}
         */
        isEditing: function() {
          return this._editing;
        },

        /**
         * Flag or unflag widget as having value pending changes
         * @param editing {boolean} the new editing state
         * @publicdoc
         */
        setEditing: function(editing) {
          this._editing = editing;
          if (this.getElement()) {
            this.getElement().toggleClass("editing", Boolean(editing));
          }
        },

        /**
         * Returns if the widget is focusable
         * @return {boolean} State of focusable
         * @publicdoc
         */
        isFocusable: function() {
          return this.hasInputElement() || $super.isFocusable.call(this);
        },

        /**
         * Tests if the widget has really the DOM focus (check document.activeElement)
         * @returns {boolean} true if the widget has the DOM focus
         * @publicdoc
         */
        hasDOMFocus: function() {
          return (this.hasInputElement() && this.getInputElement() === document.activeElement) ||
            $super.hasDOMFocus.call(this);
        },

        /**
         * Defines the enabled status of the widget
         * @param {boolean} enabled true if the widget allows user interaction, false otherwise.
         * @publicdoc
         */
        setEnabled: function(enabled, noSelectionUpdate) {
          if (this._enabled !== Boolean(enabled)) {
            this._enabled = Boolean(enabled);
            this.domAttributesMutator(function(noSelectionUpdate) {
              if (this._enabled) {
                this.removeClass("disabled");
                if (this.hasInputElement() && !this.isReadOnly()) {
                  this.getInputElement().removeAttribute("readonly");
                }
              } else {
                this.addClass("disabled");
                if (!noSelectionUpdate) {
                  if (this.hasCursors()) {
                    this.setCursors(0);
                    this.afterDomMutator(function() {
                      var selection = window.getSelection();
                      if (selection) {
                        var hasTextSelection = selection.focusNode === this._element;
                        if (hasTextSelection) {
                          selection.removeAllRanges();
                        }
                      }
                    }.bind(this));
                  }
                }
                if (this.hasInputElement()) {
                  this.getInputElement().setAttribute("readonly", "readonly");
                }
              }
            }.bind(this, noSelectionUpdate));
          }
          // bind/unbind keys events
          if (enabled && (this.isNotEditable && !this.isNotEditable() || this.isReadOnly && !this.isReadOnly())) {
            if (!this._keyEventsBound) {
              this._keyEventsBound = true;
              this.bindKeyEvents();
            }
          } else {
            if (this._keyEventsBound) {
              this._keyEventsBound = false;
              this.unbindKeyEvents();
            }
          }
        },

        /**
         * @inheritDoc
         */
        loseVMFocus: function(vmNewFocusedWidget = null) {
          $super.loseVMFocus.call(this, vmNewFocusedWidget);
          this.setEditing(false);
        },

        /**
         * @inheritDoc
         */
        loseFocus: function() {
          $super.loseFocus.call(this);
          this.setEditing(false);
        },

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

          if (this.isEnabled() && this.hasCursors()) {
            if (keyString === "home") {
              this.setCursors(0);
              keyProcessed = true;
            } else if (keyString === "end") {
              this.setCursors(this.getValue() && this.getValue().toString().length || 0);
              keyProcessed = true;
            }
          }

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

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

          if (this.isEnabled() && !this.isReadOnly()) {

            if (keyString === "ctrl+z" || keyString === 'meta+z') {
              this.undo();
              keyProcessed = true;
            } else if (keyString === "ctrl+shift+z" || keyString === 'meta+shift+z') {
              this.redo();
              keyProcessed = true;
            }

            if (this.isNotEditable()) {
              this._preventEditAllowNavigation(domKeyEvent, keyString);
            }
          }

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

        /**
         * @inheritDoc
         */
        manageKeyUp: function(keyString, domKeyEvent) {
          $super.manageKeyUp.call(this, keyString, domKeyEvent);
          this._processingKeyEvent = false;
        },

        /**
         * Cancel the last value
         */
        undo: function() {
          if (this.hasCursors()) { // only widgets with cursors manage undo/redo
            var cursors = this.getCursors();
            var prevValue = this.getValue();
            //go back but store the current as last known value
            if (this._valueStackCursor === this._valueStack.length - 1 && this._valueStack[this._valueStack.length - 1] !==
              this.getValue()) {
              this.setValue(this.getValue());
            }
            this._valueStackCursor--;
            this._valueStackCursor = this._valueStackCursor < 0 ? 0 : this._valueStackCursor;

            this.afterDomMutator(function() {
              var val = this._valueStack[this._valueStackCursor];
              if (typeof val === "string" && this.hasInputElement()) {
                this._setValue(val);
                var diff = prevValue.length - val.length;
                this.setCursors(cursors.start - diff);
              }
            }.bind(this));
          }
        },

        /**
         * Restore the last value
         */
        redo: function() {
          if (this.hasCursors()) { // only widgets with cursors manage undo/redo
            var cursors = this.getCursors();
            var prevValue = this.getValue();
            if (this._valueStackCursor < this._valueStack.length - 1) {
              this._valueStackCursor++;
            }
            this.afterDomMutator(function() {
              var val = this._valueStack[this._valueStackCursor];
              if (typeof val === "string" && this.hasInputElement()) {
                this._setValue(val);
                var diff = prevValue.length - val.length;
                this.setCursors(cursors.start - diff);
              }
            }.bind(this));
          }
        },

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

          if (!this.isReadOnly() && !this.isInTable() && this.hasInputElement()) {

            var selectAllAllowed = this.getValue().length > 0;
            contextMenu.addAction("selectAll", i18next.t("gwc.contextMenu.selectAll"), "font:FontAwesome.ttf:f0ea", "Ctrl+A", {
              clickCallback: function() {
                contextMenu.hide();
                this.setFocus();
                this.selectAllInputText();
              }.bind(this),
              disabled: !selectAllAllowed
            }, true);
          }
        },

        /**
         * Select all the text in the input element
         * @publicdoc
         */
        selectAllInputText: function() {
          if (this.hasInputElement()) {
            var cursor2 = this.getValue() && this.getValue().length || 0;
            this._inputElement.setCursorPosition(0, cursor2);
          }
        },

        /**
         * Defines a placeholder text
         * @param {string} placeholder - placeholder text
         * @param {boolean} fake - true if placeholder come from another attribute
         * @publicdoc
         */
        setPlaceHolder: function(placeholder, fake) {
          if (this.hasInputElement()) {
            this._isFakePlaceholder = fake ? true : false;
            if (placeholder) {
              this._inputElement.setAttribute('placeholder', placeholder);
            } else {
              this._inputElement.removeAttribute('placeholder');
            }
          }
        },

        /**
         * Method used to validate or not the value, this trigger a rollback if not valid when sending
         * the value to the VM
         * @return {boolean} - true if valid, false otherwise
         */
        validateValue: function() {
          // Implement your own method on widgets
          return true;
        },

        /**
         * This function requests the VM focus if this focus event hasn't been triggered
         * by a mouse or touch event.
         * This happens when the user presses the TAB key of a mobile's virtual keyboard.
         * - TAB generates only a focus event
         * - A tap or click generates a mousedown, focus and mouseup events
         * @param {FocusEvent} event HTML focus event
         * @private
         */
        _onMobileFocus: function(event) {
          // Skip event if the focus is given by FocusApplicationService._transferFocusToNode for this widget
          if (!this._isMousePressed && cls.FocusApplicationService.getCurrentTransferFocusWidget() !== this) {
            this._onRequestFocus(event);
          }
        },

        /**
         * @param {MouseEvent} event HTML mouse event
         * @private
         */
        _onMobileMouseDown: function(event) {
          this._isMousePressed = true;
        },

        /**
         * @param {MouseEvent} event HTML mouse event
         * @private
         */
        _onMobileMouseUp: function(event) {
          this._isMousePressed = false;
        },

        /**
         * Fix the char full/half char size according to the widget field width
         * @param {string} text - widget value
         * @param {string} newTextPart - new text part to verify
         * @return {string} a valid newTextPart
         */
        checkValueDisplayWidth: function(text, newTextPart) {
          if (this._maxLength <= 0 && this._vmWidth <= 0) {
            return newTextPart;
          }

          var displayWidth = this._vmWidth;
          var maxLength = this.getUserInterfaceWidget().isCharLengthSemantics() ? this._maxLength : -1;

          var fullText = text + newTextPart;
          var textLength = Array.from(text).length;
          var res = newTextPart;
          var codepoints = Array.from(newTextPart);

          if (fullText.displayWidth() > displayWidth || (maxLength > 0 && (textLength + codepoints.length) > maxLength)) {
            do {
              codepoints.pop();
              res = codepoints.join('');
              fullText = text + res;
            } while (codepoints.length > 0 && fullText.displayWidth() > displayWidth ||
              (maxLength > 0 && (textLength + codepoints.length) > maxLength));
          }

          return res;
        },

        /**
         * Fix the newTextPart according to the requested byte length
         * @param {string} text - old part of the widget value
         * @param {string} newTextPart - new text part to verify
         * @param {number} bytes - requested max bytes length
         * @return {string} a valid newTextPart
         */
        checkValueByteCount: function(text, newTextPart, bytes) {
          var fullText = text + newTextPart;

          if (fullText.length === 0 || fullText.countBytes() <= bytes) {
            return newTextPart;
          }

          if (String.isSingleByteEncoding()) {
            return newTextPart.substr(0, this._maxLength - text.length);
          }

          var codepoints = Array.from(newTextPart);
          var textLength = text.countBytes();
          var res = '';
          do {
            codepoints.pop();
            res = codepoints.join('');
          } while (codepoints.length > 0 && (textLength + res.countBytes()) > bytes);

          return res;
        },

        /**
         * @return {boolean} true if we must ignore the scroll attribute
         */
        isDataTypeWithNoScroll: function() {
          return this._dataTypeWithNoScroll;
        },

        /**
         * Set to true if we must ignore the scroll attribute
         * @param {boolean} checkDisplayValue
         */
        setDataTypeWithNoScroll: function(checkDisplayValue) {
          this._dataTypeWithNoScroll = checkDisplayValue;
        },

        /**
         * Widget VM width
         * @param {number} width
         */
        setVMWidth: function(width) {
          this._vmWidth = width;
        },

        /**
         * return true if the backup value has been restored
         * @return {boolean}
         */
        isValueRestored: function() {
          return this._elementState.isRestored();
        },

        /**
         * Set if a picture is defined on this field
         * @param {boolean} defined
         */
        setPictureDefined: function(defined) {
          this._pictureDefined = defined;
        },

        /**
         * True if a picture is defined on this field
         * @return {boolean}
         */
        isPictureDefined: function() {
          return this._pictureDefined;
        },

        /**
         * Get the element state
         * @return {cls.InputTextState}
         */
        getElementState: function() {
          return this._elementState;
        }

      };
    });
  });