/**
* 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`.
*
* Flex interface used for partitions within the same window that can be automatically resized by the user, similar to sub-windows. The `.arg1_options.style.flex` attribute of the descendant Component will determine its flex size.
* - Functional binding: <span color=00ffff>veFlexInterface</span>().
*
* ##### Constructor:
* - `arg0_value`: {@link Object}
* - `<category_key>`: {@link Object}
* - `<category_key>`: {@link Object}
* - `<component_key>`: {@link ve.Component}
* - `.options`: {@link Object}
* - `.flex_disabled=false`: {@link boolean}
* - `.type="horizontal"` - Either 'horizontal'/'vertical'.
* - `.type="horizontal"` - Either 'horizontal'/'vertical'.
* - `arg1_options`: {@link Object}
*
* ##### Instance:
* - `<category_key>`: {@link Object}<{@link ve.Component}|{@link Object}>
* - `<component_key>`: {@link ve.Component}
* - `.reserved_keys`: {@link Array}<{@link string}> - Controls what keys are reserved and cannot be destructured.
* - `.v`: {@link Object}
* - `.value`: {@link Object} - Uses this.value instead of this.components_obj.
*
* ##### Methods:
* - <span color=00ffff>{@link ve.FlexInterface.handleEvents|handleEvents}</span>()
* - <span color=00ffff>{@link ve.FlexInterface.handleResize|handleResize}</span>(arg0_e:{@link Event}, arg1_size_property:{@link string}, arg2_position_property:{@link string})
*
* ##### Static Methods:
* - <span color=00ffff>{@link ve.FlexInterface.generateHTMLRecursively|generateHTMLRecursively}</span>(arg0_root_el:{@link HTMLElement}, arg1_value:{@link Object}, arg2_options:{ flex_disabled:{@link boolean} })
*
* @augments ve.Component
* @memberof ve.Component
* @type {ve.FlexInterface}
*/
ve.FlexInterface = class extends ve.Component { //[WIP] - Finish CSS and JS handlers
constructor (arg0_value, arg1_options) {
//Convert from parameters
let value = arg0_value;
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-flex-interface");
HTML.setAttributesObject(this.element, options.attributes);
this.element.instance = this;
this.options = options;
this.value = value;
//Set .v
this.from_binding_fire_silently = true;
//KEEP AT BOTTOM!
this.name = this.options.name;
this.reserved_keys = Object.keys(this).concat(["reserved_keys", "v"]);
this.v = this.value;
delete this.from_binding_fire_silently;
}
/**
* Returns the current value of the component.
* - Accessor of: {@link ve.FlexInterface}
*
* @alias v
* @memberof ve.Component.ve.FlexInterface
* @type {Object}
*/
get v () {
//Return statement
return this.value;
}
/**
* Sets the present value of the component.
* - Accessor of: {@link ve.FlexInterface}
*
* @alias v
* @memberof ve.Component.ve.FlexInterface
*
* @param {Object} arg0_value
*/
set v (arg0_value) {
//Convert from parameters
let value = (arg0_value) ? arg0_value : {};
//Set this.value and refresh flex interface
this.components_obj = this.value;
this.value = value;
this.element.innerHTML = "";
this.element.appendChild(ve.FlexInterface.generateHTMLRecursively(undefined, this.value, {
type: this.value.type
}));
//Destructure components
Object.iterate(this.value, (local_key, local_value) => {
if (!this.reserved_keys.includes(local_key) && typeof local_value === "object") {
this[local_key] = local_value;
} else {
console.warn(`ve.RawInterface: ${local_key} is a reserved key. It can therefore not be set to:`, local_value);
}
});
this.handleEvents();
this.fireFromBinding();
}
/**
* Handles events for the current component.
* - Method of: {@link ve.FlexInterface}
*
* @alias handleEvents
* @memberof ve.Component.ve.FlexInterface
*/
handleEvents () {
this.element.addEventListener("mousedown", (e) => {
let html = document.querySelector("html");
let target = e.target;
let parent = target.parentNode;
if (target.nodeType !== 1 || target.tagName !== "FLEX-RESIZER") return;
let is_horizontal = parent.classList.contains("horizontal");
let is_vertical = parent.classList.contains("vertical");
if (is_horizontal) {
target.style.cursor = "col-resize";
html.style.cursor = "col-resize";
this.handleResize(e, "width", "pageX");
} else if (is_vertical) {
target.style.cursor = "row-resize";
html.style.cursor = "row-resize";
this.handleResize(e, "height", "pageY");
}
});
}
/**
* Handles a resize event given a MouseDown event type. Internal helper method.
* - Method of: {@link ve.FlexInterface}
*
* @alias handleResize
* @memberof ve.Component.ve.FlexInterface
*
* @param {Event} arg0_e
* @param {string} arg1_dimension_property - "width" or "height"
* @param {string} arg2_position_property - "pageX" or "pageY"
*/
handleResize (arg0_e, arg1_dimension_property, arg2_position_property) {
//Convert from parameters
let md = arg0_e;
let dimension_prop = arg1_dimension_property; // "width" or "height"
let position_property = arg2_position_property;
//Declare local instance variables
let r = md.target;
let parent = r.parentNode;
let next = r.nextElementSibling;
let prev = r.previousElementSibling;
if (!prev || !next) return;
//Capture initial state using getBoundingClientRect for sub-pixel accuracy
let prev_rect = prev.getBoundingClientRect();
let next_rect = next.getBoundingClientRect();
let parent_rect = parent.getBoundingClientRect();
let start_prev_size = prev_rect[dimension_prop];
let start_next_size = next_rect[dimension_prop];
let parent_total_size = parent_rect[dimension_prop];
let start_position = md[position_property];
let total_combined_size = start_prev_size + start_next_size;
md.preventDefault();
let _onmousemove = (e) => {
//Calculate delta from the starting click
let current_position = e[position_property];
let d = current_position - start_position;
let new_prev_size = start_prev_size + d;
let new_next_size = start_next_size - d;
// Constrain boundaries logic
if (new_prev_size < 0) {
new_prev_size = 0;
new_next_size = total_combined_size;
}
if (new_next_size < 0) {
new_next_size = 0;
new_prev_size = total_combined_size;
}
/**
* Calculate percentage relative to the parent container.
* This ensures that when the root element resizes, the partitions scale proportionally.
*/
let prev_percentage = (new_prev_size / parent_total_size) * 100;
let next_percentage = (new_next_size / parent_total_size) * 100;
//Apply styles as percentages
prev.style.flex = `1 0 ${prev_percentage}%`;
next.style.flex = `1 0 ${next_percentage}%`;
};
let _onmouseup = () => {
let html = document.querySelector('html');
html.style.cursor = 'default';
r.style.cursor = (position_property === "pageX") ? "ew-resize" : "ns-resize";
window.removeEventListener("mousemove", _onmousemove);
window.removeEventListener("mouseup", _onmouseup);
if (typeof this.fireToBinding === "function")
this.fireToBinding();
};
window.addEventListener("mousemove", _onmousemove);
window.addEventListener("mouseup", _onmouseup);
}
/**
* Generates HTML recursively given a current value compatible with the component's initialising `arg0_value` and a root element.
* - Static method of: {@link ve.FlexInterface}
*
* @alias #generateHTMLRecursively
* @memberof ve.Component.ve.FlexInterface
*
* @param {HTMLElement|undefined} arg0_root_el
* @param {Object} arg1_value
* @param {Object} [arg2_options]
* @param {boolean} [arg2_options.flex_disabled=false] - Whether flex resizing is disabled for the given element.
*
* @returns {HTMLElement}
*/
static generateHTMLRecursively (arg0_root_el, arg1_value, arg2_options) {
//Convert from parameters
let root_el = (arg0_root_el) ? arg0_root_el : document.createElement("flex");
let value = (arg1_value) ? arg1_value : {};
let options = (arg2_options) ? arg2_options : {};
//Initialise value
if (!value.type) value.type = "horizontal";
//Modify root_el
root_el.setAttribute("class", value.type);
if (getComputedStyle(root_el).getPropertyValue("flex").length === 0)
root_el.style.flex = "1";
//Iterate over all keys in value
Object.iterate(value, (local_key, local_value, local_index) => {
let flex_item_el = document.createElement("flex-item");
flex_item_el.style.flex = "1";
let flex_resizer_el = document.createElement("flex-resizer");
if (typeof local_value === "object" && local_key !== "options" && !(local_value instanceof ve.Component)) {
if (!options.flex_disabled && local_index >= 1)
root_el.appendChild(flex_resizer_el);
let container_el = ve.FlexInterface.generateHTMLRecursively(undefined, local_value, local_value.options);
root_el.appendChild(container_el);
} else if (local_value instanceof ve.Component) {
if (!options.flex_disabled && local_index >= 1)
root_el.appendChild(flex_resizer_el);
local_value.bind(flex_item_el);
flex_item_el.style.flex = Math.returnSafeNumber(parseFloat(local_value?.options?.style?.flex), 1);
root_el.appendChild(flex_item_el);
}
});
//Return statement
return root_el;
}
};
//Functional binding
/**
* @returns {ve.FlexInterface}
*/
veFlexInterface = function () {
//Return statement
return new ve.FlexInterface(...arguments);
};