/** * Represents the most basic and simple form of a Component. * It is mainly a collection of wrapper methods * around the HTMLElement methods to make them chainable. * It serves as base for further functionallity extensions. * @extends StyleAndScriptStoringComponent * @inheritdoc * */ class Component extends StyleAndScriptStoringComponent { /** * @type {boolean} */ _isCompel; /** * @type {Array} */ _toRegister; /** * @type {boolean} */ _isContextMenu; /** * Initializes the component * @param {HTMLElement} element the base element * @param {Map} attr Specific already known attributes */ constructor(element, attr = {}) { super(element, attr); this._isCompel = false; this._isContextMenu = false; this._modifier = new Modifier() .margin(0); this._modifier._modifications['display'] = "flex"; this._modifier._modifications["box-sizing"] = "border-box"; this._toRegister = []; } /** * Adds a class to classList via HTMLElement.classList.add() method. * Further collects rules in a property until generate is called. * * @CAUGHTION implementation is not safe to use, ignoring extStore is recommended; * * @todo difference between stylings and classes, extStore logic in combination with the Page.register... logic * * @override * * @param {string} styleClass (without the '.' in the front) * @param {string|Modifier|map} styling * @param {ExtStorage|ExtStoreType|ExtStorePosition|OverwriteBehaviour|EXPosConfer|ESOverwriteConfer} extStore * if a unique definition is desired, all constants or configurator objects are allowed - they will be processed accordingly * @returns {Component} this component object */ addStyleClass(styleClass, styling = null, extStore = null) { if (!extStore) { extStore = this._styleClassesExtStore; } else if (extStore.isMissing()) { extStore = extStore.fillBy(this._styleClassesExtStore); } if (styling) { if (styling instanceof Modifier) { styling = styling._modifications; } Page.registerStyling('.' + styleClass, styling); } this._element.classList.add(styleClass); return this; } /** * * @param {boolean} vertical Defines if the Component should overflow vertically (default: true) * @param {boolean} horizontal Defines if the Component should overflow horizontally (default: false) * @returns {Component} */ overflow(vertical = true, horizontal = false ) { if (vertical || horizontal) { this._modifier.join( new Modifier() .removeStyleRule("flex") .setStyleRule("overflow", "hidden auto") ); this.subscribeOnGenerate(CommonCompelGroups.OVERFLOWING); } if (vertical) { this._modifier._modifications["overflow-y"] = "hidden auto"; } if (horizontal) { this._modifier._modifications["overflow-x"] = "hidden auto"; } return this; } /** * * @param {boolean} untilFound * @returns {Component} */ hidden(untilFound = false) { let styleClass = "compel-mech-hidden"; let hid = "hidden"; Page.registerStyling("." + styleClass, { [hid]: hid }); this.addStyleClass(styleClass); this._modifier.removeStyleRule("display"); this.setAttribute( hid, (untilFound ? "until-found" : hid) ); this.subscribeOnGenerate(CommonCompelGroups.HIDDEN_ON_START); return this; } /** * Subscribes element under higher_compel group * sets corr. variable true * setAttribute("data-compel-isHCompel", "true") * * @returns {Component} */ isHigherComponent() { this.subscribeOnGenerate(CommonCompelGroups.HIGHER_COMPEL); this._isCompel = true; this.addStyleClass("compel-higher"); return this.setAttribute("data-compel-isHCompel", "true") } /** * * @returns {string} */ getHigherCompelSelector() { return this._element .closest('[data-compel-isHCompel="true"].compel-higher') .getAttribute("data-autocompel"); } /** * Collects the given List in the _toRegister attribute. * When generate() is called, * the created Element will be registered (added) in every list * within the list. * @param {*|string|Array<*>} listName */ subscribeOnGenerate(listName) { this._toRegister.push(listName); return this; } /** * * @returns {Component} */ registerAsContextMenu() { this.subscribeOnGenerate(CommonCompelGroups.IS_CONTEXT_MENU); this._isContextMenu = true; return this.addStyleClass('contextmenu') .hidden(); } /** * @todo Positioning of the contextmenu element * @todo extract into an extra function(allity) provider * * @param {Component} component * @param {Sides|Function => Sides} getRefPos * @param {ExtStorage} [extStore=null] * @returns {Component} */ contextMenu(component, getRefPos = null, extStore = null) { if (!component._isContextMenu) { component.registerAsContextMenu(); } if (!getRefPos) { getRefPos = function (cmEvent) { return new Sides() .left(cmEvent.pageX) .top(cmEvent.pageY); } } this.subscribeOnGenerate(CommonCompelGroups.HAS_CONTEXT_MENU); let identifier = component._compName; this.addEventListener( "contextmenu", DefaultContextMenu.openContextMenuAction(identifier, getRefPos) ); return this.childContext(component); } /** * * @param {DragAndDropImplementation} dadImpl * @returns {Component} */ draggable(dadImpl = new DragAndDropImplementation()) { this.subscribeOnGenerate(CommonCompelGroups.DRAGGABLE); this.subscribeOnGenerate(CommonCompelGroups.HAS_DRAG_EVENT); let addedClass = "comp-el-mech-draggable"; let selector = this._element.getAttribute("data-autocompel"); selector = `.${addedClass}[data-autocompel="${selector}"]`; return this.addStyleClass(addedClass) .setAttribute("draggable", "true") .setAttribute("dropEffect", "none") .addEventListener( CommonEvents.DRAG_START, dadImpl.dragStartAction("text/plain", selector) ); } /** * * @param {EventDrag} dragEvent * @param {Function} action */ onDrag(dragEvent, action = (e) => { e.preventDefault(); }) { this.subscribeOnGenerate(CommonCompelGroups.HAS_DRAG_EVENT); let selector = `comp-el-mech-drag${dragEvent}`; return this.addEventListener( 'drag' + dragEvent, action ); } /** * * @param {DragAndDropImplementation} dadImpl * @returns {Component} */ dropTarget(dadImpl = new DragAndDropImplementation()) { this.subscribeOnGenerate(CommonCompelGroups.DROP_TARGET); let specialClass = "comp-el-mech-droptarget"; this.addStyleClass(specialClass) .onDrag(EventDrag.OVER); let selector = `.${specialClass}[data-autocompel="${this._compName}"]` this._element.addEventListener( "drop", dadImpl.dropEventAction("text/plain", selector) ); return this; } /** * An echo of Scope-Functions from kotlin for convenience * * Executes a given function injects this component into the function. * @param {Function} func * @returns {Component} */ apply(func) { func(this); return this; } /** * An echo of Scope-Functions from kotlin for convenience * * Executes a given function injects the htmlelement of this component into the function. * @param {Function} func * @returns {Component} */ applyToEl(func) { func(this._element) return this; } /** * Ends chain. * Applies all modifications on the element. * Processes all stored additions. * Returns the constructed HTMLElement of this Component. * * @param {CompelGenerator} generator * @param {Modifier | undefined} [modifier=null] * @param {ExtStorage | undefined} [styleStore=null] * @param {ExtStorage | undefined} [functionStore=null] * @param {ExtStorage} * @returns {WebTrinity} the constructed HTMLElement of this Component. */ generate(generator = singlepage, styleStore = null, functionStore = null) { /** * In the case that this component is a chainChild created one. * The generation chain needs to be setup in the proper order * so that the resulting element tree equals the expected/desired result. * * Therefore if this is a chainChild, * it will be added to the parent via the regular childContext * and the generation of the parent will be returned. * * The parent will generate this component on its generate(). */ if (this._parentComponent) { let parent = this._parentComponent; this._parentComponent = null; return parent.childContext(this) .generate(generator, styleStore, functionStore); } return generator.generate(this, styleStore, functionStore); } }