Source: control2/Control.js

(function() {
    /**
     * @namespace control2
     * @memberof Help4
     */
    Help4.control2 = {};
    /**
     * @namespace input
     * @memberof Help4.control2
     */
    Help4.control2.input = {};
    /**
     * @namespace recording
     * @memberof Help4.control2
     */
    Help4.control2.recording = {};

    /**
     * @typedef {Object} Help4.control2.DeriveClassParams
     * @property {HTMLElement} dom - parent DOM element
     * @property {HTMLDocument} document - parent Document
     * @property {boolean} rtl - RTL mode enabled
     * @property {boolean} mobile - mobile mode enabled
     * @property {string} language - language of system; for translation
     * @property {string} contentLanguage - language of content; for translation
     */

    /**
     * resolves a classname, such as "Help4.control2.Control"
     * and delivers to corresponding object
     * @param {string} name - the to be resolved name
     * @returns {?Function}
     */
    Help4.control2.getClass = function(name) {
        /**
         * @param {string[]} parts
         * @param {Object} namespace
         * @returns {?Object|?Function}
         */
        const search = (parts, namespace) => {
            const l = parts.length;
            let i = -1;
            while (namespace && (++i < l)) {
                namespace = namespace[parts[i]];
            }
            return namespace;
        }

        // search
        // 1. within Help4.control2 namespace
        // 2. within window
        const parts = name.split('.');
        return search(parts, Help4.control2) || search(parts, window);
    }

    /**
     * @param {Help4.control2.Control|Help4.control2.DeriveClassParams} control
     * @param {Object} params
     * @return {Object}
     */
    Help4.control2.deriveClassParams = function(control, params)  {
        const {rtl, mobile, language, contentLanguage, dom, document} = control;
        params.dom ||= dom || control.getDom();
        params.document ||= document || control.getDocument();
        params.rtl ??= rtl;
        params.mobile ??= mobile;
        params.language ||= language;
        params.contentLanguage ||= contentLanguage;
        return params;
    }

    /**
     * @param {Function} object
     * @param {Help4.control2.Control|Help4.control2.DeriveClassParams} control
     * @param {Help4.control2.Control.Params} params
     * @returns {Help4.control2.Control}
     */
    Help4.control2.createControl = function(object, control, params) {
        this.deriveClassParams(control, params);
        return new object(params);
    }

    const DATA_TEXT_ATTRIBUTE = 'data-help4-text-id';

    /**
     * @typedef {Object} Help4.control2.PositionXY
     * @property {number} x - left
     * @property {number} y - top
     */

    /**
     * @typedef {Object} Help4.control2.PositionLeftTop
     * @property {number} left - left
     * @property {number} top - top
     */

    /**
     * @typedef {Object} Help4.control2.SizeWH
     * @property {number} w - width
     * @property {number} h - height
     */

    /**
     * @typedef {Object} Help4.control2.SizeWidthHeight
     * @property {number} width - width
     * @property {number} height - height
     */

    /**
     * @typedef {Object} Help4.control2.AreaXYWH
     * @property {number} x - left
     * @property {number} y - top
     * @property {number} w - width
     * @property {number} h - height
     */

    /**
     * @typedef {Object} Help4.control2.ConnectionPoints
     * @property {Help4.control2.PositionXY} [l] - left point
     * @property {Help4.control2.PositionXY} [r] - right point
     * @property {Help4.control2.PositionXY} [t] - top point
     * @property {Help4.control2.PositionXY} [b] - bottom point
     * @property {Help4.control2.PositionXY} [m] - middle point
     * @property {Help4.control2.PositionXY} [c] - center point
     */

    /**
     * @typedef {Object} Help4.control2.DragDropParams
     * @property {HTMLElement} object - the to be moved element
     * @property {HTMLElement} [area] - the area that controls the drag & drop event
     * @property {boolean} [remoteMode]
     * @property {Help4.control2.PositionXY} [min] - min coordinates for drag
     * @property {Help4.control2.PositionXY} [max] - max coordinates for drag
     */

    /**
     * @typedef {Object} Help4.control2.Control.Params
     * @property {string} [id] - the control id
     * @property {?string} [css = null] - CSS classes to be added to the control
     * @property {string} [tag = 'div'] - the control's TAG NAME in the DOM
     * @property {boolean} [rtl = false] - whether RTL is enabled
     * @property {?string} [language = null] - current language
     * @property {?string} [contentLanguage = null] - content language
     * @property {boolean} [mobile = false] - whether mobile mode is enabled
     * @property {Help4.control2.container.Container} [container = null] - a possible container that hosts this control
     * @property {?string} [title = null] - title of the control within DOM
     * @property {?string} [role = null] - role attribute
     * @property {?number} [tabIndex = null] - tabIndex attribute
     * @property {?number} [zIndex = null] - z-index CSS property for this control
     * @property {?string} [ariaLabel = null] - arial-label attribute
     * @property {?string} [ariaRoleDescription = null] - aria-roledescription attribute
     * @property {?string} [ariaLive = null] - aria-live attribute
     * @property {?string} [ariaAtomic = null] - aria-atomic attribute
     * @property {?string} [ariaRelevant = null] - aria-relevant attribute
     * @property {boolean} [visible = true] - whether the control is visible
     * @property {boolean} [active = false] - whether the control is active
     * @property {boolean} [disabled = false] - whether the control is disabled
     * @property {boolean} [animated = false] - whether the control is in an animated state
     * @property {boolean} [autoFocus = false] - whether the control shall be automatically focussed after creation
     * @property {boolean} [autoEvent = false] - whether the control shall automatically listen to events
     * @property {boolean} [enableDragDrop = false] - whether drag & drop shall be enabled for this control
     * @property {?Help4.control2.PositionLeftTop} [dragPosition = null] - initial position of drag & drop
     * @property {Object} [_metadata = {}] - provide external data as key-value pairs
     * @property {?string} [_order = null] - for {@link Help4.control2.container.SortedContainer}
     * @property {HTMLElement} [dom = window.document.body] - the hosting DOM element for this control
     * @property {HTMLDocument} [document = window.document] - the hosting document for this control
     */

    /**
     * The base class of the control2 library.
     * @augments Help4.jscore.ControlBase
     * @property {string} id - the control id; readonly
     * @property {?string} css - classes of the control; readonly
     * @property {string} tag - tag name; readonly
     * @property {boolean} rtl - enable RTL; readonly
     * @property {?string} language - language id; readonly
     * @property {boolean} mobile - enable mobile mode; readonly
     * @property {?Help4.control2.container.Container} container - reference to a possible container
     * @property {?string} title - DOM title of the control
     * @property {?string} role - DOM role
     * @property {?number} tabIndex - DOM tabIndex
     * @property {?number} zIndex - DOM z-index
     * @property {?string} contentLanguage - content language id; readonly
     * @property {?string} ariaLabel - DOM aria-label
     * @property {?string} ariaRoleDescription - DOM aria-roledescription
     * @property {?string} ariaLive - aria-live attribute
     * @property {?string} ariaAtomic - aria-atomic attribute
     * @property {?string} ariaRelevant - aria-relevant attribute
     * @property {boolean} visible - control is visible
     * @property {boolean} active - control is active
     * @property {boolean} animated - whether the control is in an animated state
     * @property {boolean} disabled - control is disabled
     * @property {boolean} autoFocus - enable automatic focus; readonly
     * @property {boolean} autoEvent - enable automatic event listening; readonly
     * @property {boolean} enableDragDrop - enable drag & drop; readonly
     * @property {?Help4.control2.PositionLeftTop} dragPosition - initial position of drag & drop; readonly
     * @property {HTMLElement} __dom - DOM of this control
     * @property {HTMLDocument} __document - document of this control
     * @property {Object} ___metadata - metadata information
     * @property {?string} ___order - for {@link Help4.control2.container.SortedContainer}
     * @property {boolean} _isMouseOver
     * @property {?Help4.jscore.DragDrop} _dragDrop
     * @property {?HTMLElement} _dom
     * @property {?number} _state
     */
    Help4.control2.Control = class extends Help4.jscore.ControlBase {
        /**
         * @override
         * @param {Help4.control2.Control.Params} [params] - control configuration
         * @param {Help4.jscore.ControlBase.Params} [derived] - configurations from derived classes
         */
        constructor(params, derived) {
            const {ControlBase} = Help4.jscore;
            const T = ControlBase.TYPES;
            const TT = ControlBase.TEXT_TYPES;
            const id = Help4.createId();  // do not move in to simplify debugging!

            super(params, {
                params: {
                    id:                  {type: T.string, init: id, readonly: true},
                    css:                 {type: T.string_null, readonly: true},
                    tag:                 {type: T.string, init: 'div', readonly: true},
                    rtl:                 {type: T.boolean, readonly: true},
                    language:            {type: T.string_null, readonly: true},
                    mobile:              {type: T.boolean, readonly: true},
                    container:           {type: T.instance},

                    title:               {type: T.string_null},
                    role:                {type: T.string_null},
                    tabIndex:            {type: T.number_null},
                    zIndex:              {type: T.number_null},
                    contentLanguage:     {type: T.string_null, readonly: true},

                    ariaLabel:           {type: T.string_null},
                    ariaRoleDescription: {type: T.string_null},
                    ariaLive:            {type: T.string_null},
                    ariaAtomic:          {type: T.string_null},
                    ariaRelevant:        {type: T.string_null},

                    visible:             {type: T.boolean, init: true},
                    active:              {type: T.boolean},
                    disabled:            {type: T.boolean},
                    animated:            {type: T.boolean},

                    autoFocus:           {type: T.boolean, readonly: true},
                    autoEvent:           {type: T.boolean, readonly: true},
                    enableDragDrop:      {type: T.boolean},
                    dragPosition:        {type: T.leftTop_null, readonly: true},
                    dom:                 {type: T.element, init: window.document.body, readonly: true, private: true},
                    document:            {type: T.element, init: window.document, readonly: true, private: true},
                    _metadata:           {type: T.json, private: true},
                    _order:              {type: T.string_null, readonly: true, private: true}
                },
                config: {
                    css: 'control',
                    onPropertyChange: event => void this._applyPropertyToDom(event)
                },
                texts: {
                    title: TT.title
                },
                statics: {
                    _isMouseOver: {init: false, destroy: false},
                    _dragDrop:    {},
                    _dom:         {destroy: false},
                    _state:       {destroy: false}
                },
                derived
            });

            Help4.control2.Store.add(this);

            const {STATE} = this.constructor;

            this._state = STATE.init;
            this._onAfterInit(params || {});

            this._state = STATE.createDom;
            _createDom.call(this);

            this._state = STATE.ready;
            this._onReady();

            const {TYPES} = Help4.EventBus;
            const eventBus = Help4.getController().getService('eventBus');
            eventBus && eventBus.fire({type: TYPES.controlCreate, control: this});
        }

        static STATE = {
            init: 0,
            createDom: 1,
            ready: 2
        }

        /**
         * deliver the DOM elements that represents this control or some element inside
         * @param {string} [id] - a possible sub-dom-id
         * @returns {HTMLElement}
         */
        getDom(id) {
            return typeof id === 'string'
                ? this._dom.querySelector('#' + this.id + id)
                : this._dom;
        }

        /**
         * deliver the document that hosts this control
         * @returns {Document}
         */
        getDocument() {
            return this.__document;
        }

        /**
         * deliver the DOM that contains this control
         * @returns {HTMLElement}
         */
        getOwnerDom() {
            return this.__dom;
        }

        /**
         * destroys the control
         * @override
         */
        destroy() {
            // due to async nature of event flow, metadata information is deleted in destroy process
            // before event reaches the listener; hence passing this in-accessible information
            const {___metadata: metadata} = this;
            const eventBus = Help4.getController()?.getService('eventBus');
            eventBus?.fire({type: Help4.EventBus.TYPES.controlDestroy, control: this, metadata});
            this._fireEvent({type: 'destroy', control: this, metadata});

            this._onBeforeDestroy();

            Help4.control2.Store.remove(this);

            const {_dom} = this;
            if (_dom) {
                Help4.Event.stopObserving(_dom);
                _dom.parentNode?.removeChild(_dom);
            }

            const {id, container} = this;
            const [control, index] = container?.find(id) || [];  // find me within a possible container
            super.destroy();
            if (control === this) container.remove(index);  // remove me from container, if contained
        }

        /**
         * called after init
         * @protected
         * @param {Help4.control2.Control.Params} params - same params as provided to the constructor
         */
        _onAfterInit(params) {}
        /**
         * called before destroy
         * @protected
         */
        _onBeforeDestroy() {}
        /**
         * called once the DOM node is available but not yet attached
         * @protected
         * @param {HTMLElement} dom - control DOM
         */
        _onDomAvailable(dom) {}
        /**
         * called once the DOM node is attached
         * @protected
         * @param {HTMLElement} dom - control DOM
         */
        _onDomAttached(dom) {}
        /**
         * called once the DOM's creation is complete
         * @protected
         * @param {HTMLElement} dom - control DOM
         */
        _onDomCreated(dom) {}
        /**
         * called once the DOM's event listeners are attached and the DOM's automatic focus is applied
         * @protected
         * @param {HTMLElement} dom - control DOM
         */
        _onDomFocussed(dom) {}
        /**
         * called once control is completely initialized
         * @protected
         */
        _onReady() {}
        /**
         * called once the control starts drag & drop
         * @protected
         */
        _onDragStart() {
            _handleDragEvent.call(this, 'start');
        }
        /**
         * called on every move of the control during drag & drop
         * @protected
         */
        _onDragMove() {
            _handleDragEvent.call(this, 'move');
        }
        /**
         * called once drag & drop ends
         * @protected
         */
        _onDragStop() {
            _handleDragEvent.call(this, 'stop');
        }
        /**
         * called to configure drag & drop properly
         * @protected
         * @returns {Help4.control2.DragDropParams} - allows to define specific DOM (object) and dragable area (area)
         */
        _onGetDragDropParams() {
            return {object: this._dom};
        }

        /**
         * allows to store any external key-value information within this control
         * @param {string|Object} key - the key
         * @param {*} value - any value to be stored
         * @returns {Help4.control2.Control}
         */
        setMetadata(key, value) {
            const {___metadata} = this;
            typeof key === 'string'
                ? ___metadata[key] = value
                : Help4.extendObject(___metadata, key);
            this.___metadata = ___metadata;  // data tracking in ControlBase does only work for complete assignments!
            return this;
        }

        /**
         * get information from the metadata store
         * @param {string} keys - keys to retrieve the stored meta information
         * @returns {*}
         */
        getMetadata(...keys) {
            const {___metadata} = this;
            if (keys.length === 1) {
                return ___metadata[keys[0]];
            } else {
                const r = {};
                keys.forEach(key => r[key] = ___metadata[key]);
                return r;
            }
        }

        /**
         * get all currently stored metadata keys
         * @returns {string[]} - all currently stored keys
         */
        getMetadataKeys() {
            return Object.keys(this.___metadata);
        }

        /**
         * get possible connection points for this control
         * @param {Object} [params] - optional parameters to be used by derived classes
         * @returns {Help4.control2.ConnectionPoints} - points top,left,bottom,right and middle of this control
         */
        getConnectionPoints(params) {
            const {left, right, top, bottom, width, height} = this.getDom().getBoundingClientRect();
            const mx = left + (width >> 1);  // mid x
            const my = top + (height >> 1);  // mid y

            return {
                l: {x: left, y: my},
                r: {x: right, y: my},
                t: {x: mx, y: top},
                b: {x: mx, y: bottom},
                m: {x: mx, y: my}
            };
        }

        /**
         * get size of this control
         * @returns {Help4.control2.SizeWH} - size of this control's bounding client rect
         */
        getSize() {
            const {width: w, height: h} = this.getDom().getBoundingClientRect();
            return {w, h};
        }

        /**
         * get position of this control
         * @returns {Help4.control2.PositionXY} - position of this control within DOM
         */
        getPosition() {
            const {left: x, top: y} = this.getDom().getBoundingClientRect();
            return {x, y};
        }

        /**
         * get position and size of this control
         * @returns {Help4.control2.AreaXYWH} - area of this control within DOM
         */
        getArea() {
            const {left: x, top: y, width: w, height: h} = this.getDom().getBoundingClientRect();
            return {
                x: Math.floor(x),
                y: Math.floor(y),
                w: Math.floor(w),
                h: Math.floor(h)
            };
        }

        /**
         * adds an attribute to the DOM
         * @param {string} name
         * @param {*} value
         * @returns {Help4.control2.Control}
         */
        setAttribute(name, value) {
            Help4.Element.setAttribute(this.getDom(), {[name]: value});
            return this;
        }

        /**
         * add CSS classnames to this control
         * @param {string} classNames - class names to be added to this control's DOM
         * @returns {Help4.control2.Control}
         */
        addCss(...classNames) {
            Help4.Element.addClass(this.getDom(), ...classNames);
            return this;
        }

        /**
         * remove CSS classnames from this control
         * @param {string} classNames - class names to be removed from this control's DOM
         * @returns {Help4.control2.Control}
         */
        removeCss(...classNames) {
            Help4.Element.removeClass(this.getDom(), ...classNames);
            return this;
        }

        /**
         * tests some classnames on this control
         * @param {string} classNames - class names to be tested on this control's DOM
         * @returns {boolean}
         */
        hasCss(...classNames) {
            return Help4.Element.hasClass(this.getDom(), ...classNames);
        }

        /**
         * add styles to this control
         * @param {Object} styles - styles to be added to this control's DOM
         * @returns {Help4.control2.Control}
         */
        setStyle(styles) {
            Help4.extendObject(this.getDom().style, styles);
            return this;
        }

        /**
         * focus the control's DOM
         * @returns {Help4.control2.Control}
         */
        focus() {
            !this.mobile && this.getDom().focus();
            return this;
        }

        /**
         * suspend drag & drop operation
         * @returns {Help4.control2.Control}
         */
        suspendDragDrop() {
            if (this._dragDrop) {
                _updateDragPosition.call(this);
                const {style} = this.getDom();
                style.removeProperty('left');
                style.removeProperty('top');
                this._destroyControl('_dragDrop');
            }
            return this;
        }

        /**
         * resume drag & drop operation
         * @returns {Help4.control2.Control}
         */
        resumeDragDrop() {
            if (!this._dragDrop) {
                const {dragPosition} = this;
                if (dragPosition) {
                    const {left, top} = dragPosition;
                    this.setStyle({
                        left: `${left}px`,
                        top: `${top}px`
                    });
                }
                if (this.enableDragDrop) _enableDragDrop.call(this);
            }
            return this;
        }

        /**
         * handle incoming events
         * @param {Object} event - the received event
         */
        onEvent(event) {
            let {type} = event;

            switch (type) {
                case 'mouseover':
                    this._isMouseOver = true;
                    break;
                case 'mouseout':
                    this._isMouseOver = false;
                    break;
                case 'mouseup':
                    if (event.keyCode === 2) type = 'middleclick';
                    break;
                case 'click':
                    if (this.enableDragDrop) return;  // suppress the 'click' event triggered by mouse up during drag stop
                    if (event.ctrlKey) type = 'middleclick';
                    break;
                case 'keyup':
                    const key = Help4.service.HotkeyService.getKey(event);
                    if (key === 'enter' || key === 'space') type = key;
                    break;
            }

            this._fireEvent({type, data: event});
        }

        /**
         * calculate whether the mouse is on top of my DOM
         * @returns {boolean} - whether mouse is over my DOM
         */
        isMouseOver() {
            const dom = this.getDom();
            if (!dom) return false;

            return dom.matches
                ? dom.matches(':hover')
                : dom.msMatchesSelector(':hover');
        }

        /**
         * get all texts within my DOM; they can then be used for e.g. automatic machine learning translation (MLT)
         * @returns {Object|null}
         * @throws {Error}
         */
        getControlTexts() {
            const texts = this.dataFunctions.getTexts();
            const dom = this.getDom();
            const result = {};

            const {Store} = Help4.control2;
            const {contentLanguage: language} = this;
            const shell = Help4.getShell();
            const {uacp = null} = language && shell.getLanguage(language) || {};

            // @var {string} textId
            // @var {{type: string, set: function, get: function}} config
            for (const [textId, config] of Object.entries(texts)) {
                if (!config.get) throw new Error(`Bad configuration for text id "${textId}"`);

                const list = _getTextNodeList(dom, textId);
                let value, attrs;

                for (const node of list) {
                    attrs = _getTextAttributeList(node);

                    if (attrs.indexOf(textId) >= 0 &&
                        (value = config.get(node)) &&
                        Store.find(node) === this)
                    {
                        result[textId] = {
                            data: value,
                            contentType: config.contentType,
                            language: uacp
                        };
                        break;
                    }
                }
            }

            return Object.keys(result).length ? result : null;
        }

        /**
         * exchange all texts of my control; e.g. after successful MLT
         * @param {Object} textMap
         * @returns {Help4.control2.Control}
         * @throws {Error}
         */
        setControlTexts(textMap) {
            const {Store} = Help4.control2;
            const texts = this.dataFunctions.getTexts();
            const dom = this.getDom();

            let hasChanges = false;
            for (const [textId, value] of Object.entries(textMap)) {
                if (!value) continue;

                const config = texts[textId];
                if (!config.set) throw new Error(`Bad configuration for text id "${textId}"`);

                const list = _getTextNodeList(dom, textId);
                for (const node of list) {
                    const attrs = _getTextAttributeList(node);

                    if (attrs.indexOf(textId) >= 0 &&
                        Store.find(node) === this)
                    {
                        hasChanges = config.set(node, value) || hasChanges;
                        break;
                    }
                }
            }

            hasChanges && this._fireEvent({type: 'afterSetControlTexts', control: this});

            return this;
        }

        /**
         * get the texts within my control and within all controls that are within my control
         * @param {boolean} [onlyVisible = false]
         * @returns {Object|null}
         */
        getTexts(onlyVisible = false) {
            if (onlyVisible && !this.visible) return null;

            const controlTexts = {};
            const controls = [...this.getHostedControls(onlyVisible), this];
            for (const control of controls) {
                const texts = control.getControlTexts();
                if (texts) controlTexts[control.id] = texts;
            }

            const textMap = {};
            for (const [controlId, texts] of Object.entries(controlTexts)) {
                for (const [textKey, text] of Object.entries(texts)) {
                    textMap[controlId + '.' + textKey] = text;
                }
            }
            return Object.keys(textMap).length ? textMap : null;
        }

        /**
         * set the texts within my control and within all controls that are within my control
         * @param {Object} textMap
         * @returns {Help4.control2.Control}
         */
        setTexts(textMap) {
            const controlTexts = {};
            for (const [combinedId, text] of Object.entries(textMap)) {
                const [controlId, textId] = combinedId.split('.');
                if (!controlTexts[controlId]) controlTexts[controlId] = {};
                controlTexts[controlId][textId] = text;
            }

            const {Store} = Help4.control2;
            const controls = [...this.getHostedControls(), this];
            for (const [controlId, texts] of Object.entries(controlTexts)) {
                const control = Store.get(controlId);
                if (control && controls.indexOf(control) >= 0) {  // only do for controls hosted by me and me
                    control.setControlTexts(texts);
                }
            }

            return this;
        }

        /**
         * @param {boolean} [onlyVisible] - deliver only currently visible controls
         * @returns {Help4.control2.Control[]}
         */
        getHostedControls(onlyVisible = false) {
            const html = this.getDom().innerHTML;
            const matches = [...html.matchAll(/ id=(['"])(.*?)\1/g)];
            const controls = [];

            const {Store} = Help4.control2;

            let control;
            for (const [match, quote, id] of matches) {
                if (control = Store.get(id)) {
                    if (onlyVisible && !control.visible) continue;
                    controls.push(control);
                }
            }

            return controls;
        }

        /** @returns {?string} */
        getOrder() {
            return this.___order;
        }

        /**
         * used to support text extraction and replacement
         * @param {HTMLElement} node
         * @param {string} textId
         * @protected
         */
        _setTextAttribute(node, ...textId) {
            let attrs = _getTextAttributeList(node);
            attrs.push(...textId);
            attrs = [...new Set(attrs)].join(',');
            node.setAttribute(DATA_TEXT_ATTRIBUTE, attrs);
        }

        /**
         * used to support text extraction and replacement
         * @param {HTMLElement} node
         * @param {string} textId
         * @protected
         */
        _removeTextAttribute(node, ...textId) {
            const attrs = _getTextAttributeList(node)
            .filter(item => textId.indexOf(item) < 0)
            .join(',');

            attrs
                ? node.setAttribute(DATA_TEXT_ATTRIBUTE, attrs)
                : node.removeAttribute(DATA_TEXT_ATTRIBUTE);
        }

        /**
         * used to support text extraction and replacement
         * @param {HTMLElement} node
         * @param {string} textId
         * @param {boolean} value
         * @protected
         */
        _applyTextAttribute(node, textId, value = true) {
            value
                ? this._setTextAttribute(node, textId)
                : this._removeTextAttribute(node, textId);
        }

        /**
         * creates a new DOM element within my control's DOM
         * @param {string} tagName - tag name of new DOM element
         * @param {Object} params - additional parameters
         * @returns {HTMLElement}
         * @protected
         */
        _createElement(tagName, params = {}) {
            if (typeof params.id === 'string' && params.id !== '') {
                params.id = this.id + params.id;
            }
            params.dom ||= this.getDom();
            return Help4.Element.create(tagName, params);
        }

        /**
         * @protected
         * @param {Function} object
         * @param {Help4.control2.Control.Params} params
         * @returns {Help4.control2.Control}
         */
        _createControl(object, params) {
            return Help4.control2.createControl(object, this, params);
        }

        /**
         * is called on data changes of my control
         * should be used to apply this changes to my DOM
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         * @protected
         */
        _applyPropertyToDom({name, value}) {
            const {
                Element,
                control2: {Control: {STATE}}
            } = Help4;

            switch (name) {
                case 'visible':
                    this.getDom().style.visibility = this._state <= STATE.createDom && value === false ? 'hidden' : '';
                    value ? this.removeCss('hidden') : this.addCss('hidden');
                    break;

                case 'active':
                case 'disabled':
                case 'animated':
                    value ? this.addCss(name) : this.removeCss(name);
                    break;

                case 'zIndex':
                    this.getDom().style.zIndex = value;
                    break;

                case 'title':
                    const d = this.getDom();
                    Element.setAttribute(d, {title: value});
                    this._applyTextAttribute(d, 'title', !!value);
                    break;

                case 'role':
                case 'tabIndex':
                case 'ariaLabel':
                case 'ariaRoleDescription':
                case 'ariaLive':
                case 'ariaAtomic':
                case 'ariaRelevant':
                    const o = {};
                    o[name] = value;
                    Element.setAttribute(this.getDom(), o);
                    break;

                case 'enableDragDrop':
                    value ? _enableDragDrop.call(this) : _disableDragDrop.call(this);
                    break;
            }
        }

        /** @param {HTMLElement} dom */
        setDom(dom) {
            const {__dom} = this;
            if (__dom !== dom) {
                this.dataFunctions.set({dom}, {allowReadonlyOverride: true});  // allow readonly override
                this.dataFunctions.set({document: dom.ownerDocument}, {allowReadonlyOverride: true});  // allow readonly override
                dom.appendChild(this._dom);
            }
        }
    }

    /**
     * creates the DOM of this control
     * @memberof Help4.control2.Control#
     * @private
     */
    function _createDom() {
        const {id, tag, css, visible, __dom} = this;
        const dom = this._dom = Help4.Element.create(tag, {id: id, css: css});

        // XRAY-4669: prevent flickering of controls that should be invisible
        if (!visible) this._applyPropertyToDom({name: 'visible', value: visible, oldValue: visible});

        this._onDomAvailable(dom);

        if (!__dom) return;
        __dom.appendChild(dom);

        this._onDomAttached(dom);

        const {autoEvent, autoFocus} = this;
        autoEvent && Help4.Event.observe(dom, {
            type: 'control',
            controlId: id
        });

        this._onDomCreated(dom);

        autoFocus && this.focus();

        this._onDomFocussed(dom);

        this.resumeDragDrop();

        // apply state to DOM
        this.dataFunctions.forEach(name => {
            const value = this[name];  // do not invoke getter twice
            this._applyPropertyToDom({name, value, oldValue: value});
        });
    }

    /**
     * enables drag & drop for this control
     * @memberof Help4.control2.Control#
     * @private
     */
    function _enableDragDrop() {
        if (this._dragDrop) return;

        const params = this._onGetDragDropParams();
        const dd = this._dragDrop = new Help4.jscore.DragDrop(params);

        dd.onStart.addListener(() => this._onDragStart());
        dd.onMove.addListener(() => this._onDragMove());
        dd.onEnd.addListener(() => this._onDragStop());
    }

    /**
     * disables drag & drop for this control
     * @memberOf Help4.control2.Control#
     * @private
     */
    function _disableDragDrop() {
        this._destroyControl('_dragDrop');
        this.dataFunctions.set({dragPosition: {left: 0, top: 0}}, {allowReadonlyOverride: true});  // this.dragPosition is readonly; allow override
    }

    /**
     * gets all text attributes for a DOM node
     * @memberof Help4.control2.Control#
     * @param {HTMLElement} node - the DOM node
     * @returns {string[]}
     * @private
     */
    function _getTextAttributeList(node) {
        return (node.getAttribute(DATA_TEXT_ATTRIBUTE) || '')
        .split(',')
        .map(value => value.trim())
        .filter(value => value !== '');
    }

    /**
     * finds all nodes with a certain text attribute
     * @memberof Help4.control2.Control#
     * @param {HTMLElement} dom - the start node
     * @param {string} textId - the text attribute
     * @returns {HTMLElement[]} unique list of elements
     * @private
     */
    function _getTextNodeList(dom, textId) {
        const nodeList = dom.querySelectorAll(`[${DATA_TEXT_ATTRIBUTE}*=${textId}]`);
        const domList = [].slice.call(nodeList);
        domList.unshift(dom);
        return [...new Set(domList)];
    }

    /**
     * updates the current drag position
     * @memberof Help4.control2.Control#
     * @private
     */
    function _updateDragPosition() {
        let {left, top} = this.getDom().style;
        left = parseInt(left);
        top = parseInt(top);
        this.dataFunctions.set({dragPosition: {left, top}}, {allowReadonlyOverride: true});  // this.dragPosition is readonly; allow override
    }

    /**
     * fires the dragdrop event, common for start, move and stop
     * @memberof Help4.control2.Control#
     * @private
     * @param {'start'|'move'|'stop'} action - type of drag action
     */
    function _handleDragEvent(action) {
        _updateDragPosition.call(this);
        this._fireEvent({type: 'dragdrop', position: this.dragPosition, action});
    }
})();