2024-05-20 22:29:44 +00:00
|
|
|
/** @format */
|
|
|
|
|
2024-05-20 20:34:53 +00:00
|
|
|
// Prevent eslint errors on functions defined in other files.
|
|
|
|
/*global
|
|
|
|
isNullOrUndefinedThrowError,
|
|
|
|
isNullOrUndefinedLogError,
|
|
|
|
isNullOrUndefined,
|
|
|
|
isString,
|
|
|
|
cssRelativeUnitToPx,
|
|
|
|
isNumber,
|
|
|
|
isElementThrowError,
|
|
|
|
isElement,
|
|
|
|
*/
|
|
|
|
/*eslint no-undef: "error"*/
|
|
|
|
|
|
|
|
// Should be between 0 and 15. Any higher and the delay becomes noticable.
|
|
|
|
// Higher values reduce computational load.
|
|
|
|
const MOVE_TIME_DELAY_MS = 15;
|
|
|
|
// Prevents handling element resize events too quickly. Lower values increase
|
|
|
|
// computational load and may lead to lag when resizing.
|
|
|
|
const RESIZE_DEBOUNCE_TIME_MS = 100;
|
|
|
|
// The timeframe in which a second pointerup event must be fired to be treated
|
|
|
|
// as a double click.
|
|
|
|
const DBLCLICK_TIME_MS = 500;
|
|
|
|
// The padding around the draggable resize handle.
|
|
|
|
// NOTE: Must be an even number.
|
|
|
|
const PAD_PX = 16;
|
|
|
|
if (!(PAD_PX > 0 && PAD_PX % 2 === 0)) {
|
|
|
|
throw new Error('PAD_PX must be an even number > 0');
|
|
|
|
}
|
|
|
|
|
|
|
|
const resize_grids = {};
|
|
|
|
|
|
|
|
/* ==== HELPER FUNCTIONS ==== */
|
|
|
|
const _gen_id_string = () => {
|
|
|
|
return Math.random().toString(16).slice(2);
|
|
|
|
};
|
|
|
|
|
|
|
|
const _get_unique_id = () => {
|
|
|
|
let id = _gen_id_string();
|
|
|
|
while (id in Object.keys(resize_grids)) {
|
|
|
|
id = _gen_id_string();
|
|
|
|
}
|
|
|
|
return id;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _parse_array_type = (arr, type_check_fn) => {
|
|
|
|
/** Validates that a variable is an array with members of a specified type.
|
|
|
|
* `type_check_fn` must accept array elements as arguments and return whether
|
|
|
|
* they match the expected type.
|
|
|
|
*/
|
|
|
|
isNullOrUndefinedThrowError(type_check_fn);
|
|
|
|
if (isNullOrUndefined(arr)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
if (!Array.isArray(arr) && type_check_fn(arr)) {
|
|
|
|
return [arr];
|
|
|
|
} else if (Array.isArray(arr) && arr.every((x) => type_check_fn(x))) {
|
|
|
|
return arr;
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid array types:', arr);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const _axis_to_int = (axis) => {
|
|
|
|
/** Converts an axis to a standardized axis integer.
|
|
|
|
* Returns:
|
|
|
|
* "x" or 0: 0
|
|
|
|
* "y" or 1: 1
|
|
|
|
*/
|
|
|
|
if (axis === 0 || axis === 'x') {
|
|
|
|
return 0;
|
|
|
|
} else if (axis === 1 || axis === 'y') {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
throw new Error(`"Axis" expected (x (0), y (1)), got: ${axis}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
class ResizeGridHandle {
|
|
|
|
/** Class defining the clickable "handle" between two grid items. */
|
|
|
|
visible = true;
|
|
|
|
id = null; // unique identifier for this instance.
|
|
|
|
pad_px = PAD_PX;
|
|
|
|
constructor({id, parent, axis, class_list} = {}) {
|
|
|
|
this.id = isNullOrUndefined(id) ? _gen_id_string() : id;
|
|
|
|
this.parent = parent;
|
|
|
|
this.elem = document.createElement('div');
|
|
|
|
this.elem.id = id;
|
|
|
|
this.elem.classList.add('resize-grid--handle');
|
|
|
|
_parse_array_type(class_list, isString).forEach((class_name) => {
|
|
|
|
this.elem.classList.add(class_name);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.axis = _axis_to_int(axis);
|
|
|
|
|
|
|
|
if (this.axis === 0) {
|
|
|
|
this.elem.style.minHeight = this.pad_px + 'px';
|
|
|
|
this.elem.style.maxHeight = this.pad_px + 'px';
|
|
|
|
} else if (this.axis === 1) {
|
|
|
|
this.elem.style.minWidth = this.pad_px + 'px';
|
|
|
|
this.elem.style.maxWidth = this.pad_px + 'px';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.elem.remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
this.elem.classList.remove('hidden');
|
|
|
|
this.visible = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
hide() {
|
|
|
|
this.elem.classList.add('hidden');
|
|
|
|
this.visible = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ResizeGridItem {
|
|
|
|
/** Class defining the cells in a grid. These can be rows or columns. */
|
|
|
|
handle = null;
|
|
|
|
visible = true;
|
|
|
|
pad_px = PAD_PX;
|
2024-05-22 16:54:00 +00:00
|
|
|
callbacks = {
|
|
|
|
/** Allows user to modify the `size_px` value passed to setSize().
|
|
|
|
* Default callback just returns null. (no op).
|
|
|
|
* Args:
|
|
|
|
* item [ResizeGridItem]: This class instance.
|
|
|
|
* size_px [int]: The size (in px) to be overridden.
|
|
|
|
* Returns:
|
|
|
|
* int: The overridden setSize `size_px` paremeter. If null/undefined,
|
|
|
|
* then `size_px` does not get modified.
|
|
|
|
*/
|
|
|
|
set_size_override: (item, size_px) => { return; },
|
|
|
|
};
|
|
|
|
constructor({id, parent, elem, axis, callbacks} = {}) {
|
2024-05-20 20:34:53 +00:00
|
|
|
this.id = isNullOrUndefined(id) ? _gen_id_string() : id;
|
|
|
|
this.parent = parent; // the parent class instance
|
|
|
|
this.elem = elem;
|
|
|
|
this.axis = _axis_to_int(axis);
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
// Parse user specified callback overrides.
|
|
|
|
if (isObject(callbacks)) {
|
|
|
|
if (isFunction(callbacks.set_size_override)) {
|
|
|
|
this.callbacks.set_size_override = callbacks.set_size_override;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-20 20:34:53 +00:00
|
|
|
this.is_flex_grow = Boolean(parseInt(this.elem.style.flexGrow));
|
|
|
|
this.default_is_flex_grow = this.is_flex_grow;
|
|
|
|
let flex_basis = parseInt(cssRelativeUnitToPx(this.elem.style.flexBasis));
|
|
|
|
// If user specifies data-min-size, then the flexBasis is just used to set
|
|
|
|
// the initial size.
|
|
|
|
if ('minSize' in this.elem.dataset) {
|
|
|
|
this.min_size = parseInt(cssRelativeUnitToPx(this.elem.dataset.minSize));
|
|
|
|
if (isNumber(flex_basis)) {
|
|
|
|
this.base_size = flex_basis;
|
|
|
|
} else {
|
|
|
|
this.base_size = this.min_size;
|
|
|
|
}
|
|
|
|
} else if (isNumber(flex_basis)) {
|
|
|
|
this.min_size = flex_basis;
|
|
|
|
this.base_size = flex_basis;
|
|
|
|
} else {
|
|
|
|
this.min_size = 0;
|
|
|
|
this.base_size = 0;
|
|
|
|
}
|
|
|
|
const dims = this.elem.getBoundingClientRect();
|
|
|
|
this.base_size =
|
|
|
|
this.axis === 0 ? parseInt(dims.height) : parseInt(dims.width);
|
|
|
|
this.elem.dataset.id = this.id;
|
|
|
|
this.original_css_text = this.elem.style.cssText;
|
|
|
|
}
|
|
|
|
|
|
|
|
render({force_flex_grow, reset} = {}) {
|
|
|
|
/** Sets the element's flex styles. */
|
|
|
|
force_flex_grow = force_flex_grow === true;
|
|
|
|
reset = reset === true;
|
|
|
|
|
|
|
|
this.elem.style.flexShrink = 0;
|
|
|
|
if (reset) {
|
|
|
|
this.elem.style.flexGrow = Number(this.is_flex_grow);
|
|
|
|
this.elem.style.flexBasis = parseInt(this.base_size) + 'px';
|
|
|
|
} else if (force_flex_grow) {
|
|
|
|
this.elem.style.flexGrow = 1;
|
|
|
|
} else {
|
|
|
|
this.elem.style.flexGrow = Number(this.is_flex_grow);
|
|
|
|
if (!this.is_flex_grow) {
|
|
|
|
this.elem.style.flexBasis =
|
|
|
|
parseInt(Math.max(this.min_size, this.getSize())) + 'px';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
if (!isNullOrUndefined(this.handle)) {
|
|
|
|
this.handle.destroy();
|
|
|
|
this.handle = null;
|
|
|
|
}
|
|
|
|
// Revert changes to the container element.
|
|
|
|
this.elem.style.cssText = this.original_css_text;
|
|
|
|
if (!this.elem.style.cssText) {
|
|
|
|
this.elem.removeAttribute('style');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
shrink(px, {limit_to_base} = {}) {
|
|
|
|
/** Shrink size along axis by specified pixels. Returns remainder. */
|
|
|
|
limit_to_base = limit_to_base === true;
|
|
|
|
const target_size = limit_to_base ? this.base_size : this.min_size;
|
|
|
|
const curr_size = this.getSize();
|
|
|
|
|
|
|
|
if (px === -1) {
|
|
|
|
// shrink to min_size
|
|
|
|
this.setSize(target_size);
|
|
|
|
return 0;
|
|
|
|
} else if (curr_size - target_size < px) {
|
|
|
|
this.setSize(target_size);
|
|
|
|
return px - (curr_size - target_size);
|
|
|
|
} else {
|
|
|
|
this.setSize(curr_size - px);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
grow(px, {only_if_flex} = {}) {
|
|
|
|
/** Grows along axis and returns the amount grown in pixels. */
|
|
|
|
only_if_flex = only_if_flex === true;
|
|
|
|
if (only_if_flex && !this.is_flex_grow) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
let new_size;
|
|
|
|
const curr_size = this.getSize();
|
|
|
|
if (px === -1) {
|
|
|
|
// grow to fill container (only works if visible)
|
|
|
|
// set flexGrow to 1 to expand to max width so we can calc new width.
|
|
|
|
this.elem.style.flexGrow = 1;
|
|
|
|
new_size = this.getSize();
|
|
|
|
this.elem.style.flexGrow = Number(this.is_flex_grow);
|
|
|
|
} else {
|
|
|
|
new_size = curr_size + px;
|
|
|
|
}
|
|
|
|
this.setSize(new_size);
|
|
|
|
return new_size - curr_size;
|
|
|
|
}
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
setSize(size_px) {
|
|
|
|
const new_size_px = this.callbacks.set_size_override(this, size_px);
|
|
|
|
if (isNumber(new_size_px)) {
|
|
|
|
size_px = new_size_px;
|
|
|
|
}
|
|
|
|
this.elem.style.flexBasis = parseInt(size_px) + 'px';
|
|
|
|
//this.render();
|
|
|
|
return size_px;
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getSize() {
|
|
|
|
// If this item is visible, then we can use the computed dimensions.
|
|
|
|
// Otherwise we are forced to use the flexBasis inline style.
|
|
|
|
if (this.visible) {
|
|
|
|
const dims = this.elem.getBoundingClientRect();
|
|
|
|
return this.axis === 0 ? parseInt(dims.height) : parseInt(dims.width);
|
|
|
|
} else {
|
|
|
|
return parseInt(this.elem.style.flexBasis);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
genHandle(class_list) {
|
|
|
|
/** Generates a ResizeGridHandle after this item based on the axis. */
|
|
|
|
this.handle = new ResizeGridHandle({
|
|
|
|
id: `${this.id}_handle`,
|
|
|
|
parent: this.parent,
|
|
|
|
axis: this.axis,
|
|
|
|
class_list: class_list,
|
|
|
|
});
|
|
|
|
if (isElement(this.elem.nextElementSibling)) {
|
|
|
|
this.elem.parentElement.insertBefore(
|
|
|
|
this.handle.elem,
|
|
|
|
this.elem.nextSibling
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.elem.parentElement.appendChild(this.handle.elem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
/** Shows this item and its ResizeGridHandle. */
|
|
|
|
this.elem.classList.remove('hidden');
|
|
|
|
// Only show the handle if there is another ResizeGridItem after this one.
|
|
|
|
if (!isNullOrUndefined(this.handle.elem.nextSibling)) {
|
|
|
|
this.handle.show();
|
|
|
|
}
|
|
|
|
this.visible = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
hide() {
|
|
|
|
/** Hides this item and its ResizeGridHandle. */
|
|
|
|
this.elem.classList.add('hidden');
|
|
|
|
this.handle.hide();
|
|
|
|
this.visible = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ResizeGridContainer {
|
|
|
|
/** Class defining a collection of ResizeGridItem and ResizeGridHandle instances. */
|
2024-05-22 16:54:00 +00:00
|
|
|
constructor({id, parent, elem, callbacks} = {}) {
|
2024-05-20 20:34:53 +00:00
|
|
|
this.id = isNullOrUndefined(id) ? _gen_id_string() : id;
|
|
|
|
this.parent = parent;
|
|
|
|
this.elem = elem;
|
2024-05-22 16:54:00 +00:00
|
|
|
this.callbacks = callbacks;
|
2024-05-20 20:34:53 +00:00
|
|
|
this.original_css_text = this.elem.style.cssText;
|
|
|
|
|
|
|
|
this.grid = [];
|
|
|
|
this.rows = [];
|
|
|
|
this.id_map = {};
|
|
|
|
this.added_outer_row = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.rows.forEach((row) => {
|
|
|
|
row.destroy();
|
|
|
|
});
|
|
|
|
this.rows = null;
|
|
|
|
if (this.added_outer_row) {
|
|
|
|
this.elem.innerHTML = this.elem.querySelector(
|
|
|
|
':scope > .resize-grid--row'
|
|
|
|
).innerHTML;
|
|
|
|
}
|
|
|
|
super.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
addRow(id, elem, row_idx) {
|
|
|
|
/** Generates a ResizeGridItem and ResizeGridHandle for a row element. */
|
|
|
|
const row = new ResizeGridItem({
|
|
|
|
id: id,
|
|
|
|
parent: this,
|
|
|
|
elem: elem,
|
2024-05-22 16:54:00 +00:00
|
|
|
callbacks: this.callbacks,
|
2024-05-20 20:34:53 +00:00
|
|
|
axis: 0,
|
|
|
|
});
|
|
|
|
row.genHandle('resize-grid--row-handle');
|
|
|
|
row.elem.dataset.row = row_idx;
|
|
|
|
this.rows.push(row);
|
|
|
|
this.id_map[id] = row;
|
|
|
|
return row;
|
|
|
|
}
|
|
|
|
|
|
|
|
addCol(id, elem, row_idx, col_idx) {
|
|
|
|
/** Generates a ResizeGridItem and ResizeGridHandle for a column element. */
|
|
|
|
const col = new ResizeGridItem({
|
|
|
|
id: id,
|
|
|
|
parent: this,
|
|
|
|
elem: elem,
|
2024-05-22 16:54:00 +00:00
|
|
|
callbacks: this.callbacks,
|
2024-05-20 20:34:53 +00:00
|
|
|
axis: 1,
|
|
|
|
});
|
|
|
|
col.genHandle('resize-grid--col-handle');
|
|
|
|
col.elem.dataset.row = row_idx;
|
|
|
|
col.elem.dataset.col = col_idx;
|
|
|
|
this.grid[row_idx].push(col);
|
|
|
|
this.id_map[id] = col;
|
|
|
|
return col;
|
|
|
|
}
|
|
|
|
|
|
|
|
build() {
|
|
|
|
/** Generates rows/cols based on this instance element's content. */
|
2024-05-22 16:54:00 +00:00
|
|
|
let row_elems = Array.from(this.elem.querySelectorAll(":scope > .resize-grid--row"));
|
|
|
|
|
2024-05-20 20:34:53 +00:00
|
|
|
// If we do not have any rows, then we generate a single row to contain the cols.
|
|
|
|
if (!row_elems.length) {
|
|
|
|
const elem = document.createElement('div');
|
|
|
|
elem.classList.add('resize-grid--row');
|
|
|
|
elem.append(...this.elem.children);
|
|
|
|
this.elem.replaceChildren(elem);
|
|
|
|
row_elems = [elem];
|
|
|
|
// track this addition so we can remove it later.
|
|
|
|
this.added_outer_row = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that if we only have one row, that it fills the container.
|
|
|
|
if (row_elems.length === 1 && !row_elems[0].style.flexBasis) {
|
|
|
|
row_elems[0].style.flexGrow = 1;
|
|
|
|
row_elems[0].style.flexBasis =
|
|
|
|
parseInt(this.elem.getBoundingClientRect().height) + 'px';
|
|
|
|
}
|
|
|
|
|
|
|
|
let id = 0;
|
|
|
|
this.grid = [...Array(row_elems.length)].map((_) => []);
|
|
|
|
row_elems.forEach((row_elem, i) => {
|
|
|
|
this.addRow(id++, row_elem, i);
|
|
|
|
const col_elems = row_elem.querySelectorAll('.resize-grid--col');
|
2024-05-22 16:54:00 +00:00
|
|
|
if (col_elems.length === 1) {
|
|
|
|
col_elems[0].style.flexGrow = 1;
|
|
|
|
}
|
2024-05-20 20:34:53 +00:00
|
|
|
col_elems.forEach((col_elem, j) => {
|
|
|
|
this.addCol(id++, col_elem, i, j);
|
|
|
|
});
|
|
|
|
this.grid[i][this.grid[i].length - 1].handle.hide();
|
|
|
|
});
|
|
|
|
this.rows[this.rows.length - 1].handle.hide();
|
|
|
|
|
|
|
|
// Now that all handles are added, we need to render the flex styles for each item.
|
|
|
|
for (let i = 0; i < this.rows.length; i++) {
|
|
|
|
this.rows[i].render({reset: true});
|
|
|
|
for (let j = 0; j < this.grid[i].length; j++) {
|
|
|
|
this.grid[i][j].render({reset: true});
|
|
|
|
}
|
2024-05-20 22:29:44 +00:00
|
|
|
const vis_cols = this.grid[i].filter((x) => x.visible);
|
|
|
|
if (vis_cols.length === 1) {
|
|
|
|
vis_cols[0].render({force_flex_grow: true});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const vis_rows = this.rows.filter((x) => x.visible);
|
|
|
|
if (vis_rows.length === 1) {
|
|
|
|
vis_rows[0].render({force_flex_grow: true});
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getByElem(elem) {
|
|
|
|
return this.id_map[elem.dataset.id];
|
|
|
|
}
|
|
|
|
|
|
|
|
getByIdx({row_idx, col_idx} = {}) {
|
|
|
|
/** Returns the ResizeGridItem at the row/col index. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
if (
|
|
|
|
(!isNumber(row_idx) && !isNumber(col_idx)) ||
|
|
|
|
(!isNumber(row_idx) && isNumber(col_idx))
|
|
|
|
) {
|
|
|
|
console.error('Invalid row/col idx:', row_idx, col_idx);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (isNumber(row_idx) && !isNumber(col_idx)) {
|
|
|
|
if (row_idx >= this.rows.length) {
|
|
|
|
console.error(
|
|
|
|
`row_idx out of range: (${row_idx} > ${this.rows.length})`
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this.rows[row_idx];
|
|
|
|
}
|
|
|
|
if (isNumber(row_idx) && isNumber(col_idx)) {
|
|
|
|
if (row_idx >= this.grid.length) {
|
|
|
|
console.error(
|
|
|
|
`row_idx out of range: (${row_idx} > ${this.grid.length})`
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (col_idx >= this.grid[row_idx].length) {
|
|
|
|
console.error(
|
|
|
|
`col_idx out of range: (${col_idx} > ${this.grid[row_idx].length})`
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this.grid[row_idx][col_idx];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateVisibleHandles() {
|
|
|
|
/** Sets the visibility of each ResizeGridHandle based on surrounding items. */
|
|
|
|
const last_vis_rows_idx = this.rows.findLastIndex((x) => x.visible);
|
|
|
|
for (let i = 0; i < this.rows.length; i++) {
|
|
|
|
const last_vis_grid_idx = this.grid[i].findLastIndex((x) => x.visible);
|
|
|
|
for (let j = 0; j < this.grid[i].length; j++) {
|
|
|
|
const item = this.getByIdx({row_idx: i, col_idx: j});
|
|
|
|
if (isNullOrUndefined(item)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't show handle if item is last column in row.
|
|
|
|
if (this.grid[i][j].visible && j !== last_vis_grid_idx) {
|
|
|
|
this.grid[i][j].handle.show();
|
|
|
|
} else {
|
|
|
|
this.grid[i][j].handle.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const item = this.getByIdx({row_idx: i});
|
|
|
|
if (isNullOrUndefined(item)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't show handle if item is last row in grid.
|
|
|
|
if (this.rows[i].visible && i !== last_vis_rows_idx) {
|
|
|
|
this.rows[i].handle.show();
|
|
|
|
} else {
|
|
|
|
this.rows[i].handle.hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
makeRoomForItem(item, siblings, item_idx, {use_base_size} = {}) {
|
|
|
|
/** Shrinks items along axis until the supplied item can fit. */
|
|
|
|
use_base_size = use_base_size === true;
|
|
|
|
let tot = use_base_size ? item.base_size : item.getSize();
|
|
|
|
// Get the item after this item's handle.
|
|
|
|
let sibling = siblings.slice(item_idx + 1).find((x) => x.visible);
|
|
|
|
if (isNullOrUndefined(sibling)) {
|
|
|
|
// No items after this item. Instead get the item just before this item.
|
|
|
|
sibling = siblings.slice(0, item_idx).findLast((x) => x.visible);
|
|
|
|
isNullOrUndefinedThrowError(sibling); // Indicates programmer error.
|
|
|
|
// Last item so we want to hide its handle.
|
|
|
|
item.handle.hide();
|
|
|
|
// Add previous item handle's size
|
|
|
|
tot += sibling.handle.pad_px;
|
|
|
|
} else {
|
|
|
|
// Need to add handle between this item and next item.
|
|
|
|
item.handle.show();
|
|
|
|
tot += item.handle.pad_px;
|
|
|
|
}
|
|
|
|
|
|
|
|
const sibling_idx = siblings.indexOf(sibling);
|
|
|
|
|
|
|
|
let rem = tot;
|
|
|
|
rem = sibling.shrink(rem, {limit_to_base: use_base_size});
|
|
|
|
if (rem <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Shrink from flexGrow items next starting from the end.
|
|
|
|
if (rem > 0) {
|
|
|
|
const others = siblings.filter(
|
|
|
|
(x) =>
|
|
|
|
x.visible && x.is_flex_grow && siblings.indexOf(x) !== sibling_idx
|
|
|
|
);
|
|
|
|
for (const other of others.slice().reverse()) {
|
|
|
|
rem = other.shrink(rem, {limit_to_base: use_base_size});
|
|
|
|
if (rem <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Now shrink from non-flexGrow items starting from the end.
|
|
|
|
if (rem > 0) {
|
|
|
|
const others = siblings.filter(
|
|
|
|
(x) =>
|
|
|
|
x.visible && !x.is_flex_grow && siblings.indexOf(x) !== sibling_idx
|
|
|
|
);
|
|
|
|
for (const other of others.slice().reverse()) {
|
|
|
|
rem = other.shrink(rem, {limit_to_base: use_base_size});
|
|
|
|
if (rem <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shrink the item itself if we still don't have room.
|
|
|
|
if (rem > 0) {
|
|
|
|
rem = item.shrink(rem, {limit_to_base: use_base_size});
|
|
|
|
if (rem <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If still not enough room, try again but use the base sizes.
|
|
|
|
if (rem > 0 && !use_base_size) {
|
|
|
|
this.makeRoomForItem(item, siblings, item_idx, {use_base_size: true});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// If we still couldn't make room, this indicates programmer error.
|
|
|
|
throw new Error(`No space for row. tot: ${tot}, rem: ${rem}`);
|
|
|
|
}
|
|
|
|
|
2024-05-20 22:29:44 +00:00
|
|
|
growToFill(item, siblings, item_idx, tot_px) {
|
2024-05-20 20:34:53 +00:00
|
|
|
/** Expands item along axis until the axis has no remaining space. */
|
|
|
|
// Expand the item that was attached via the hidden item's handle first.
|
|
|
|
let sibling = siblings.slice(item_idx + 1).find((x) => x.visible);
|
|
|
|
if (isNullOrUndefined(sibling)) {
|
|
|
|
// Otherwise, expand the previous attached item.
|
|
|
|
sibling = siblings.slice(0, item_idx).findLast((x) => x.visible);
|
|
|
|
isNullOrUndefinedThrowError(sibling); // Indicates programmer error.
|
|
|
|
} else {
|
|
|
|
tot_px += item.pad_px;
|
|
|
|
}
|
|
|
|
// Hide sibling's handle if sibling is last visible item.
|
|
|
|
if (
|
|
|
|
siblings
|
|
|
|
.slice(siblings.findIndex((x) => x === sibling) + 1)
|
|
|
|
.every((x) => !x.visible)
|
|
|
|
) {
|
|
|
|
if (sibling.handle.visible) {
|
|
|
|
sibling.handle.hide();
|
|
|
|
tot_px += sibling.pad_px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are growing sibling to fill, then just set flexGrow=1.
|
|
|
|
if (
|
|
|
|
siblings.length <= 2 ||
|
|
|
|
siblings.every((x) => !x.visible && x !== sibling && x !== item)
|
|
|
|
) {
|
|
|
|
sibling.render({force_flex_grow: true});
|
|
|
|
} else {
|
2024-05-20 22:29:44 +00:00
|
|
|
sibling.grow(-1);
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
showRow(row_idx, {show_empty_row} = {}) {
|
|
|
|
/** Makes space for the row then shows it. */
|
|
|
|
show_empty_row = show_empty_row === true;
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
const item = this.getByIdx({row_idx: row_idx});
|
|
|
|
isNullOrUndefinedThrowError(item);
|
|
|
|
|
|
|
|
if (item.visible) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.axis !== 0) {
|
|
|
|
console.error('Expected row, got col:', item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no columns are visible, then we can't show the row.
|
|
|
|
if (this.grid[row_idx].every((x) => !x.visible) && !show_empty_row) {
|
|
|
|
console.error('No visible columns in row. Cannot show.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// All rows are hidden. We just show this row and make it fill the container.
|
|
|
|
if (this.rows.every((x) => !x.visible)) {
|
|
|
|
item.show();
|
|
|
|
item.handle.hide();
|
|
|
|
item.render({force_flex_grow: true});
|
|
|
|
} else {
|
|
|
|
this.makeRoomForItem(item, this.rows, row_idx);
|
|
|
|
item.show();
|
|
|
|
if (this.rows.slice(row_idx + 1).every((x) => !x.visible)) {
|
|
|
|
// If this is the last visible row, hide the handle.
|
|
|
|
item.handle.hide();
|
|
|
|
}
|
|
|
|
const prev_item = this.rows.slice(0, row_idx).find((x) => x.visible);
|
|
|
|
if (!isNullOrUndefined(prev_item)) {
|
|
|
|
prev_item.handle.show();
|
|
|
|
}
|
|
|
|
item.render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
showCol(row_idx, col_idx) {
|
|
|
|
/** Makes space for the column then shows it. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx});
|
|
|
|
isNullOrUndefinedThrowError(item);
|
|
|
|
|
|
|
|
if (item.visible) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.axis !== 1) {
|
|
|
|
console.error('Expected col, got row:', item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the row isn't visible, we need to show it before we can show columns.
|
|
|
|
if (!this.rows[row_idx].visible) {
|
|
|
|
this.showRow(row_idx, {show_empty_row: true});
|
|
|
|
}
|
|
|
|
|
|
|
|
// All cols are hidden. We just show this col and make it fill the row.
|
|
|
|
if (this.grid[row_idx].every((x) => !x.visible)) {
|
|
|
|
item.show();
|
|
|
|
item.handle.hide();
|
|
|
|
item.render({force_flex_grow: true});
|
|
|
|
} else {
|
|
|
|
this.makeRoomForItem(item, this.grid[row_idx], col_idx);
|
|
|
|
item.show();
|
|
|
|
if (this.grid[row_idx].slice(col_idx + 1).every((x) => !x.visible)) {
|
|
|
|
// If this is the last visible col, hide the handle.
|
|
|
|
item.handle.hide();
|
|
|
|
}
|
|
|
|
const prev_item = this.grid[row_idx]
|
|
|
|
.slice(0, col_idx)
|
|
|
|
.find((x) => x.visible);
|
|
|
|
if (!isNullOrUndefined(prev_item)) {
|
|
|
|
prev_item.handle.show();
|
|
|
|
}
|
|
|
|
item.render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hideRow(row_idx) {
|
|
|
|
/** Hides a row and resizes other rows to fill the gap. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
const item = this.getByIdx({row_idx: row_idx});
|
|
|
|
isNullOrUndefinedThrowError(item);
|
|
|
|
|
|
|
|
if (!item.visible) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.axis !== 0) {
|
|
|
|
console.error('Expected row, got col:', item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let tot_px = item.elem.getBoundingClientRect().height;
|
|
|
|
item.hide();
|
|
|
|
// If no other rows are visible, we don't need to do anything else.
|
|
|
|
if (this.rows.every((x) => !x.visible)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-20 22:29:44 +00:00
|
|
|
this.growToFill(item, this.rows, row_idx, tot_px);
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hideCol(row_idx, col_idx) {
|
|
|
|
/** Hides a column and resizes other columns to fill the gap. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx});
|
|
|
|
isNullOrUndefinedThrowError(item);
|
|
|
|
|
|
|
|
if (!item.visible) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.axis !== 1) {
|
|
|
|
console.error('Expected col, got row:', item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-20 22:29:44 +00:00
|
|
|
let tot_px = item.getSize();
|
2024-05-20 20:34:53 +00:00
|
|
|
item.hide();
|
|
|
|
// If no other cols are visible, hide the containing row.
|
|
|
|
if (this.grid[row_idx].every((x) => !x.visible)) {
|
|
|
|
this.hideRow(row_idx);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-20 22:29:44 +00:00
|
|
|
this.growToFill(item, this.grid[row_idx], col_idx, tot_px);
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
show({row_idx, col_idx} = {}) {
|
|
|
|
/** Shows a row or column based on the provided row/col indices. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
if (isNumber(row_idx) && !isNumber(col_idx)) {
|
|
|
|
this.showRow(row_idx);
|
|
|
|
} else if (isNumber(row_idx) && isNumber(col_idx)) {
|
|
|
|
this.showCol(row_idx, col_idx);
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid parameters for row/col idx:', row_idx, col_idx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hide({row_idx, col_idx} = {}) {
|
|
|
|
/** Hides a row or column based on the provided row/col indices. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
if (isNumber(row_idx) && !isNumber(col_idx)) {
|
|
|
|
this.hideRow(row_idx);
|
|
|
|
} else if (isNumber(row_idx) && isNumber(col_idx)) {
|
|
|
|
this.hideCol(row_idx, col_idx);
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid parameters for row/col idx:', row_idx, col_idx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggle({row_idx, col_idx, override} = {}) {
|
|
|
|
/** Toggles a row or column's visibility based on the provided row/col indices. */
|
|
|
|
row_idx = parseInt(row_idx);
|
|
|
|
col_idx = parseInt(col_idx);
|
|
|
|
const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx});
|
|
|
|
isNullOrUndefinedThrowError(item);
|
|
|
|
|
|
|
|
let new_state = !item.visible;
|
|
|
|
if (override === true || override === false) {
|
|
|
|
new_state = override;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.axis === 0) {
|
|
|
|
new_state ? this.showRow(row_idx) : this.hideRow(row_idx);
|
|
|
|
} else {
|
|
|
|
new_state ?
|
|
|
|
this.showCol(row_idx, col_idx) :
|
|
|
|
this.hideCol(row_idx, col_idx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ResizeGrid {
|
|
|
|
/** Class representing a resizable grid. */
|
|
|
|
event_abort_controller = null;
|
|
|
|
added_outer_row = false;
|
|
|
|
container = null;
|
|
|
|
setup_has_run = false;
|
|
|
|
prev_dims = null;
|
|
|
|
resize_observer = null;
|
|
|
|
resize_observer_timer;
|
2024-05-22 16:54:00 +00:00
|
|
|
constructor(id, elem, {callbacks}={}) {
|
2024-05-20 20:34:53 +00:00
|
|
|
this.id = id;
|
|
|
|
this.elem = elem;
|
|
|
|
this.elem.dataset.gridId = this.id;
|
2024-05-22 16:54:00 +00:00
|
|
|
this.callbacks = callbacks;
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.destroyEvents();
|
|
|
|
if (!isNullOrUndefined(this.container)) {
|
|
|
|
this.container.destroy();
|
|
|
|
this.container = null;
|
|
|
|
}
|
|
|
|
this.setup_has_run = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
/** Fully prepares this instance for use. */
|
|
|
|
if (!this.elem.querySelector('.resize-grid--row,.resize-grid--col')) {
|
|
|
|
throw new Error('Container has no rows or cols.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isNullOrUndefined(this.container)) {
|
|
|
|
this.container.destroy();
|
|
|
|
this.container = null;
|
|
|
|
}
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
this.container = new ResizeGridContainer({
|
|
|
|
parent: this,
|
|
|
|
elem: this.elem,
|
|
|
|
callbacks: this.callbacks,
|
|
|
|
});
|
2024-05-20 20:34:53 +00:00
|
|
|
this.container.build();
|
|
|
|
this.prev_dims = this.elem.getBoundingClientRect();
|
|
|
|
this.setupEvents();
|
|
|
|
this.setup_has_run = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
setupEvents() {
|
|
|
|
/** Sets up all event delegators and observers for this instance. */
|
|
|
|
this.event_abort_controller = new AbortController();
|
|
|
|
let prev;
|
|
|
|
let handle;
|
|
|
|
let next;
|
|
|
|
let touch_count = 0;
|
|
|
|
let dblclick_timer;
|
|
|
|
let last_move_time;
|
|
|
|
|
|
|
|
window.addEventListener(
|
|
|
|
'pointerdown',
|
|
|
|
(event) => {
|
|
|
|
if (event.target.hasPointerCapture(event.pointerId)) {
|
|
|
|
event.target.releasePointerCapture(event.pointerId);
|
|
|
|
}
|
|
|
|
if (event.pointerType === 'mouse' && event.button !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (event.pointerType === 'touch') {
|
|
|
|
touch_count++;
|
|
|
|
if (touch_count !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const elem = event.target.closest('.resize-grid--handle');
|
|
|
|
if (!elem) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Clicked handles will always be between two elements. If the user
|
|
|
|
// somehow clicks an invisible handle then we have bigger problems.
|
|
|
|
prev = this.container.getByElem(elem.previousElementSibling);
|
|
|
|
if (!prev.visible) {
|
|
|
|
const row_idx = prev.elem.dataset.row;
|
|
|
|
const col_idx = prev.elem.dataset.col;
|
|
|
|
const siblings =
|
|
|
|
prev.axis === 0 ?
|
|
|
|
this.container.rows :
|
|
|
|
this.container.grid[row_idx];
|
|
|
|
const idx = prev.axis === 0 ? row_idx : col_idx;
|
|
|
|
prev = siblings.slice(0, idx).findLast((x) => x.visible);
|
|
|
|
}
|
|
|
|
handle = prev.handle;
|
|
|
|
next = this.container.getByElem(elem.nextElementSibling);
|
|
|
|
if (!next.visible) {
|
|
|
|
const row_idx = next.elem.dataset.row;
|
|
|
|
const col_idx = next.elem.dataset.col;
|
|
|
|
const siblings =
|
|
|
|
next.axis === 0 ?
|
|
|
|
this.container.rows :
|
|
|
|
this.container.grid[row_idx];
|
|
|
|
const idx = next.axis === 0 ? row_idx : col_idx;
|
|
|
|
next = siblings.slice(idx).find((x) => x.visible);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
isNullOrUndefinedLogError(prev) ||
|
|
|
|
isNullOrUndefinedLogError(handle) ||
|
|
|
|
isNullOrUndefinedLogError(next)
|
|
|
|
) {
|
|
|
|
prev = null;
|
|
|
|
handle = null;
|
|
|
|
next = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
handle.elem.setPointerCapture(event.pointerId);
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
// Temporarily set styles for elements. These are cleared on pointerup.
|
|
|
|
// See `onMove()` comments for more info.
|
|
|
|
prev.elem.style.flexGrow = 0;
|
|
|
|
next.elem.style.flexGrow = 1;
|
|
|
|
next.elem.style.flexShrink = 1;
|
|
|
|
|
2024-05-20 20:34:53 +00:00
|
|
|
document.body.classList.add('resizing');
|
|
|
|
if (handle.axis === 0) {
|
|
|
|
document.body.classList.add('resizing-col');
|
|
|
|
} else {
|
|
|
|
document.body.classList.add('resizing-row');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{signal: this.event_abort_controller.signal}
|
|
|
|
);
|
|
|
|
|
|
|
|
window.addEventListener(
|
|
|
|
'pointermove',
|
|
|
|
(event) => {
|
|
|
|
if (
|
|
|
|
isNullOrUndefined(prev) ||
|
|
|
|
isNullOrUndefined(handle) ||
|
|
|
|
isNullOrUndefined(next)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
const now = new Date().getTime();
|
|
|
|
if (!last_move_time || now - last_move_time > MOVE_TIME_DELAY_MS) {
|
|
|
|
this.onMove(event, prev, handle, next);
|
|
|
|
last_move_time = now;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{signal: this.event_abort_controller.signal}
|
|
|
|
);
|
|
|
|
|
|
|
|
window.addEventListener(
|
|
|
|
'pointerup',
|
|
|
|
(event) => {
|
|
|
|
if (
|
|
|
|
isNullOrUndefined(prev) ||
|
|
|
|
isNullOrUndefined(handle) ||
|
|
|
|
isNullOrUndefined(next)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.target.hasPointerCapture(event.pointerId)) {
|
|
|
|
event.target.releasePointerCapture(event.pointerId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.pointerType === 'mouse' && event.button !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (event.pointerType === 'touch') {
|
|
|
|
touch_count--;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
handle.elem.releasePointerCapture(event.pointerId);
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
// Set the new flexBasis value for the `next` element then revert
|
|
|
|
// the style changes set in the `pointerup` event.
|
|
|
|
next.elem.style.flexBasis = next.setSize(next.getSize());
|
|
|
|
prev.elem.style.flexGrow = Number(prev.is_flex_grow);
|
|
|
|
next.elem.style.flexGrow = Number(next.is_flex_grow);
|
|
|
|
next.elem.style.flexShrink = 0;
|
|
|
|
|
|
|
|
|
2024-05-20 20:34:53 +00:00
|
|
|
document.body.classList.remove('resizing');
|
|
|
|
document.body.classList.remove('resizing-col');
|
|
|
|
document.body.classList.remove('resizing-row');
|
|
|
|
|
|
|
|
if (!dblclick_timer) {
|
|
|
|
handle.elem.dataset.awaitDblClick = '';
|
|
|
|
dblclick_timer = setTimeout(
|
|
|
|
(elem) => {
|
|
|
|
dblclick_timer = null;
|
|
|
|
delete elem.dataset.awaitDblClick;
|
|
|
|
},
|
|
|
|
DBLCLICK_TIME_MS,
|
|
|
|
handle.elem
|
|
|
|
);
|
|
|
|
} else if ('awaitDblClick' in handle.elem.dataset) {
|
|
|
|
clearTimeout(dblclick_timer);
|
|
|
|
dblclick_timer = null;
|
|
|
|
delete handle.elem.dataset.awaitDblClick;
|
|
|
|
handle.elem.dispatchEvent(
|
|
|
|
new CustomEvent('resize_handle_dblclick', {
|
|
|
|
bubbles: true,
|
|
|
|
detail: this,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
prev = null;
|
|
|
|
handle = null;
|
|
|
|
next = null;
|
|
|
|
},
|
|
|
|
{signal: this.event_abort_controller.signal}
|
|
|
|
);
|
|
|
|
|
|
|
|
window.addEventListener(
|
|
|
|
'pointerout',
|
|
|
|
(event) => {
|
|
|
|
if (event.pointerType === 'touch') {
|
|
|
|
touch_count--;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{signal: this.event_abort_controller.signal}
|
|
|
|
);
|
|
|
|
|
|
|
|
this.resize_observer = new ResizeObserver((entries) => {
|
|
|
|
for (const entry of entries) {
|
|
|
|
if (entry.target.id === this.elem.id) {
|
|
|
|
clearTimeout(this.resize_observer_timer);
|
|
|
|
this.resize_observer_timer = setTimeout(() => {
|
|
|
|
this.onResize();
|
|
|
|
}, RESIZE_DEBOUNCE_TIME_MS);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.resize_observer.observe(this.elem);
|
|
|
|
}
|
|
|
|
|
|
|
|
destroyEvents() {
|
|
|
|
/** Destroys all event listeners and observers. */
|
|
|
|
// We can simplify removal of event listeners by firing an AbortController
|
|
|
|
// abort signal. Must pass the signal to any event listeners on creation.
|
|
|
|
if (this.event_abort_controller) {
|
|
|
|
this.event_abort_controller.abort();
|
|
|
|
}
|
|
|
|
if (!isNullOrUndefined(this.resize_observer)) {
|
|
|
|
this.resize_observer.disconnect();
|
|
|
|
}
|
|
|
|
clearTimeout(this.resize_observer_timer);
|
|
|
|
this.resize_observer = null;
|
|
|
|
this.resize_observer_timer = null;
|
|
|
|
}
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
onMove(event, a, handle, b) {
|
|
|
|
/** Handles pointermove events by calculating and setting new size of elements.
|
|
|
|
*
|
|
|
|
* While we are dragging the handle, we only manually set size for the
|
|
|
|
* item before the handle. During this time, the element after the handle
|
|
|
|
* should have flexGrow=1 and flexShrink=1 so that it can react to the size
|
|
|
|
* of the element before the handle. This simplifies calculations since we
|
|
|
|
* only have to do math for one side of handle.
|
|
|
|
*/
|
|
|
|
const a_dims = a.elem.getBoundingClientRect();
|
|
|
|
const b_dims = b.elem.getBoundingClientRect();
|
|
|
|
const a_start = Math.ceil(handle.axis === 0 ? a_dims.top : a_dims.left);
|
|
|
|
const b_end = Math.floor(handle.axis === 0 ? b_dims.bottom : b_dims.right);
|
|
|
|
const a_lim = a_start + a.min_size;
|
|
|
|
const b_lim = b_end - b.min_size;
|
|
|
|
const half_pad = Math.floor(handle.pad_px / 2);
|
|
|
|
let pos = parseInt(handle.axis === 0 ? event.y : event.x);
|
|
|
|
pos = Math.min(Math.max(pos, a_lim + half_pad), b_lim - half_pad);
|
|
|
|
a.setSize((pos - half_pad) - a_start);
|
2024-05-20 20:34:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onResize() {
|
|
|
|
/** Resizes grid items on resize observer events. */
|
|
|
|
const curr_dims = this.elem.getBoundingClientRect();
|
|
|
|
const d_w = curr_dims.width - this.prev_dims.width;
|
|
|
|
const d_h = curr_dims.height - this.prev_dims.height;
|
|
|
|
|
|
|
|
// If no change to size, don't proceed.
|
|
|
|
if (d_w === 0 && d_h === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (d_w < 0) {
|
|
|
|
// Width decrease
|
|
|
|
for (const row of this.container.grid) {
|
|
|
|
let rem = Math.abs(d_w);
|
|
|
|
for (const col of row.slice().reverse()) {
|
|
|
|
const flex_grow = parseInt(col.elem.style.flexGrow);
|
|
|
|
rem = col.shrink(rem, {limit_to_base: false});
|
|
|
|
// Shrink causes flexGrow to be set back to default.
|
|
|
|
// We want to keep the current flex grow setting so we set it back.
|
|
|
|
col.render({force_flex_grow: flex_grow === 0 ? false : true});
|
|
|
|
if (rem <= 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (d_w > 0) {
|
|
|
|
// width increase
|
|
|
|
for (const row of this.container.grid) {
|
|
|
|
for (const col of row.slice().reverse()) {
|
|
|
|
const amt = col.grow(-1, {only_if_flex: true});
|
|
|
|
if (amt === 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (d_h < 0) {
|
|
|
|
// height decrease
|
|
|
|
let rem = Math.abs(d_h);
|
|
|
|
for (const row of this.container.rows.slice().reverse()) {
|
|
|
|
const flex_grow = parseInt(row.elem.style.flexGrow);
|
|
|
|
rem = row.shrink(rem, {limit_to_base: false});
|
|
|
|
// Shrink causes flexGrow to be set back to default.
|
|
|
|
// We want to keep the current flex grow setting so we set it back.
|
|
|
|
row.render({force_flex_grow: flex_grow === 0 ? false : true});
|
|
|
|
if (rem <= 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (d_h > 0) {
|
|
|
|
// height increase
|
|
|
|
for (const row of this.container.rows.slice().reverse()) {
|
|
|
|
if (row.grow(-1, {only_if_flex: true}) === 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.prev_dims = curr_dims;
|
|
|
|
}
|
|
|
|
|
|
|
|
show({row_idx, col_idx} = {}) {
|
|
|
|
/** Show a row or column.
|
|
|
|
* Columns require both the row_idx and col_idx.
|
|
|
|
*/
|
|
|
|
this.container.show({row_idx: row_idx, col_idx: col_idx});
|
|
|
|
this.container.updateVisibleHandles();
|
|
|
|
}
|
|
|
|
|
|
|
|
hide({row_idx, col_idx} = {}) {
|
|
|
|
/** Hide a row or column.
|
|
|
|
* Columns require both the row_idx and col_idx.
|
|
|
|
*/
|
|
|
|
this.container.hide({row_idx: row_idx, col_idx: col_idx});
|
|
|
|
this.container.updateVisibleHandles();
|
|
|
|
}
|
|
|
|
|
|
|
|
toggle({row_idx, col_idx, override} = {}) {
|
|
|
|
/** Toggle visibility of a row or column.
|
|
|
|
* Columns require both the row_idx and col_idx.
|
|
|
|
*/
|
|
|
|
this.container.toggle({
|
|
|
|
row_idx: row_idx,
|
|
|
|
col_idx: col_idx,
|
|
|
|
override: override,
|
|
|
|
});
|
|
|
|
this.container.updateVisibleHandles();
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleElem(elem, override) {
|
|
|
|
/** Toggles the nearest ResizeGridItem to the passed element. */
|
|
|
|
isElementThrowError(elem);
|
|
|
|
let _elem = elem.closest('.resize-grid--col');
|
|
|
|
if (isElement(_elem)) {
|
|
|
|
this.toggle({
|
|
|
|
row_idx: _elem.dataset.row,
|
|
|
|
col_idx: _elem.dataset.col,
|
|
|
|
override: override,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
_elem = elem.closest('.resize-grid--row');
|
|
|
|
isElementThrowError(_elem);
|
|
|
|
this.toggle({row_idx: _elem.dataset.row, override: override});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function resizeGridGetGrid(elem) {
|
|
|
|
/** Returns the nearest ResizeGrid to the passed element. */
|
|
|
|
// Need to find grid element so we can lookup by its id.
|
|
|
|
const grid_elem = elem.closest('.resize-grid');
|
|
|
|
if (!isElement(grid_elem)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Now try to get the actual ResizeGrid instance.
|
|
|
|
const grid = resize_grids[grid_elem.dataset.gridId];
|
|
|
|
if (isNullOrUndefined(grid)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return grid;
|
|
|
|
}
|
|
|
|
|
2024-05-22 16:54:00 +00:00
|
|
|
function resizeGridSetup(elem, {id, callbacks} = {}) {
|
2024-05-20 20:34:53 +00:00
|
|
|
/** Sets up a new ResizeGrid instance for the provided element. */
|
|
|
|
isElementThrowError(elem);
|
|
|
|
if (!isString(id)) {
|
|
|
|
id = _get_unique_id();
|
|
|
|
}
|
2024-05-22 16:54:00 +00:00
|
|
|
const grid = new ResizeGrid(id, elem, {callbacks: callbacks});
|
2024-05-20 20:34:53 +00:00
|
|
|
grid.setup();
|
|
|
|
resize_grids[id] = grid;
|
|
|
|
return grid;
|
|
|
|
}
|