view class doc
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524/// FOURJS_START_COPYRIGHT(D,2015)
/// Property of Four Js*
/// (c) Copyright Four Js 2015, 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('ImageWidget', ['ColoredWidgetBase', 'WidgetFactory'],
  function(context, cls) {

    /**
     * Image widget.
     * @class ImageWidget
     * @memberOf classes
     * @extends classes.ColoredWidgetBase
     * @publicdoc Widgets
     */
    cls.ImageWidget = context.oo.Class(cls.ColoredWidgetBase, function($super) {
      return /** @lends classes.ImageWidget.prototype */ {
        __name: 'ImageWidget',
        /**
         * @type {?string}
         */
        _src: null,
        _defaultColor: null,
        /** @type {boolean} */
        _autoScale: false,
        /** @type {String|null} **/
        _scaleIconValue: null,

        /** @type {boolean} */
        _gotFirstInitialImage: false,
        /** @type {boolean} */
        _firstInitialSizing: true,
        /** @type {boolean} */
        _initialAutoscaling: false,
        /** @type {HTMLElement} */
        _img: null,
        /** @type {HTMLElement} */
        _border: null,
        /** @type {boolean} */
        _standalone: false,
        /** @type {boolean} */
        _hasContent: false,
        /** @type {Object} */
        _alignment: null,

        /**
         * Custom error handler called inside _onError
         * @type {function} */
        _onErrorHandler: null,

        /**
         * @inheritDoc
         */
        _initLayout: function() {
          this._layoutInformation = new cls.LayoutInformation(this);
          this._layoutInformation.shouldFillStack = true;
          this._layoutEngine = new cls.ImageLayoutEngine(this);
          this._layoutEngine._shouldFillHeight = true;
        },

        /**
         * @inheritDoc
         */
        destroy: function() {
          this._border = null;
          $super.destroy.call(this);
        },

        /**
         * @inheritDoc
         */
        manageMouseClick: function(domEvent) {
          this._onRequestFocus(domEvent); // request focus
          this.emit(context.constants.widgetEvents.click, domEvent);
          return true;
        },

        /**
         * Define image as a regular standalone widget
         * @param {boolean} standalone - true if standalone, false otherwise
         */
        setStandaloneImage: function(standalone) {
          this._standalone = Boolean(standalone);
          this._element.toggleClass('gbc_withBorder', this._standalone);
          this._element.toggleClass('gbc_selfImage', this._standalone);
        },

        /**
         * If image has action, change cursor
         * @param {boolean} clickable - true if clickable, false otherwise
         * @publicdoc
         */
        setClickableImage: function(clickable) {
          if (clickable) {
            this.addClass('clickable');
          } else {
            this.removeClass('clickable');

          }
        },

        /**
         * ShortCut for setSrc
         * This is used in the context of an Image FormField
         * @param {string} val the URL of the image to display or a font-image URL: font:[fontname]:[character]:[color]
         * @see setSrc
         * @publicdoc
         */
        setValue: function(val) {
          this.setSrc(val);
        },

        /**
         * Shortcut for getSrc
         * This is used in the context of an Image FormField
         * @returns {string} the URL of the displayed image or a font-image URL: font:[fontname]:[character]:[color]
         * @see getSrc
         * @publicdoc
         */
        getValue: function() {
          return this.getSrc();
        },

        /**
         * ShortCut for setSrc
         * This is used in the context of a Static Image
         * @param {string} image the URL of the image to display or a font-image URL: font:[fontname]:[character]:[color]
         * @see setSrc
         * @publicdoc
         */
        setImage: function(image) {
          this.setSrc(image);
        },

        /**
         * Shortcut for getSrc
         * This is used in the context of a Static FormField
         * @returns {string} the URL of the displayed image or a font-image URL: font:[fontname]:[character]:[color]
         * @see getSrc
         * @publicdoc
         */
        getImage: function() {
          return this.getSrc();
        },

        /**
         * Check if image is a font image
         * @return {boolean} true if is a font image
         * @publicdoc
         */
        isFontImage: function() {
          if (this._src) {
            return this._src.startsWith('font:');
          } else {
            return false;
          }
        },

        /**
         * Set the source of the image file
         * @param {string} src the URL of the image to display or a font-image URL: font:[fontname]:[character]:[color]
         * @param {boolean} directApply true to apply src directly (internal use)
         * @publicdoc
         */
        setSrc: function(src, directApply) {
          this.getLayoutInformation().invalidateMeasure();
          if (src !== this._src) {
            var old = this._src,
              initial = this.getLayoutInformation().getSizePolicyConfig().isInitial();
            this._src = src;
            if (initial && this._gotFirstInitialImage && old !== null && src !== null) {
              this._firstInitialSizing = false;
            }
            if (initial && old === null && src !== null) {
              this._gotFirstInitialImage = true;
            }
            this._updateImage(directApply);
          }
          this.domAttributesMutator(function() {
            if (!this._destroyed && this._img && this.getTitle()) {
              this._img.setAttribute("alt", this.getTitle());
            }
          }.bind(this));
        },

        /**
         * Get the source of the image file
         * @returns {string} the URL of the displayed image or a font-image URL: font:[fontname]:[character]:[color]
         * @publicdoc
         */
        getSrc: function() {
          return this._src;
        },

        /**
         * @inheritDoc
         */
        setTitle: function(title) {
          $super.setTitle.call(this, title);
          if (this._img) {
            this._img.setAttribute('alt', title);
          }
        },

        /**
         * Define the image as stretchable
         * @param {boolean} stretch - true if stretchable
         * @publicdoc
         */
        setStretch: function(stretch) {
          this._element.toggleClass('stretch', stretch);
        },

        /**
         * Forces the image to be stretched to fit in the area reserved for the image.
         * @param {boolean} setted true : autoScale , false: default
         * @publicdoc
         */
        setAutoScale: function(setted) {
          if (setted !== this._autoScale) {
            this._autoScale = setted;
            this._updateImage();
          }
        },

        /**
         * Set autoscale value as nnnpx
         * @param {string} value - css string value with valid units
         */
        setScaleIconValue: function(value) {
          this._scaleIconValue = value;
          this.addClass('gbc_scaleIconValue');
          this._updateImage(true);
        },

        /**
         * Se the default color
         * @param {string} color - any CSS compliant color
         */
        setDefaultColor: function(color) {
          this._defaultColor = color;
        },

        /**
         * @inheritDoc
         */
        setFocus: function(fromMouse) {
          this._element.domFocus();
        },

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

        /**
         * Align the image
         * @param {number|string} y - y position
         * @param {number|string} x - x position
         * @publicdoc
         */
        setAlignment: function(y, x) {
          var rtl = this.getStart() === 'right';
          this._alignment = {
            x: x,
            y: y,
            val: (x === 'horizontalCenter' || x === 'center' ? 'center' :
                ((x === 'right' && !rtl) || (x !== 'right' && rtl) ? 'right' : 'left')
              ) + ' ' +
              (y === 'verticalCenter' || y === 'center' ? 'center' : (y === 'bottom' ? 'bottom' : 'top'))
          };
          var pos = {
            'align-items': y === 'verticalCenter' ? 'center' : (y === 'bottom' ? 'flex-end' : 'flex-start'),
            'justify-content': x === 'horizontalCenter' ? 'center' : (x === 'right' ? 'flex-end' : 'flex-start'),
            'background-position': this._alignment.val
          };
          this.setStyle(pos);
        },

        /**
         * Update image according to several pre-set parameters
         * @param {boolean} directApply true to apply src directly (internal use)
         * @private
         */
        _updateImage: function(directApply) {
          if (!this._element) {
            return;
          }
          if (this._hasContent) {
            this._element.empty();
            this._hasContent = false;
          }
          if (this._img) {
            this._img.off('error.ImageWidget');
            this._img.off('load.ImageWidget');
            this._img = null;
          }
          var backgroundImage = null;
          var backgroundSize = null;
          var backgroundRepeat = null;
          var backgroundPosition = null;
          var width = null;
          var height = null;

          if (this._src) {

            if (this._src.startsWith('font:')) {
              var pattern = /font:([^:]+).ttf:([^:]+):?([^:]*)/,
                match = this._src.match(pattern),
                fontName, sCharCode, color, iCharCode, finalChar;
              if (match) {
                fontName = match[1];
                sCharCode = match[2];
                iCharCode = parseInt('0x' + sCharCode, 16);
                if (0x10000 <= iCharCode && iCharCode <= 0x10FFFF) {
                  iCharCode = iCharCode - 0x10000;
                  finalChar = String.fromCharCode(0xD800 | (iCharCode >> 10)) +
                    String.fromCharCode(0xDC00 | (iCharCode & 0x3FF));
                } else {
                  finalChar = String.fromCharCode('0x' + sCharCode);
                }
                color = match[3] || this._defaultColor;
              }
              if (fontName && sCharCode) {
                var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.setAttribute('viewBox', '0 0 640 512');
                // to left align svg, we need to set xMin, otherwise with a 100% width viewBox it will be centered
                svg.setAttribute('preserveAspectRatio', 'xMinYMid meet');
                var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                text.setAttribute('text-anchor', 'middle');
                // EDGE & IE doesn't support dominant-baseline central attribute, so we need to center using another way
                if (window.browserInfo.isEdge || window.browserInfo.isIE) {
                  text.setAttribute('dy', '0.7ex');
                } else {
                  text.setAttribute('dominant-baseline', 'central');
                }
                text.setAttribute('x', '320');
                text.setAttribute('y', '256');
                text.setAttribute('font-size', '470');
                text.setAttribute('font-family', '"image2font_' + fontName.trim() + '"');

                if (this._scaleIconValue) {
                  svg.style.setProperty('--scaleIconValue', this._scaleIconValue);
                }
                if (directApply) {
                  text.textContent = finalChar;
                } else {
                  window.requestAnimationFrame(function(text, character) {
                    text.textContent = character;
                  }.bind(this, text, finalChar));
                }
                if (color) {
                  text.setAttribute('fill', color);
                }
                svg.appendChild(text);
                this._element.appendChild(svg);
                this._hasContent = true;
                this.emit(context.constants.widgetEvents.ready);
              }
              this.getElement().toggleClass('gbc_fixedSvg', !this._autoScale && !this._scaleIconValue);
            } else {
              var isInitial = this.getLayoutInformation().getSizePolicyConfig().isInitial();
              if (this._inTable || this._autoScale && (!isInitial || !this._firstInitialSizing)) {
                backgroundImage = "url('" + this._src + "')";
                backgroundSize = 'contain';
                backgroundRepeat = 'no-repeat';
                width = '100%';
                height = '100%';
                backgroundPosition = this._alignment && this._alignment.val || this.getStart();
                this.emit(context.constants.widgetEvents.ready);
              } else {
                this._img = document.createElement('img');
                this._img.on('error.ImageWidget', this._onError.bind(this));
                if (directApply) {
                  this._img.setAttribute("src", this._src);
                } else {
                  this._setElementAttribute("src", this._src, "_img");
                }
                this._img.on('load.ImageWidget', this._onLoad.bind(this));
                this._element.appendChild(this._img);
              }
              this._hasContent = true;
            }
            this.toggleClass('gbc_autoScale', this._autoScale);
            if (this._scaleIconValue && this._img) {
              this._img.style.setProperty('--scaleIconValue', this._scaleIconValue);
              this.getLayoutInformation().invalidateMeasure();
            }
          }
          if (this._standalone) {
            if (!this._border) {
              this._border = document.createElement('div');
              this._border.addClass('gbc_ImageWidget_border');
            }
            this._element.appendChild(this._border);
          }
          this.setStyle({
            'background-image': backgroundImage,
            'background-size': backgroundSize,
            'background-repeat': backgroundRepeat,
            'background-position': backgroundPosition,
            'width': width
          });
          if (this.__charMeasurer) {
            this._element.appendChild(this.__charMeasurer);
          }
        },

        /**
         * Set a custom process in the default error handler
         * @param errorHdl
         */
        setErrorHandler: function(errorHdl) {
          this._onErrorHandler = errorHdl;
        },

        /**
         * Error handler in case of wrong loading and other
         * @private
         */
        _onError: function() {
          this._img.off('error.ImageWidget');
          this._img.off('load.ImageWidget');
          if (this._element) {
            this._element.addClass('hidden');
          }

          if (this._onErrorHandler) {
            this._onErrorHandler();
          }
        },

        /**
         * Load handler to decide what to do after image finished loading
         * @private
         */
        _onLoad: function() {
          this._img.off('error.ImageWidget');
          this._img.off('load.ImageWidget');
          if (this._element) {
            this._element.removeClass('hidden');
            var w = this._img.naturalWidth,
              h = this._img.naturalHeight;
            if (!this.getLayoutEngine().hasNaturalSize()) {
              if (!this._autoScale) {
                this._layoutEngine.invalidateMeasure();
              }
              this.getLayoutEngine()._needMeasure = true;
            }
            this.getLayoutEngine().setNaturalSize(w, h);
            this._element.toggleClass('gbc_ImageWidget_wider', w > h).toggleClass('gbc_ImageWidget_higher', w <= h);

            this.getLayoutInformation()._sizeRatio = h / w;

            var isInitial = this.getLayoutInformation().getSizePolicyConfig().isInitial();
            if (isInitial && this._firstInitialSizing) {
              if (this._autoScale) {
                this._initialAutoscaling = true;
                this.getLayoutInformation()._keepRatio = true;
              } else {
                if (!this.getLayoutEngine().hasNaturalSize()) {
                  this.getLayoutEngine()._needMeasure = true;
                }
              }
            }
            this.emit(context.constants.widgetEvents.ready, this.getLayoutEngine().hasNaturalSize());
            gbc.LogService.ui.log("Image loaded", true, this.__name, this);
          }
        },

        /**
         * Callback once image has been layouted
         * @private
         */
        _whenLayouted: function() {
          if (this._initialAutoscaling) {
            this._initialAutoscaling = false;
            this._firstInitialSizing = false;
            this._updateImage();
          }
        },

        /**
         * @inheritDoc
         */
        setHidden: function(hidden) {
          $super.setHidden.call(this, hidden);
          if (!this._hidden && this._element.parentNode) {
            this._element.parentNode.removeClass('gl_gridElementHidden');
          }
        },

        getStyleSheetId: function() {
          var windowWidget = this.getWindowWidget(),
            windowWidgetId = windowWidget && windowWidget.getUniqueIdentifier();
          return this._uuid || windowWidgetId || "_";
        },

        /**
         * Get the natural dimension of the image
         * @return {{width: number, height: number}}
         */
        getNaturalDimension: function() {
          return {
            width: this._img.naturalWidth,
            height: this._img.naturalHeight
          };
        }

      };
    });
    cls.WidgetFactory.registerBuilder('Image', cls.ImageWidget);
    cls.WidgetFactory.registerBuilder('ImageWidget', cls.ImageWidget);
  });