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
132 changes: 110 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,146 @@



A library for resizable & repositionable panel layouts, using
[CSS `grid`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout).
A library for resizable & repositionable panel layouts.

- Zero depedencies, pure TypeScript, tiny.
- Implemented as a [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components),
interoperable with any framework and fully customizable.
interoperable with any framework.
- Zero DOM mutation at runtime, implemented entirely by generating using
[CSS `grid`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout) rules.
- Supports arbitrary theming via CSS [variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties) and [`::part`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::part).
- Supports async-aware container rendering for smooth animations even when rendering ovvurs over an event loop boundary.
- Covered in bees.

## Demo

<a href="https://texodus.github.io/regular-layout/"><img src="./examples/PREVIEW.png" /></a>

## Installation

```bash
npm install regular-layout
```

## Usage
## Quick Start

Add the `<regular-layout>` custom element to your HTML:
Import the library and add `<regular-layout>` to your HTML. Children are
matched to layout slots by their `name` attribute.

```html
<script type="module" src="regular-layout/dist/index.js"></script>

<regular-layout>
<div name="main">Main content</div>
<div name="sidebar">Sidebar content</div>
</regular-layout>
```

Create and manipulate layouts programmatically:
For draggable, tabbed panels, use `<regular-layout-frame>`:

```html
<regular-layout>
<regular-layout-frame name="main">
Main content
</regular-layout-frame>
<regular-layout-frame name="sidebar">
Sidebar content
</regular-layout-frame>
</regular-layout>
```

Panels must be added and remove programmatically (e.g they are not
auto-registered):

```javascript
import "regular-layout/dist/index.js";
const layout = document.querySelector("regular-layout");

const layout = document.querySelector('regular-layout');
// This adds the panel definition to the layout (and makes it visible via CSS),
// but does not mutat the DOM.
layout.insertPanel("main");
layout.insertPanel("sidebar");

// Add panels
layout.insertPanel('main');
layout.insertPanel('sidebar');
// This removes the panel from the layout (and hides it via CSS) but does not
// mutate the DOM.
layout.removePanel("sidebar");
```

// Save layout state
## Save/Restore

Layout state serializes to a JSON tree of splits and tabs, which can be
persisted and restored:

```javascript
const state = layout.save();
localStorage.setItem("layout", JSON.stringify(state));

// Later...
layout.restore(JSON.parse(localStorage.getItem("layout")));
```

// Remove panels (this does not change the DOM, the element is unslotted).
layout.removePanel('sidebar');
`restore()` dispatches a cancelable `regular-layout-before-resize` event before
applying the new state. Call `preventDefault()` to suspend the update, then
`layout.resumeResize()` when ready:

// Restore saved state
layout.restore(state);
```javascript
layout.addEventListener("regular-layout-before-resize", (event) => {
event.preventDefault();
// ... prepare for resize ...
layout.resumeResize();
});
```

Create repositionable panels using `<regular-layout-frame>`:
The `restore()` API can also be used as an alternative to
`insertPanel`/`removePanel` for initializing a `<regular-layout>`.

## Theming

Themes are plain CSS files that style the layout and its `::part()` selectors,
scoped by a class on `<regular-layout>`. Apply a theme by adding its stylesheet
and setting the class:

```html
<regular-layout>
<regular-layout-frame name="main">
Main content
</regular-layout-frame>
<link rel="stylesheet" href="regular-layout/themes/chicago.css">

<regular-layout class="chicago">
...
</regular-layout>
```
```

`<regular-layout-frame>` exposes these CSS parts:

| Part | Description |
|------|-------------|
| `titlebar` | Tab bar container |
| `tab` | Individual tab |
| `active-tab` | Currently selected tab |
| `close` | Close button |
| `active-close` | Close button on the active tab |
| `container` | Content area |

```css
regular-layout.mytheme regular-layout-frame::part(titlebar) {
background: #333;
}

