/** * 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) { this.subscribeOnGenerate(CommonCompelGroups.OVERFLOWING); this._modifier._modifications["overflow-y"] = "auto"; } if (horizontal) { this.subscribeOnGenerate(CommonCompelGroups.OVERFLOWING); this._modifier._modifications["overflow-x"] = "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; } /** * Defines how a child Component is to be appended. * @param {Component} component the child component to add it. * @returns {HTMLElement} */ _appendChildComponent(component) { let child = new WebTrinity(); if (component instanceof Component) { child = component.generate(); } if (component instanceof WebTrinity) { child = component; } if (component instanceof HTMLElement) { console.log("No wenity set - htmlEl was given"); child.html = component; } this._element.append(child.html); return child; } /** * * @param {ExtStorage} extStore * @returns {Array} */ _processStyles(extStore = null) { extStore = (extStore ? extStore : this._stylesExtStore ) .setupForGeneralStyling(); let forCollection = []; let counter = 0; for (const ssd of this._styles) { /* Make sure that the type is unified for later processing */ if (ssd._definition instanceof Modifier) { ssd._definition = ssd._definition._modifications; } /* Check/Ensure proper ExtStorageType for following comparison */ /** * @type {ExtStorage} */ let curExtStore = extStore; if (Object.hasOwn(ssd, "_extStore") && ssd._extStore) { curExtStore = ssd._extStore.setupForGeneralStyling(); } if (curExtStore.getStylingDistribution()(ssd, this._element, counter)) { forCollection.push(ssd); } } return forCollection; } /** * * @param {ExtStorage} extStore * @returns {Array} */ _processFunctions(extStore = null) { extStore = (extStore ? extStore : this._functionsExtStore ) .setupForFunctions(); const forCollection = new Map(); const collectForBefore = []; let counter = 0; for (const ssd of this._functions) { /* Make sure that the type is unified for later processing */ let curExtStore = extStore; if (Object.hasOwn(ssd, "_extStore") && ssd._extStore) { curExtStore = ssd._extStore.setupForFunctions(); } if (curExtStore.getFunctionDistribution()(ssd, counter)) { if (curExtStore._position.BEFORE) { collectForBefore.push(ssd); } else { if (!forCollection.has(curExtStore)) { forCollection.set(curExtStore, []); } forCollection.get(curExtStore).push(ssd); } } } return forCollection; } /** * Ends chain. * Applies all modifications on the element. * Processes alls stored additions. * Returns the constructed HTMLElement of this Component. * * * @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(modifier = null, generator, styleStore = null, functionStore = null) { if (this._parentComponent) { let parent = this._parentComponent; this._parentComponent = null; return parent.childContext(this) .generate(modifier, styleStore, functionStore); } if (modifier) { this._modifier = modifier .join(this._modifier); } let _wenity = new WebTrinity(); if (!styleStore) { styleStore = this._stylesExtStore; } /* DEAL WITH COMPONENT MODIFICATION FIRST */ // @todo pay attention to the "overwrite" behaviour - the local modifier styles are the "closest" // it might be appropriate to use this._styles.unshift(...) instead. this._styles.push(new SStoreDefinition( (styleStore._aggregation !== ESAggregation.INTERNALIZED ? "." : "") + this._compName, this._modifier, this._stylesExtStore )); /* DEAL WITH CHILDREN */ let collectedWenities = []; for (let i = 0; i < this._children.length; i++) { /** * @type {Component} */ let child = this._children[i]; if (child instanceof ChainableModifier) { child = child.toComponent(); } if (Page._useCssCalc) { child._modifier._updateDimensionsBy(this._modifier._paddingValues); } child = child.generate(modifier, styleStore, functionStore); let wenity = this._appendChildComponent(child); if (!wenity.isSSEmpty()) { collectedWenities.push(wenity); } } /* DEAL WITH STYLING AND PROCESSING */ /** * @type {Array} */ let styleCollection = this._processStyles(styleStore); /** * @type {Map>} */ const funcCollections = this._processFunctions(functionStore); /** * checks if the source has the extStoreType * fills the target extStoreType-array with the corr. elements of source. * @param {Map>} source * @param {Map>} target * @param {ExtStoreType} extStoreType * @returns */ function transferCollectedFunctions(source, target, extStoreType) { if (source) { if (source.has(extStoreType)) { if (funcCollections.has(extStoreType)) { target.get(extStoreType) .push(source.get(extStoreType)) } else { target.set( extStoreType, source.get(extStoreType) ); } } } return target; } /** * Executes the given function upon the delegating ExtStoreTypes * @param {*} func * @returns {Map transferCollectedFunctions(child.js, funcCollections, extstoretype) ); } } if (this._isCompel) { /** * * @param {Map} map * @param {ExtStorageType} extStoreType */ function dealCollected(map, extStoreType) { if (map.has(extStoreType)) { let collectionScriptTag = generateAndFillScriptTag(map.get(extStoreType)); if (extStoreType === ExtStoreType.COLLECTED_SEGMENT_BEGIN) { this._element.insertAdjacentElement( "afterbegin", generateAndFillScriptTag(segment) ); } else { Page.addElementToPage( collectionScriptTag, extStoreType ); } } } executeOnExtStoreTypeCollectedTriple((est) => dealCollected(funcCollections, est)); } else { _wenity.js = funcCollections; _wenity.css = styleCollection; } _wenity.html = this._element; for (const group of this._toRegister) { Page.subscribeComponentToGroup(group, this._compName) } return _wenity; } }