Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const browserConfig: esbuild.BuildOptions = {
minify: true,
minifyWhitespace: true,
minifyIdentifiers: true,
mangleProps: /^[_#]/,
outfile: "dist/index.js",
platform: "browser",
format: "esm",
Expand Down
7 changes: 5 additions & 2 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const add = document.querySelector("#add") as HTMLButtonElement;
const save = document.querySelector("#save") as HTMLButtonElement;
const restore = document.querySelector("#restore") as HTMLButtonElement;
const clear = document.querySelector("#clear") as HTMLButtonElement;

// biome-ignore lint/style/noNonNullAssertion: demo
const layout = document.querySelector("regular-layout")!;

add.addEventListener("click", () => {
// Note: this *demo* implementation leaks `div` elements, because they
// are not removed from the light DOM by `clear` or `restore`. You must
Expand All @@ -36,12 +40,11 @@ add.addEventListener("click", () => {

themes.addEventListener("change", (_event) => {
layout.className = themes.value;
})
});

const req = await fetch("./layout.json");
let state = await req.json();

const layout = document.querySelector("regular-layout") as any;
layout.restore(state);
save.addEventListener("click", () => {
state = layout.save();
Expand Down
38 changes: 23 additions & 15 deletions src/layout/calculate_edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { SPLIT_EDGE_TOLERANCE, SPLIT_ROOT_EDGE_TOLERANCE } from "./constants";
import { DEFAULT_PHYSICS, type Physics } from "./constants";
import { insert_child } from "./insert_child";
import type { Layout, LayoutPath, Orientation, ViewWindow } from "./types";

Expand All @@ -33,39 +33,47 @@ export function calculate_edge(
slot: string,
drop_target: LayoutPath,
box?: DOMRect,
physics: Physics = DEFAULT_PHYSICS,
): LayoutPath {
// Check root edges first
if (col < SPLIT_ROOT_EDGE_TOLERANCE) {
if (col < physics.SPLIT_ROOT_EDGE_TOLERANCE) {
return insert_root_edge(panel, slot, drop_target, [0], true, "horizontal");
}

if (col > 1 - SPLIT_ROOT_EDGE_TOLERANCE) {
if (col > 1 - physics.SPLIT_ROOT_EDGE_TOLERANCE) {
return insert_root_edge(
panel,
slot,
drop_target,
drop_target.path.length > 0 ? drop_target.path : [],
drop_target.path.length > 0 ? drop_target.path : [1],
false,
"horizontal",
);
}

if (row < SPLIT_ROOT_EDGE_TOLERANCE) {
if (row < physics.SPLIT_ROOT_EDGE_TOLERANCE) {
return insert_root_edge(panel, slot, drop_target, [0], true, "vertical");
}

if (row > 1 - SPLIT_ROOT_EDGE_TOLERANCE) {
return insert_root_edge(panel, slot, drop_target, [], false, "vertical");
if (row > 1 - physics.SPLIT_ROOT_EDGE_TOLERANCE) {
return insert_root_edge(
panel,
slot,
drop_target,
drop_target.path.length > 0 ? drop_target.path : [1],
false,
"vertical",
);
}

// Check panel edges
const is_column_edge =
drop_target.column_offset < SPLIT_EDGE_TOLERANCE ||
drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE;
drop_target.column_offset < physics.SPLIT_EDGE_TOLERANCE ||
drop_target.column_offset > 1 - physics.SPLIT_EDGE_TOLERANCE;

const is_row_edge =
drop_target.row_offset < SPLIT_EDGE_TOLERANCE ||
drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE;
drop_target.row_offset < physics.SPLIT_EDGE_TOLERANCE ||
drop_target.row_offset > 1 - physics.SPLIT_EDGE_TOLERANCE;

// If both edges triggered, choose closer axis
if (is_column_edge && is_row_edge) {
Expand All @@ -86,8 +94,8 @@ export function calculate_edge(
slot,
drop_target,
use_column
? drop_target.column_offset < SPLIT_EDGE_TOLERANCE
: drop_target.row_offset < SPLIT_EDGE_TOLERANCE,
? drop_target.column_offset < physics.SPLIT_EDGE_TOLERANCE
: drop_target.row_offset < physics.SPLIT_EDGE_TOLERANCE,
use_column ? "horizontal" : "vertical",
);
}
Expand All @@ -97,7 +105,7 @@ export function calculate_edge(
panel,
slot,
drop_target,
drop_target.column_offset < SPLIT_EDGE_TOLERANCE,
drop_target.column_offset < physics.SPLIT_EDGE_TOLERANCE,
"horizontal",
);
}
Expand All @@ -107,7 +115,7 @@ export function calculate_edge(
panel,
slot,
drop_target,
drop_target.row_offset < SPLIT_EDGE_TOLERANCE,
drop_target.row_offset < physics.SPLIT_EDGE_TOLERANCE,
"vertical",
);
}
Expand Down
16 changes: 9 additions & 7 deletions src/layout/calculate_intersect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { GRID_DIVIDER_SIZE } from "./constants.ts";
import type { LayoutPath, LayoutDivider, Layout, ViewWindow } from "./types.ts";

const VIEW_WINDOW = {
Expand Down Expand Up @@ -42,21 +41,21 @@ export function calculate_intersection(
column: number,
row: number,
layout: Layout,
check_dividers?: DOMRect,
check_dividers?: { rect: DOMRect; size: number },
): LayoutPath | null | LayoutDivider;

export function calculate_intersection(
column: number,
row: number,
layout: Layout,
check_dividers?: DOMRect | null,
check_dividers?: { rect: DOMRect; size: number } | null,
): LayoutPath | null | LayoutDivider;

export function calculate_intersection(
column: number,
row: number,
layout: Layout,
check_dividers: DOMRect | null = null,
check_dividers: { rect: DOMRect; size: number } | null = null,
): LayoutPath | null | LayoutDivider {
return calculate_intersection_recursive(column, row, layout, check_dividers);
}
Expand All @@ -65,7 +64,7 @@ function calculate_intersection_recursive(
column: number,
row: number,
panel: Layout,
check_dividers: DOMRect | null,
check_dividers: { rect: DOMRect; size: number } | null,
parent_orientation: "horizontal" | "vertical" | null = null,
view_window: ViewWindow = structuredClone(VIEW_WINDOW),
path: number[] = [],
Expand Down Expand Up @@ -99,15 +98,18 @@ function calculate_intersection_recursive(
const position = is_vertical ? row : column;
const start_key = is_vertical ? "row_start" : "col_start";
const end_key = is_vertical ? "row_end" : "col_end";
const rect_dim = is_vertical ? check_dividers?.height : check_dividers?.width;
const rect_dim = is_vertical
? check_dividers?.rect?.height
: check_dividers?.rect?.width;

let current_pos = view_window[start_key];
const total_size = view_window[end_key] - view_window[start_key];
for (let i = 0; i < panel.children.length; i++) {
const next_pos = current_pos + total_size * panel.sizes[i];

// Check if position is on a divider
if (check_dividers && rect_dim) {
const divider_threshold = GRID_DIVIDER_SIZE / rect_dim;
const divider_threshold = check_dividers.size / rect_dim;
if (Math.abs(position - next_pos) < divider_threshold) {
return {
path: [...path, i],
Expand Down
132 changes: 94 additions & 38 deletions src/layout/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,108 @@
import type { OverlayMode } from "./types";

/**
* The prefix to use for `CustomEvent`s generated by `regular-layout`, e.g.
* `"regular-layout-before-update"`.
* Instance-specific constants which define the behavior and rendering details
* of a `<regular-layout>`.
*/
export const CUSTOM_EVENT_NAME_PREFIX = "regular-layout";
export interface Physics {
/**
* The prefix to use for `CustomEvent`s generated by `regular-layout`, e.g.
* `"regular-layout-before-update"`.
*/
CUSTOM_EVENT_NAME_PREFIX: string;

/**
* The minimum number of pixels the mouse must move to be considered a drag.
*/
export const MIN_DRAG_DISTANCE = 10;
/**
* The attribute name to use for matching child `Element`s to grid
* positions.
*/
CHILD_ATTRIBUTE_NAME: string;

/**
* Class name to use for child elements in overlay position (dragging).
*/
export const OVERLAY_CLASSNAME = "overlay";
/**
* The minimum number of pixels the mouse must move to be considered a drag.
*/
MIN_DRAG_DISTANCE: number;

/**
* The percentage of the maximum resize distance that will be clamped.
*
*/
export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15;
/**
* Should floating point pixel calculations be rounded. Useful for testing.
*/
SHOULD_ROUND: boolean;

/**
* Threshold from panel edge that is considered a split vs drop action.
*/
export const SPLIT_EDGE_TOLERANCE = 0.25;
/**
* Class name to use for child elements in overlay position (dragging).
*/
OVERLAY_CLASSNAME: string;

/**
* Threshold from _container_ edge that is considered a split action on the root
* node.
*/
export const SPLIT_ROOT_EDGE_TOLERANCE = 0.01;
/**
* The percentage of the maximum resize distance that will be clamped.
*/
MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD: number;

/**
* Tolerance threshold for considering two grid track positions as identical.
*
* When collecting and deduplicating track positions, any positions closer than
* this value are treated as the same position to avoid redundant grid tracks.
*/
export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.001;
/**
* Threshold from panel edge that is considered a split vs drop action.
*/
SPLIT_EDGE_TOLERANCE: number;

/**
* The overlay default behavior.
*/
export const OVERLAY_DEFAULT: OverlayMode = "absolute";
/**
* Threshold from _container_ edge that is considered a split action on the root
* node.
*/
SPLIT_ROOT_EDGE_TOLERANCE: number;

/**
* Tolerance threshold for considering two grid track positions as identical.
*
* When collecting and deduplicating track positions, any positions closer than
* this value are treated as the same position to avoid redundant grid tracks.
*/
GRID_TRACK_COLLAPSE_TOLERANCE: number;

/**
* The overlay default behavior.
*/
OVERLAY_DEFAULT: OverlayMode;

/**
* Width of split panel dividers in pixels (for hit-test purposes).
*/
GRID_DIVIDER_SIZE: number;

/**
* Whether the grid should trigger column resize if the grid itself is not
* the `event.target`.
*/
GRID_DIVIDER_CHECK_TARGET: boolean;
}

/**
* Width of split panel dividers in pixels (for hit-test purposes).
* Like `GlobalPhysics`, but suitable for partial definition for incremental
* updates.
*/
export const GRID_DIVIDER_SIZE = 6;
export interface PhysicsUpdate {
CUSTOM_EVENT_NAME_PREFIX?: string;
CHILD_ATTRIBUTE_NAME?: string;
MIN_DRAG_DISTANCE?: number;
SHOULD_ROUND?: boolean;
OVERLAY_CLASSNAME?: string;
MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD?: number;
SPLIT_EDGE_TOLERANCE?: number;
SPLIT_ROOT_EDGE_TOLERANCE?: number;
GRID_TRACK_COLLAPSE_TOLERANCE?: number;
OVERLAY_DEFAULT?: OverlayMode;
GRID_DIVIDER_SIZE?: number;
GRID_DIVIDER_CHECK_TARGET?: boolean;
}

export const DEFAULT_PHYSICS: Physics = Object.freeze({
CUSTOM_EVENT_NAME_PREFIX: "regular-layout",
CHILD_ATTRIBUTE_NAME: "name",
MIN_DRAG_DISTANCE: 10,
SHOULD_ROUND: false,
OVERLAY_CLASSNAME: "overlay",
MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD: 0.15,
SPLIT_EDGE_TOLERANCE: 0.25,
SPLIT_ROOT_EDGE_TOLERANCE: 0.01,
GRID_TRACK_COLLAPSE_TOLERANCE: 0.001,
OVERLAY_DEFAULT: "absolute",
GRID_DIVIDER_SIZE: 6,
GRID_DIVIDER_CHECK_TARGET: true,
});
Loading