regular-layout.mytheme regular-layout-frame::part(active-tab) {
background: #fff;
color: #000;
}
```

See [the example `themes/`](./themes) directory for examples of how to write a
complete theme for `<regular-layout>` and `regular-layout-frame>`.

## Events

| Event | Detail | Cancelable | Description |
|-------|--------|------------|-------------|
| `regular-layout-before-resize` | `{ calculatePresizePaths() }` | Yes | Fired before any layout change. Cancel to suspend until `resumeResize()`. |
| `regular-layout-update` | `Layout` | No | Fired after layout state is updated. |

```javascript
layout.addEventListener("regular-layout-update", (event) => {
console.log("New layout:", event.detail);
});
```
Binary file added examples/PREVIEW.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 10 additions & 8 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<link rel="stylesheet" href="../themes/gibson.css">
<link rel="stylesheet" href="../themes/fluxbox.css">
<link rel="stylesheet" href="../themes/hotdog.css">
<link rel="stylesheet" href="../themes/borland.css">
<script type="module" src="/.esbuild-serve/index.js"></script>
</head>
<body>
Expand All @@ -25,17 +26,18 @@
<option value="chicago">Chicago</option>
<option value="fluxbox">Fluxbox</option>
<option value="hotdog">Hot Dog</option>
<option value="borland">Borland</option>
</select>
</header>
<regular-layout class="lorax">
<regular-layout-frame name="AAA"> </regular-layout-frame>
<regular-layout-frame name="BBB"></regular-layout-frame>
<regular-layout-frame name="CCC"></regular-layout-frame>
<regular-layout-frame name="DDD"></regular-layout-frame>
<regular-layout-frame name="EEE"></regular-layout-frame>
<regular-layout-frame name="FFF"></regular-layout-frame>
<regular-layout-frame name="GGG"></regular-layout-frame>
<regular-layout-frame name="HHH"></regular-layout-frame>
<regular-layout-frame name="AAA">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</regular-layout-frame>
<regular-layout-frame name="BBB">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</regular-layout-frame>
<regular-layout-frame name="CCC">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</regular-layout-frame>
<regular-layout-frame name="DDD">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</regular-layout-frame>
<regular-layout-frame name="EEE">Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.</regular-layout-frame>
<regular-layout-frame name="FFF">Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</regular-layout-frame>
<regular-layout-frame name="GGG">Maecenas faucibus mollis interdum. Donec sed odio dui. Cras justo odio, dapibus ut facilisis in, egestas eget quam.</regular-layout-frame>
<regular-layout-frame name="HHH">Vestibulum id ligula porta felis euismod semper. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis.</regular-layout-frame>
</regular-layout>
</body>

Expand Down
6 changes: 6 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ add.addEventListener("click", () => {
layout.insertPanel(name, []);
});

const urlTheme = new URLSearchParams(window.location.search).get("theme");
if (urlTheme) {
themes.value = urlTheme;
layout.className = urlTheme;
}

themes.addEventListener("change", (_event) => {
layout.className = themes.value;
});
Expand Down
4 changes: 2 additions & 2 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export const DEFAULT_PHYSICS: Physics = Object.freeze({
SHOULD_ROUND: false,
OVERLAY_CLASSNAME: "overlay",
MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD: 0.15,
SPLIT_EDGE_TOLERANCE: 0.25,
SPLIT_ROOT_EDGE_TOLERANCE: 0.01,
SPLIT_EDGE_TOLERANCE: 0.33,
SPLIT_ROOT_EDGE_TOLERANCE: 0.03,
GRID_TRACK_COLLAPSE_TOLERANCE: 0.001,
OVERLAY_DEFAULT: "absolute",
GRID_DIVIDER_SIZE: 6,
Expand Down
2 changes: 1 addition & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export interface LayoutPath {
}

/**
* The detail payload of the `regular-layout-resize-before` event.
* The detail payload of the `regular-layout-before-resize` event.
*/
export interface PresizeDetail {
calculatePresizePaths(): Record<string, LayoutPath>;
Expand Down
4 changes: 2 additions & 2 deletions src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ declare global {
): void;

addEventListener(
name: "regular-layout-resize-before",
name: "regular-layout-before-resize",
cb: (e: RegularLayoutPresizeEvent) => void,
options?: { signal: AbortSignal },
): void;
Expand All @@ -76,7 +76,7 @@ declare global {
): void;

removeEventListener(
name: "regular-layout-resize-before",
name: "regular-layout-before-resize",
cb: (e: RegularLayoutPresizeEvent) => void,
): void;
}
Expand Down
2 changes: 1 addition & 1 deletion src/regular-layout-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const CSS = `

const HTML_TEMPLATE = `
<div part="titlebar"></div>
<slot part="container"></slot>
<div part="container"><slot></slot></div>
`;

