(function() {
/**
* @namespace container
* @memberof Help4.control2
*/
Help4.control2.container = {};
/**
* @typedef {Help4.control2.Control.Params} Help4.control2.container.Container.Params
* @property {string} [type = 'Control'] - default type of the hosted controls
* @property {any[]} [data = []] - to be created controls on container creation
* @property {?Help4.control2.AreaXYWH} [virtualArea = null] - virtual DOM area that the container should see as its space (containers will always have size 0x0 - but can use virtual space)
*/
/**
* A container to structure some controls. Will also provide a container element for those controls within the DOM.
* @augments Help4.control2.Control
* @property {string} type - default type of the hosted controls
* @property {any[]} data - to be created controls on container creation
* @property {?Help4.control2.AreaXYWH} virtualArea - virtual DOM area that the container should see as its space (containers will always have size 0x0 - but can use virtual space)
*/
Help4.control2.container.Container = class extends Help4.control2.Control {
/**
* @override
* @param {Help4.control2.container.Container.Params} [params]
* @param {Help4.jscore.ControlBase.Params} [derived]
*/
constructor(params, derived) {
const T = Help4.jscore.ControlBase.TYPES;
super(params, {
params: {
type: {type: T.string, init: 'Control', readonly: true},
data: {type: T.array, private: true, readonly: true},
virtualArea: {type: T.xywh_null}
},
statics: {
_store: {init: [], destroy: false}
},
config: {
css: 'control-container'
},
derived
});
}
/** @override */
_onBeforeDestroy() {
this.clean();
}
/**
* @override
* @param {HTMLElement} dom - control DOM
*/
_onDomCreated(dom) {
const {__data} = this;
if (__data.length) this.add(...__data);
}
/** @returns {Help4.control2.container.Container} */
clean() {
const {_store} = this;
// control.destroy() will invoke this.remove(<index>)
// and modify _store outside of this loop
// therefore DO NOT use an index based loop here!
let control;
while (control = _store[0]) {
control.destroy();
}
this._store = [];
return this;
}
/**
* create controls out of parameters and add them to this container
* @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
* @returns {?Help4.control2.Control|Array<?Help4.control2.Control>} the added control(s)
*/
add(...params) {
const toAdd = params.length;
const added = new Array(toAdd);
for (const [index, item] of Help4.arrayEntries(params)) {
added[index] = _addFromObject.call(this, item);
}
return toAdd === 1 ? added[0] : added;
}
/**
* replaces a control at the given position
* @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
* @param {string} controlId - the control id of the to-be-replaced control
* @returns {?Help4.control2.Control} the added control
*/
replace(params, controlId) {
const index = this.getIndex(controlId);
if (index < 0) return null;
const control = this.insertAt(params, index);
this.remove(controlId);
return control;
}
/**
* moves a control to a certain position
* @param {string} controlId - the control id of the to-be-moved control
* @param {number} index - new position
*/
moveTo(controlId, index) {
const {/** @type {Help4.control2.Control[]} */ _store} = this;
const control = this.get(controlId);
const current = control && this.getIndex(controlId);
if (!control || current === index) return;
const dom = this.getDom();
const controlDom = control.getDom();
if (index >= _store.length) {
// move to end
dom.appendChild(controlDom);
_store.splice(current, 1);
_store.push(control);
} else {
const successor = _store[index];
const successorDom = successor.getDom();
dom.insertBefore(controlDom, successorDom);
_store.splice(current, 1);
_store.splice(index, 0, control);
}
}
/**
* inserts a control at the given position
* @param {Help4.control2.Control.Params} params - parameters for the to-be-created controls
* @param {number} index - the insert position
* @returns {?Help4.control2.Control} the added control
*/
insertAt(params, index) {
const {/** @type {Help4.control2.Control[]} */ _store} = this;
// simple: insert at end
if (index >= _store.length) return this.add(params);
// complex: insert before another control
// insert control as usual
const control = /** @type {?Help4.control2.Control} */ this.add(params);
if (control) {
// get control at target position (the to-be-successor)
const successor = /** @type {Help4.control2.Control} */ _store[index];
// get DOM nodes and move new control from end to target position
const dom = this.getDom();
const successorDom = successor.getDom();
const controlDom = control.getDom();
dom.insertBefore(controlDom, successorDom);
// adopt internal _store: move new control from end to target position
_store.splice(index, 0, _store.pop());
return control;
}
return null;
}
/**
* is able to find a control also within sub containers
* @param {string} controlId
* @returns {Array<Help4.control2.Control, number, Help4.control2.container.Container>|Array<null,null,null>}
*/
find(controlId) {
const {Container} = Help4.control2.container;
for (const [index, control] of this.entries()) {
if (control.id === controlId) {
return [control, index, this];
} else if (control instanceof Container) {
const value = control.find(controlId);
if (value[0]) return value;
}
}
return [null, null, null];
}
/**
* @param {*} controlId - index of control in container, or control ID or metadata information
* @returns {?Help4.control2.Control}
*/
get(controlId) {
if (typeof controlId === 'number') { // controlId is index
return this._store[controlId] || null;
} else if (typeof controlId === 'string') { // controlId is control id
const [control] = this.find(controlId);
return control;
} else if (typeof controlId === 'object' && controlId) {
// special get mode: e.g. {byMetadata: {id: '1'}}
const k = Object.keys(controlId)[0];
if (k === 'byMetadata') {
const [control] = _getByMetadata.call(this, controlId[k]);
return control;
}
}
return null;
}
/**
* @param {*} controlId - index of control in container, or control ID or metadata information
* @returns {Help4.control2.Control|null} the removed control
*/
remove(controlId) {
if (typeof controlId === 'number') { // controlId is an index
const {_store} = this;
const control = _store[controlId];
if (control) {
_store.splice(controlId, 1);
control.destroy();
return control;
}
} else if (typeof controlId === 'string') { // controlId is control id
const [control, index, container] = this.find(controlId);
if (control) return container.remove(index);
} else if (typeof controlId === 'object' && controlId) {
// special get mode: e.g. {byMetadata: {id: '1'}}
const k = Object.keys(controlId)[0];
if (k === 'byMetadata') {
const [control, index, container] = _getByMetadata.call(this, controlId[k]);
if (control) return container.remove(index);
}
}
return null;
}
/**
* @param {string} controlId
* @returns {number}
*/
getIndex(controlId) {
for (const [index, control] of this.entries()) {
if (control.id === controlId) return index;
}
return -1;
}
/** @returns {number} */
count() {
return this._store.length;
}
/** @returns {Help4.control2.AreaXYWH} */
getArea() {
const {virtualArea} = this;
if (virtualArea) return virtualArea;
let {x, y, w, h} = super.getArea();
if (!x && !y && !w && !h) {
// all containers that have no defined area are considered "virtual"
// and might contain controls all over the window
// e.g. consider a container with only controls that are "position: absolute"
w = window.innerWidth;
h = window.innerHeight;
}
return {x, y, w, h};
}
/**
* @param {Help4.typedef.MapExecutor} executor
* @returns {Array<*>}
*/
map(executor) {
return this._store.map((control, index, store) => executor(control, index, store, this));
}
/**
* iterates the container
* @param {Help4.typedef.ForEachExecutor} executor
*/
forEach(executor) {
this._store.forEach((control, index, store) => executor(control, index, store, this));
}
/**
* iterates the container
* @param {Help4.typedef.EveryExecutor} executor
* @returns {boolean}
*/
every(executor) {
return this._store.every((control, index, store) => executor(control, index, store, this));
}
/**
* for (const [index, control] of container.entries()) {
* ...
* }
*
* @generator
* @yields [number, Help4.control2.Control]
*/
*entries() {
// do not cache _store.length as it might change during the loop
const {_store} = this;
let index = 0;
while (_store[index]) {
yield [index, _store[index]];
index++;
}
}
/**
* for (const control of container) {
* ...
* }
*
* @returns {{next: (function(): {value: *, done: boolean})}}
*/
[Symbol.iterator]() {
let index = -1;
return {
next: () => ({
value: this._store[++index],
done: !(index in this._store)
})
};
}
}
/**
* @memberof Help4.control2.container.Container#
* @param {Object} metadata
* @returns {Array<Help4.control2.Control, number, Help4.control2.container.Container>|Array<null, null, null>}
* @private
*/
function _getByMetadata(metadata) {
const {Container} = Help4.control2.container;
const keys = Object.keys(metadata);
for (const [index, control] of this.entries()) {
if (keys.every(key => control.getMetadata(key) === metadata[key])) {
return [control, index, this];
} else if (control instanceof Container) {
const value = _getByMetadata.call(control, metadata);
if (value[0]) return value;
}
}
return [null, null, null];
}
/**
* @memberof Help4.control2.container.Container#
* @param {Object} object
* @returns {?Help4.control2.Control}
* @private
*/
function _addFromObject(object) {
if (object.id && this.get(object.id)) return null; // duplicate check
const {control2} = Help4;
const {_store, type} = this;
control2.deriveClassParams(this, object);
object.container = this;
const classObject = control2.getClass(object.controlType || type);
if (classObject) {
const control = /** @type {Help4.control2.Control} */ new classObject(object)
.addListener('*', event => void this._fireEvent(event));
_store.push(control);
return control;
}
return null;
}
})();