/**
* Refer to <span color = "yellow">{@link ve.Component}</span> for methods or fields inherited from this Component's parent such as `.options.attributes` or `.element`.
*
* Hierarchy used for organising both nested and un-nested lists via item/group distinctions.
* - Functional binding: <span color=00ffff>veHierarchy</span>().
*
* ##### Constructor:
* - `arg0_components_obj`: {@link Object}<{@link ve.Component}|{@link ve.HierarchyDatatype}> - The individual items to append to the current hierarchy.
* - `arg1_options`: {@link Object}
* - `.allow_disabled_reordering=false`: {@link boolean}
* - `.disable_searchbar=false`: {@link boolean}
* - `.namespace=Class.generateRandomID(ve.Hierarchy)`: {@link string}
* - `.searchbar_style`: {@link Object} - The Telestyle object to apply to the searchbar.
*
* ##### Instance:
* - `.components_obj`: {@link Object}<{@link ve.Component}|{@link ve.HierarchyDatatype}>
* - `.nestable`: {@link Nestable}
* - `.v`: {@link this.components_obj} - Accessor. The current components_obj mounted to the ve.Hierarchy.
*
* ##### Methods:
* - <span color=00ffff>{@link ve.Hierarchy.addItem|addItem}</span>(arg0_parent_el:{@link HTMLElement}, arg1_hierarchy_datatype:{@link ve.HierarchyDatatype})
* - <span color=00ffff>{@link ve.Hierarchy.getHierarchyArray|getHierarchyArray}</span>(arg0_options:{flatten_object: {@link boolean} }) | {@link Object[]}
* - <span color=00ffff>{@link ve.Hierarchy.getHierarchyObject|getHierarchyObject}</span>(arg0_options:{flatten_object: {@link boolean} }) | {@link Object}
* - <span color=00ffff>{@link ve.Hierarchy.removeItem|removeItem}</span>(arg0_hierarchy_datatype:{@link ve.HierarchyDatatype})
*
* @augments ve.Component
* @memberof ve.Component
* @type {ve.Hierarchy}
*/
ve.Hierarchy = class extends ve.Component {
static instances = [];
static reserved_keys = ["element", "id", "name"];
constructor (arg0_components_obj, arg1_options) {
//Convert from parameters
let components_obj = (arg0_components_obj) ? arg0_components_obj : {};
let options = (arg1_options) ? arg1_options : {};
super(options);
//Initialise options
options.attributes = (options.attributes) ? options.attributes : {};
//Declare local instance variables
this.element = document.createElement("div");
this.element.setAttribute("component", "ve-hierarchy");
Object.iterate(options.attributes, (local_key, local_value) =>
this.element.setAttribute(local_key, local_value.toString()));
this.element.instance = this;
this.options = options;
//Append components_obj to this.element
this.v = components_obj;
ve.Hierarchy.instances.push(this);
}
/**
* Returns the current {@link this.components_obj}.
* - Accessor of {@link ve.Hierarchy}
*
* @alias v
* @memberof ve.Component.ve.Hierarchy
* @type {ve.Component[]}
*/
get v () {
//Return statement
return this.components_obj;
}
/**
* Sets the current {@link this.components_obj} displayed in the hierarchy.
* - Accessor of {@link ve.Hierarchy}
*
* @alias v
* @memberof ve.Component.ve.Hierarchy
* @param arg0_components_obj {ve.Component[]}
*/
set v (arg0_components_obj) {
//Convert from parameters
let components_obj = arg0_components_obj;
//Reset element; re-append all components in components_obj to element
this.element.innerHTML = "";
/**
* Stores the components currently displayed in the {@link ve.Hierarchy}.
* @instance
* @memberof this
* @type {ve.Component[]}
*/
this.components_obj = components_obj;
//0. Append searchbar to this.components_obj
if (!this.options.disable_searchbar) {
this.searchbar_interface = new ve.RawInterface({
searchbar_icon: new ve.HTML("<icon>search</icon>", { style: { padding: `var(--cell-padding)` } }),
searchbar_input: new ve.Datalist({
}, {
attributes: {
placeholder: loc("ve.registry.localisation.Hierarchy_search_for_item")
},
name: " ",
onuserchange: (v) => {
this.updateSearchFilter(v);
}
})
}, {
name: " ",
attributes: {
"ve-searchbar": "true",
"ve-sticky": "true"
},
style: this.options.searchbar_style
});
this.searchbar_interface.bind(this.element);
}
//1. Append all non-hierarchy datatype Vercengen components to controls; iterate over all this.components_obj
Object.iterate(this.components_obj, (local_key, local_value) => {
if (!local_value.is_vercengen_hierarchy_datatype)
this.element.appendChild(local_value.element);
});
//2. Append all hierarchy datatype Vercengen components; iterate over all this.components_obj
let ol_el = document.createElement("ol");
ol_el.id = (this.options.namespace) ?
this.options.namespace : Class.generateRandomID(ve.Hierarchy);
ol_el.setAttribute("class", "list ve-drag-disabled ve-hierarchy");
Object.iterate(this.components_obj, (local_key, local_value) => {
if (local_value.is_vercengen_hierarchy_datatype)
ol_el.appendChild(local_value.element);
});
this.element.appendChild(ol_el);
this.nestable = new Nestable(ol_el, { items: ".group, .item" });
this.nestable.on("stop", (e) => {
if (!this.options.allow_disabled_ordering)
this._handleDisabledOrdering(e);
this.on_stop_data = e;
this.fireToBinding();
});
this.fireFromBinding();
}
/**
* Prevents placing items before disabled elements.
* - Private method of: {@link ve.Hierarchy}
*
* @alias _handleDisabledOrdering
* @memberof ve.Component.ve.Hierarchy
* @param {Object} arg0_e
*/
_handleDisabledOrdering (arg0_e) {
//Convert from parameters
let e = arg0_e;
//Declare local instance variables
let moved_node = e.movedNode;
if (!moved_node) return; //Internal guard clause if there is no moved node
let parent_list = moved_node.parentElement;
if (!parent_list || parent_list.tagName !== "OL") return; //Internal guard clause if not moved and nested
let siblings = Array.from(parent_list.children);
let disabled = siblings.filter((local_el) =>
local_el !== moved_node && (local_el.classList.contains("ve-drag-disabled") || local_el.dataset.nestableDisabled === "disabled"));
if (!disabled.length) return; //Internal guard clause if there are no disabled elements
let first_disabled_index = siblings.indexOf(disabled[0]);
let moved_index = siblings.indexOf(moved_node);
//If moved before disabled, move after last disabled
if (moved_index < first_disabled_index) {
let lastDisabled = disabled[disabled.length - 1];
parent_list.insertBefore(moved_node, lastDisabled.nextSibling);
veToast(`<icon>warning</icon> You cannot move an element before a disabled element.`);
}
}
/**
* Appends the associated hierarchy datatype to the hierarchy.
* - Method of: {@link ve.Hierarchy}
*
* @alias addItem
* @memberof ve.Component.ve.Hierarchy
*
* @param {HTMLElement} arg0_parent_el
* @param {ve.HierarchyDatatype} arg1_hierarchy_datatype
*/
addItem (arg0_parent_el, arg1_hierarchy_datatype) {
//Convert from parameters
let parent_el = (arg0_parent_el) ? arg0_parent_el : this.element.querySelector("ol");
let hierarchy_datatype = arg1_hierarchy_datatype;
//Append child
if (typeof parent_el === "string") parent_el = this.element.querySelector(parent_el);
if (parent_el)
parent_el.appendChild(hierarchy_datatype.element);
}
/**
* Returns a flat array representative of the items in the hierarchy, ordered top to bottom.
* - Method of: {@link ve.Hierarchy}
*
* @alias getHierarchyArray
* @memberof ve.Component.ve.Hierarchy
*
* @param {Object} [arg0_options]
* @param {boolean} [arg0_options.flatten_object=false] - Whether the object should be flattened, returning only serialisable JSON keys.
*
* @returns {Object[]}
*/
getHierarchyArray (arg0_options) {
//Convert from parameters
let options = arg0_options ? arg0_options : {};
//Declare local instance variables
let ol_el = this.element.querySelector("ol");
let reserved_keys = ["id", "element", "instance", "name", "type", "reserved_keys"];
let result = [];
/**
* Recursive helper to traverse the DOM tree top-to-bottom.
*
* @param {HTMLElement} current_ol - The <ol> element to traverse
*/
let traverse = (current_ol) => {
// Get only direct LI children to maintain correct hierarchy order
let children = Array.from(current_ol.children).filter((local_el) =>
local_el.tagName === "LI");
//Iterate over all children
for (let i = 0; i < children.length; i++) {
let li_el = children[i];
let sub_ol = li_el.querySelector("ol");
//Extract metadata (matching getHierarchyObject logic)
let local_name = (li_el && li_el.instance && li_el.instance.name)
? li_el.instance.name : "";
//Determine if this is a group (has children) or a leaf item
let type = sub_ol ? "group" : "item";
result.push({
id: li_el.id,
element: (!options.flatten_object) ? li_el : undefined,
instance: li_el.instance,
name: local_name ? local_name : undefined,
reserved_keys: reserved_keys,
type: type,
});
//If this item has a sub-list, recurse into it immediately (Top > Bottom)
if (sub_ol) traverse(sub_ol);
}
};
if (ol_el) traverse(ol_el);
//Return statement
return result;
}
/**
* Returns an object representative of the items in the hierarchy.
* - Method of: {@link ve.Hierarchy}
*
* @alias getHierarchyObject
* @memberof ve.Component.ve.Hierarchy
*
* @param {Object} [arg0_options]
* @param {boolean} [arg0_options.flatten_object=false] - Whether the object should be flattened, returning only serialisable JSON keys.
*
* @returns {Object}
*/
getHierarchyObject (arg0_options) {
//Convert from parameters
let options = (arg0_options) ? arg0_options : {};
//Declare local instance variables
let ol_el = this.element.querySelector("ol");
let reserved_keys = ["id", "element", "instance", "name", "type", "reserved_keys"];
//Return statement
return HTML.listToObject(ol_el, (local_el) => {
//Declare local instance variables
let local_name = (local_el && local_el.instance && local_el.instance.name) ?
local_el.instance.name : "";
//Return statement
if (local_el.tagName === "OL") {
let parent_el = local_el.parentElement;
try {
local_name = parent_el.instance.name;
} catch {}
return {
id: parent_el.id,
element: (!options.flatten_object) ? parent_el : undefined,
instance: parent_el.instance,
name: (local_name) ? local_name : undefined,
reserved_keys: reserved_keys,
type: "group"
};
} else if (local_el.tagName === "LI") {
return {
id: local_el.id,
element: (!options.flatten_object) ? local_el : undefined,
instance: local_el.instance,
name: (local_name) ? local_name : undefined,
reserved_keys: reserved_keys,
type: "item"
};
}
});
}
/**
* Removes the associated hierarchy datatype from the hierarchy.
* - Method of: {@link ve.Hierarchy}
*
* @alias removeItem
* @memberof ve.Component.ve.Hierarchy
*
* @param {ve.HierarchyDatatype} arg0_hierarchy_datatype
*/
removeItem (arg0_hierarchy_datatype) {
//Convert from parameters
let hierarchy_datatype = arg0_hierarchy_datatype;
//Remove item
hierarchy_datatype.remove();
}
/**
* Updates the current search filter based on the inputted query. Ignores `.options.disabled=true` fields, since they are likely kept at the top as action menus.
* - Method of: {@link ve.Hierarchy}
*
* @alias updateSearchFilter
* @memberof ve.Component.ve.Hierarchy
*
* @param {string} arg0_name
*/
updateSearchFilter (arg0_name) {
//Convert from parameters
let name = (arg0_name) ? arg0_name : "";
//Declare local instance variables
let all_hierarchy_datatype_els = this.element.querySelectorAll(`[component="ve-hierarchy-datatype"]`);
//If name is nothing, restore visibility to all hidden results
if (name.length === 0) {
for (let i = 0; i < all_hierarchy_datatype_els.length; i++)
all_hierarchy_datatype_els[i].style.display = "block";
} else {
let all_filtered_els = [];
for (let i = 0; i < all_hierarchy_datatype_els.length; i++)
if (all_hierarchy_datatype_els[i].instance.name.toLowerCase().trim().indexOf(name.toLowerCase().trim()) !== -1) {
all_hierarchy_datatype_els[i].style.display = "block";
all_filtered_els.push(all_hierarchy_datatype_els[i]);
} else {
all_hierarchy_datatype_els[i].style.display = "none";
}
for (let i = 0; i < all_hierarchy_datatype_els.length; i++)
for (let x = 0; x < all_filtered_els.length; x++)
if (all_hierarchy_datatype_els[i].contains(all_filtered_els[x]) || all_hierarchy_datatype_els[i].instance.options.disabled === true)
all_hierarchy_datatype_els[i].style.display = "block";
}
}
};
//Functional binding
/**
* @returns {ve.Hierarchy}
*/
veHierarchy = function () {
//Return statement
return new ve.Hierarchy(...arguments);
};