type DragState = { moved?: boolean; path: LayoutPath };
Expand Down
8 changes: 4 additions & 4 deletions src/regular-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class RegularLayout extends HTMLElement {
this._stylesheet = new CSSStyleSheet();
this._cursor_stylesheet = new CSSStyleSheet();
this._cursor_override = false;
const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-resize-before`;
const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-resize`;
this._presizeQueue = new PresizeQueue(this, event_name);
this._overlayController = new OverlayController(this.create_overlay_host());
this._shadowRoot.adoptedStyleSheets = [
Expand Down Expand Up @@ -288,7 +288,7 @@ export class RegularLayout extends HTMLElement {

/**
* Restores the layout from a saved state synchronously, without
* dispatching the `regular-layout-resize-before` event.
* dispatching the `regular-layout-before-resize` event.
*
* @param layout - The layout tree to restore
*/
Expand All @@ -304,7 +304,7 @@ export class RegularLayout extends HTMLElement {
/**
* Restores the layout from a saved state.
*
* Before applying, dispatches a cancelable `regular-layout-resize-before`
* Before applying, dispatches a cancelable `regular-layout-before-resize`
* event. If the event is cancelled via `preventDefault()`, the layout
* update is suspended until {@link resumeResize} is called.
*
Expand Down Expand Up @@ -333,7 +333,7 @@ export class RegularLayout extends HTMLElement {

/**
* Resumes a layout update that was suspended by cancelling the
* `regular-layout-resize-before` event.
* `regular-layout-before-resize` event.
*/
resumeResize = () => {
this._presizeQueue.resume();
Expand Down
22 changes: 11 additions & 11 deletions tests/integration/resize-before.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { expect, test } from "../helpers/coverage.ts";
import { setupLayout, saveLayout, dragMouse } from "../helpers/integration.ts";
import { LAYOUTS } from "../helpers/fixtures.ts";

test("should fire regular-layout-resize-before on restore", async ({
test("should fire regular-layout-before-resize on restore", async ({
page,
}) => {
await setupLayout(page, LAYOUTS.SINGLE_AAA);
const fired = await page.evaluate(async () => {
const layout = document.querySelector("regular-layout");
let eventFired = false;
layout?.addEventListener("regular-layout-resize-before", () => {
layout?.addEventListener("regular-layout-before-resize", () => {
eventFired = true;
});
await layout?.restore({
Expand All @@ -42,7 +42,7 @@ test("should provide calculatePresizePaths in event detail", async ({
value: null,
};

layout?.addEventListener("regular-layout-resize-before", (e) => {
layout?.addEventListener("regular-layout-before-resize", (e) => {
presizePaths.value = (
e as CustomEvent
).detail.calculatePresizePaths() as Record<string, unknown>;
Expand Down Expand Up @@ -80,7 +80,7 @@ test("should suspend resize when preventDefault is called", async ({
await setupLayout(page, LAYOUTS.SINGLE_AAA);
const result = await page.evaluate(async () => {
const layout = document.querySelector("regular-layout");
layout?.addEventListener("regular-layout-resize-before", (e) => {
layout?.addEventListener("regular-layout-before-resize", (e) => {
e.preventDefault();
});

Expand Down Expand Up @@ -113,7 +113,7 @@ test("should resume suspended resize when resumeResize is called", async ({
await setupLayout(page, LAYOUTS.SINGLE_AAA);
const result = await page.evaluate(async () => {
const layout = document.querySelector("regular-layout");
layout?.addEventListener("regular-layout-resize-before", (e) => {
layout?.addEventListener("regular-layout-before-resize", (e) => {
e.preventDefault();
});

Expand Down Expand Up @@ -144,7 +144,7 @@ test("should proceed immediately when event is not cancelled", async ({
const tabs = await page.evaluate(async () => {
const layout = document.querySelector("regular-layout");
let eventCount = 0;
layout?.addEventListener("regular-layout-resize-before", () => {
layout?.addEventListener("regular-layout-before-resize", () => {
eventCount++;
});
await layout?.restore({
Expand All @@ -167,7 +167,7 @@ test("should fire resize-before on drag resize", async ({ page }) => {
await page.evaluate(() => {
const layout = document.querySelector("regular-layout");
(window as unknown as Record<string, number>).__resizeBeforeCount = 0;
layout?.addEventListener("regular-layout-resize-before", () => {
layout?.addEventListener("regular-layout-before-resize", () => {
(window as unknown as Record<string, number>).__resizeBeforeCount++;
});
});
Expand Down Expand Up @@ -196,7 +196,7 @@ test("should queue concurrent resizes and process sequentially", async ({
let callCount = 0;
let cancelFirst = true;

layout?.addEventListener("regular-layout-resize-before", (e) => {
layout?.addEventListener("regular-layout-before-resize", (e) => {
callCount++;
events.push(`before-${callCount}`);
if (cancelFirst) {
Expand Down Expand Up @@ -243,7 +243,7 @@ test("should provide pre-resize panel paths for nested layout", async ({
const paths = await page.evaluate(async () => {
const layout = document.querySelector("regular-layout");
let presizePaths: Record<string, Record<string, unknown>> | null = null;
layout?.addEventListener("regular-layout-resize-before", (e) => {
layout?.addEventListener("regular-layout-before-resize", (e) => {
presizePaths = (e as CustomEvent).detail.calculatePresizePaths();
});

Expand Down Expand Up @@ -299,7 +299,7 @@ test("should fire resize-before on double-click equalize", async ({ page }) => {
await page.evaluate(() => {
const layout = document.querySelector("regular-layout");
(window as unknown as Record<string, boolean>).__resizeBeforeFired = false;
layout?.addEventListener("regular-layout-resize-before", () => {
layout?.addEventListener("regular-layout-before-resize", () => {
(window as unknown as Record<string, boolean>).__resizeBeforeFired = true;
});
});
Expand All @@ -326,7 +326,7 @@ test("should not fire resize-before on restoreSync", async ({ page }) => {
const fired = await page.evaluate(() => {
const layout = document.querySelector("regular-layout");
let eventFired = false;
layout?.addEventListener("regular-layout-resize-before", () => {
layout?.addEventListener("regular-layout-before-resize", () => {
eventFired = true;
});
layout?.restoreSync({
Expand Down
Loading
Loading