svelte-tiler 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import { DndContext } from './shared/dnd.svelte.ts';
2
- import type { Tile, TileComponent, Tiles, TileType } from './model.ts';
2
+ import type { Tile, TileComponent, TileInsertData, Tiles, TileType } from './model.js';
3
3
  export declare const getTilerContext: () => TilerContext, setTilerContext: (context: TilerContext) => TilerContext;
4
4
  export interface TileDefinition<T extends TileType> {
5
5
  default: TileComponent<T>;
6
6
  onRemoveChild: (ctx: TilerContext, tile: Tiles[T], index: number) => void;
7
+ onInsert: (ctx: TilerContext, tile: Tiles[T], index: number, data: TileInsertData<T>) => void;
7
8
  onClear: (ctx: TilerContext, tile: Tiles[T]) => void;
8
9
  }
9
10
  export type TileDefinitions = {
@@ -13,29 +14,29 @@ export type TileEffects = {
13
14
  [T in TileType]?: (tile: Tiles[T]) => void | (() => void);
14
15
  };
15
16
  export interface TilerContextOptions {
16
- tiles: TileDefinitions;
17
- parents?: WeakMap<Tile, Tile>;
17
+ definitions: TileDefinitions;
18
18
  dnd?: DndContext<Tile>;
19
19
  effects?: TileEffects;
20
20
  }
21
21
  export declare class TilerContext {
22
22
  protected definitions: TileDefinitions;
23
- protected parents: WeakMap<Tile, Tile>;
24
23
  protected effects: TileEffects;
25
24
  protected updateRootFn: ((tile: Tile) => void) | undefined;
25
+ protected registry: FinalizationRegistry<string>;
26
+ protected tiles: Map<string, WeakRef<Tile>>;
27
+ protected parents: WeakMap<Tile, Tile>;
26
28
  readonly dnd: DndContext<Tile>;
27
29
  constructor(options: TilerContextOptions);
28
- registerParent(tile: Tile, parent: Tile): void;
29
- setUpdateRoot(tile: Tile, update: (tile: Tile) => void): void;
30
- getTileEffect(tile: Tile): ((tile: import("./model.ts").TileBase<"leaf"> & {
30
+ registerTile(tile: Tile, parent: Tile | ((tile: Tile) => void)): void;
31
+ getTileEffect(tile: Tile): ((tile: import("./model.js").TileBase<"leaf"> & {
31
32
  name: string;
32
- }) => void | (() => void)) | ((tile: import("./model.ts").TileBase<"split"> & {
33
+ }) => void | (() => void)) | ((tile: import("./model.js").TileBase<"split"> & {
33
34
  constraints: Array<import("./shared/constraints.ts").Constraint[]>;
34
35
  weights: number[];
35
36
  direction: import("./shared/spatial.ts").Direction;
36
37
  resizer?: string;
37
38
  gapPx: number;
38
- }) => void | (() => void)) | ((tile: import("./model.ts").TileBase<"tabs"> & {
39
+ }) => void | (() => void)) | ((tile: import("./model.js").TileBase<"tabs"> & {
39
40
  titles: string[];
40
41
  selectedTab: number;
41
42
  headersDirection: import("./tiles/tabs.svelte.ts").HeadersDirection;
@@ -44,7 +45,13 @@ export declare class TilerContext {
44
45
  empty?: string;
45
46
  }) => void | (() => void)) | undefined;
46
47
  getTileComponent(tile: Tile): TileComponent<"leaf"> | TileComponent<"split"> | TileComponent<"tabs">;
47
- replaceWith(tile: Tile, replace: Tile): void;
48
- removeChild(tile: Tile, index: number): void;
49
- destroy(tile: Tile): void;
48
+ replace(tile: Tile | undefined, replace: Tile): void;
49
+ replaceTile(tileId: string | undefined, replace: Tile): void;
50
+ insertInto<T extends TileType>(tile: Tiles[T], index: number, data: TileInsertData<T>): void;
51
+ insertIntoTile<T extends TileType>(tileId: string, type: T, index: number, data: TileInsertData<T>): void;
52
+ removeChildFrom(tile: Tile, index: number): void;
53
+ removeChildFromTile(tileId: string, index: number): void;
54
+ remove(tile: Tile): void;
55
+ removeTile(tileId: string): void;
56
+ protected getTileById(tileId: string): Tile;
50
57
  }
@@ -0,0 +1,92 @@
1
+ import { createContext } from 'svelte';
2
+ import { DndContext } from "./shared/dnd.svelte.js";
3
+ export const [getTilerContext, setTilerContext] = createContext();
4
+ export class TilerContext {
5
+ definitions;
6
+ effects;
7
+ updateRootFn;
8
+ registry = new FinalizationRegistry((id) => {
9
+ this.tiles.delete(id);
10
+ });
11
+ tiles = new Map();
12
+ parents = new WeakMap();
13
+ dnd;
14
+ constructor(options) {
15
+ this.definitions = options.definitions;
16
+ this.dnd = options.dnd ?? new DndContext();
17
+ this.effects = options.effects ?? {};
18
+ }
19
+ registerTile(tile, parent) {
20
+ this.tiles.set(tile.id, new WeakRef(tile));
21
+ this.registry.register(tile, tile.id);
22
+ if (typeof parent === 'function') {
23
+ this.parents.delete(tile);
24
+ this.updateRootFn = parent;
25
+ }
26
+ else {
27
+ this.parents.set(tile, parent);
28
+ }
29
+ }
30
+ getTileEffect(tile) {
31
+ return this.effects[tile.type];
32
+ }
33
+ getTileComponent(tile) {
34
+ return this.definitions[tile.type].default;
35
+ }
36
+ replace(tile, replace) {
37
+ if (tile) {
38
+ const parent = this.parents.get(tile);
39
+ if (parent) {
40
+ const index = parent.children.findIndex((c) => c.id === tile.id);
41
+ if (index < 0) {
42
+ throw new Error(`Invalid parent for "${tile.id}" tile`);
43
+ }
44
+ parent.children[index] = replace;
45
+ return;
46
+ }
47
+ }
48
+ this.updateRootFn?.(replace);
49
+ }
50
+ replaceTile(tileId, replace) {
51
+ const tile = tileId && this.getTileById(tileId);
52
+ this.replace(tile || undefined, replace);
53
+ }
54
+ insertInto(tile, index, data) {
55
+ this.definitions[tile.type].onInsert(this, tile, index, data);
56
+ }
57
+ insertIntoTile(tileId, type, index, data) {
58
+ const tile = this.getTileById(tileId);
59
+ if (tile.type !== type) {
60
+ throw new Error(`Tile type mismatch: expected "${type}", but got "${tile.type}"`);
61
+ }
62
+ this.insertInto(tile, index, data);
63
+ }
64
+ removeChildFrom(tile, index) {
65
+ this.definitions[tile.type].onRemoveChild(this, tile, index);
66
+ }
67
+ removeChildFromTile(tileId, index) {
68
+ this.removeChildFrom(this.getTileById(tileId), index);
69
+ }
70
+ remove(tile) {
71
+ const parent = this.parents.get(tile);
72
+ if (parent === undefined) {
73
+ this.definitions[tile.type].onClear(this, tile);
74
+ return;
75
+ }
76
+ const index = parent.children.findIndex((c) => c.id === tile.id);
77
+ if (index < 0) {
78
+ throw new Error(`Invalid parent for "${tile.id}" tile`);
79
+ }
80
+ this.removeChildFrom(parent, index);
81
+ }
82
+ removeTile(tileId) {
83
+ this.remove(this.getTileById(tileId));
84
+ }
85
+ getTileById(tileId) {
86
+ const tile = this.tiles.get(tileId)?.deref();
87
+ if (tile === undefined) {
88
+ throw new Error(`Unable to find tile with "${tileId}" id`);
89
+ }
90
+ return tile;
91
+ }
92
+ }
@@ -0,0 +1,11 @@
1
+ <script lang="ts">
2
+ import type { Tile } from './model.js';
3
+ import Self from './debug.svelte';
4
+
5
+ const { tile, level = 0 }: { tile: Tile; level?: number } = $props();
6
+ </script>
7
+
8
+ <div style="padding-left: {level * 12}px;">{tile.type} ({tile.id})</div>
9
+ {#each tile.children as c (c.id)}
10
+ <Self tile={c} level={level + 1} />
11
+ {/each}
@@ -0,0 +1,8 @@
1
+ import type { Tile } from './model.js';
2
+ type $$ComponentProps = {
3
+ tile: Tile;
4
+ level?: number;
5
+ };
6
+ declare const Debug: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type Debug = ReturnType<typeof Debug>;
8
+ export default Debug;
package/dist/dnd.d.ts CHANGED
@@ -1,6 +1,21 @@
1
- import { DndContext, Droppable } from './shared/dnd.svelte.js';
1
+ import { Draggable, Droppable, type DraggableOptions, type StopEvent } from './shared/dnd.svelte.js';
2
2
  import type { Tile } from './model.js';
3
- export declare class TileDroppable<T extends Tile> extends Droppable<Tile, T> {
4
- readonly targetTileId: string;
5
- constructor(ctx: DndContext<Tile>, targetTileId: string);
3
+ import type { TilerContext } from './context.ts';
4
+ export declare class TileDropTarget<T extends Tile> extends Droppable<Tile, T> {
5
+ readonly tileId: string;
6
+ protected tilerCtx: TilerContext;
7
+ constructor(ctx: TilerContext, tileId: string);
8
+ getTargetTileId(): string | undefined;
9
+ protected isOwnChild(d: Draggable): d is TileDragSource;
10
+ }
11
+ export interface TileDragSourceOptions extends DraggableOptions<Tile> {
12
+ parentTileId: string;
13
+ childIndex: number;
14
+ }
15
+ export declare class TileDragSource extends Draggable<Tile> {
16
+ protected tilerCtx: TilerContext;
17
+ readonly parentTileId: string;
18
+ readonly childIndex: number;
19
+ constructor(ctx: TilerContext, options: TileDragSourceOptions);
20
+ protected onStop({ reason }: StopEvent): void;
6
21
  }
package/dist/dnd.js CHANGED
@@ -1,8 +1,33 @@
1
- import { DndContext, Droppable } from './shared/dnd.svelte.js';
2
- export class TileDroppable extends Droppable {
3
- targetTileId;
4
- constructor(ctx, targetTileId) {
5
- super(ctx);
6
- this.targetTileId = targetTileId;
1
+ import { Draggable, Droppable, } from './shared/dnd.svelte.js';
2
+ export class TileDropTarget extends Droppable {
3
+ tileId;
4
+ tilerCtx;
5
+ constructor(ctx, tileId) {
6
+ super(ctx.dnd);
7
+ this.tileId = tileId;
8
+ this.tilerCtx = ctx;
9
+ }
10
+ getTargetTileId() {
11
+ return this.tileId;
12
+ }
13
+ isOwnChild(d) {
14
+ return d instanceof TileDragSource && this.tileId === d.parentTileId;
15
+ }
16
+ }
17
+ export class TileDragSource extends Draggable {
18
+ tilerCtx;
19
+ parentTileId;
20
+ childIndex;
21
+ constructor(ctx, options) {
22
+ super(ctx.dnd, options);
23
+ this.tilerCtx = ctx;
24
+ this.parentTileId = options.parentTileId;
25
+ this.childIndex = options.childIndex;
26
+ }
27
+ onStop({ reason }) {
28
+ if (reason !== 'drop') {
29
+ return;
30
+ }
31
+ this.tilerCtx.removeChildFromTile(this.parentTileId, this.childIndex);
7
32
  }
8
33
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export * from './context.svelte.ts';
2
- export * from './model.ts';
1
+ export * from './context.ts';
2
+ export * from './model.js';
3
3
  export * from './dnd.ts';
4
4
  export { default as Panel } from './panel.svelte';
5
5
  export { default as Tiler } from './tiler.svelte';
6
+ export { default as Debug } from './debug.svelte';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- export * from "./context.svelte.js";
2
- export * from "./model.js";
1
+ export * from "./context.js";
2
+ export * from './model.js';
3
3
  export * from "./dnd.js";
4
4
  export { default as Panel } from './panel.svelte';
5
5
  export { default as Tiler } from './tiler.svelte';
6
+ export { default as Debug } from './debug.svelte';
package/dist/model.d.ts CHANGED
@@ -11,10 +11,27 @@ export type Tiles = {
11
11
  [T in TileType]: TileBase<T> & TileRegistry[T];
12
12
  };
13
13
  export type Tile = Tiles[TileType];
14
+ export interface TileInsertRequirements {
15
+ }
16
+ type WithOnlyRequired<T, K extends keyof T> = Required<Pick<T, K>> & Partial<Omit<T, K>>;
17
+ export type TileInsertData<T extends TileType> = WithOnlyRequired<Tiles[T], (T extends keyof TileInsertRequirements ? TileInsertRequirements[T] & keyof Tiles[T] : never) | 'children'>;
14
18
  export type TileProps<T extends TileType> = {
15
19
  tile: Tiles[T];
16
20
  parent: Tile | undefined;
17
21
  index: number;
18
22
  child: Snippet<[number]>;
19
23
  };
20
- export type TileComponent<T extends TileType> = Component<TileProps<T>, {}, 'tile' | 'parent'>;
24
+ export type TileComponent<T extends TileType> = Component<TileProps<T>, {}, 'tile'>;
25
+ export type TileArrayProperties<T extends TileType> = {
26
+ [K in keyof Tiles[T] as Tiles[T][K] extends Array<any> ? K : never]: Tiles[T][K];
27
+ };
28
+ export type TileArrayProperty<T extends TileType> = keyof TileArrayProperties<T>;
29
+ /**
30
+ * @returns Returns the insertion position taking into account removed duplicates
31
+ */
32
+ export declare function insertWithDeduplication<T extends TileType>(tile: Tiles[T], i: number, arrays: {
33
+ [K in Exclude<TileArrayProperty<T>, 'children'>]?: Tiles[T][K & keyof Tiles[T]];
34
+ } & {
35
+ children: Tile[];
36
+ }): number;
37
+ export {};
package/dist/model.js CHANGED
@@ -1 +1,32 @@
1
- export {};
1
+ /**
2
+ * @returns Returns the insertion position taking into account removed duplicates
3
+ */
4
+ export function insertWithDeduplication(tile, i, arrays) {
5
+ const newIds = new Set(arrays.children.map((c) => c.id));
6
+ let write = 0;
7
+ let shift = 0;
8
+ const c = tile.children;
9
+ const l = c.length;
10
+ const keys = Object.keys(arrays);
11
+ const tileArrays = keys.map((arr) => tile[arr]);
12
+ for (let read = 0; read < l; read++) {
13
+ if (!newIds.has(c[read].id)) {
14
+ for (const arr of tileArrays) {
15
+ // @ts-expect-error ignore
16
+ arr[write] = arr[read];
17
+ }
18
+ write++;
19
+ }
20
+ else if (read < i) {
21
+ shift++;
22
+ }
23
+ }
24
+ i -= shift;
25
+ for (const key of keys) {
26
+ // @ts-expect-error ignore
27
+ tile[key].length = write;
28
+ // @ts-expect-error ignore
29
+ tile[key].splice(i, 0, ...arrays[key]);
30
+ }
31
+ return i;
32
+ }
@@ -1,4 +1,4 @@
1
- import type { Tile } from './model.ts';
1
+ import type { Tile } from './model.js';
2
2
  type $$ComponentProps = {
3
3
  layout: Tile | undefined;
4
4
  };
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { getTilerContext } from './context.svelte.js';
2
+ import { getTilerContext } from './context.js';
3
3
  import type { Tile } from './model.js';
4
4
  import Self from './render.svelte';
5
5
 
@@ -15,13 +15,7 @@
15
15
 
16
16
  const ctx = getTilerContext();
17
17
 
18
- $effect(() => {
19
- if (parent) {
20
- ctx.registerParent(tile, parent);
21
- } else {
22
- ctx.setUpdateRoot(tile, (t) => (parent = t));
23
- }
24
- });
18
+ $effect(() => ctx.registerTile(tile, parent ?? ((t) => (parent = t))));
25
19
 
26
20
  $effect(() => ctx.getTileEffect(tile)?.(tile as never));
27
21
 
@@ -29,7 +23,7 @@
29
23
  </script>
30
24
 
31
25
  {#snippet child(index: number)}
32
- <Self bind:parent={tile} bind:tile={tile.children[index]} {index} />
26
+ <Self parent={tile} bind:tile={tile.children[index]} {index} />
33
27
  {/snippet}
34
28
 
35
- <TileComponent bind:parent bind:tile={tile as never} {index} {child} />
29
+ <TileComponent {parent} bind:tile={tile as never} {index} {child} />
@@ -1,4 +1,4 @@
1
- import type { Tile } from './model.ts';
1
+ import type { Tile } from './model.js';
2
2
  type $$ComponentProps = {
3
3
  tile: Tile;
4
4
  parent: Tile | undefined;
package/dist/tiler.svelte CHANGED
@@ -3,7 +3,7 @@
3
3
  setTilerContext,
4
4
  TilerContext,
5
5
  type TilerContextOptions,
6
- } from './context.svelte.js';
6
+ } from './context.js';
7
7
  import type { Tile } from './model.js';
8
8
  import Panel from './panel.svelte';
9
9
 
@@ -1,4 +1,4 @@
1
- import { type TilerContextOptions } from './context.svelte.ts';
1
+ import { type TilerContextOptions } from './context.js';
2
2
  import type { Tile } from './model.js';
3
3
  type $$ComponentProps = {
4
4
  layout: Tile | undefined;
@@ -32,6 +32,8 @@
32
32
  export function onRemoveChild() {}
33
33
 
34
34
  export function onClear() {}
35
+
36
+ export function onInsert() {}
35
37
  </script>
36
38
 
37
39
  <script lang="ts">
@@ -12,6 +12,7 @@ type LeafContext<N extends string = string> = Registry<N, Snippet<[Tiles['leaf']
12
12
  export declare function setup<N extends string>(leafs: LeafContext<N>): (name: N) => Tiles["leaf"];
13
13
  export declare function onRemoveChild(): void;
14
14
  export declare function onClear(): void;
15
+ export declare function onInsert(): void;
15
16
  declare const Leaf: import("svelte").Component<TileProps<"leaf">, {}, "tile">;
16
17
  type Leaf = ReturnType<typeof Leaf>;
17
18
  export default Leaf;
@@ -10,9 +10,15 @@
10
10
  } from '../shared/constraints.js';
11
11
  import type { Direction } from '../shared/spatial.js';
12
12
  import { almostEqual } from '../shared/math.js';
13
- import type { Tile, TileProps, Tiles } from '../model.js';
14
- import type { TilerContext } from '../context.svelte.js';
15
- import { TileDroppable } from '../dnd.js';
13
+ import {
14
+ insertWithDeduplication,
15
+ type Tile,
16
+ type TileInsertData,
17
+ type TileProps,
18
+ type Tiles,
19
+ } from '../model.js';
20
+ import type { TilerContext } from '../context.js';
21
+ import { TileDropTarget } from '../dnd.js';
16
22
 
17
23
  declare module '../model.js' {
18
24
  interface TileRegistry {
@@ -88,11 +94,11 @@
88
94
  ctx.dnd.targetId && ctx.dnd.droppables.get(ctx.dnd.targetId);
89
95
  if (
90
96
  !droppable ||
91
- (droppable instanceof TileDroppable &&
92
- tile.children.every((c) => c.id !== droppable.targetTileId))
97
+ (droppable instanceof TileDropTarget &&
98
+ tile.id !== droppable.getTargetTileId())
93
99
  ) {
94
100
  tick().then(() => {
95
- ctx.replaceWith(tile, tile.children[1 - i]);
101
+ ctx.replace(tile, tile.children[1 - i]);
96
102
  });
97
103
  return;
98
104
  }
@@ -103,23 +109,26 @@
103
109
  tile.constraints.splice(i, 1);
104
110
  return;
105
111
  }
106
- ctx.destroy(tile);
112
+ ctx.remove(tile);
107
113
  }
108
114
 
109
- export function onClear(ctx: TilerContext, tile: Tiles['split']) {
110
- if (tile.children.length > 0) {
111
- ctx.replaceWith(tile, tile.children[0]);
112
- }
113
- }
115
+ export function onClear(_ctx: TilerContext, _tile: Tiles['split']) {}
114
116
 
115
- export function insertTile(
116
- node: Tiles['split'],
117
+ export function onInsert(
118
+ _ctx: TilerContext,
119
+ tile: Tiles['split'],
117
120
  index: number,
118
- { tile, constraints = [], weight = 1 }: SplitTileOptions
121
+ {
122
+ children,
123
+ constraints = children.map(() => []),
124
+ weights = children.map(() => 1),
125
+ }: TileInsertData<'split'>
119
126
  ) {
120
- node.children.splice(index, 0, tile);
121
- node.constraints.splice(index, 0, constraints);
122
- node.weights.splice(index, 0, weight);
127
+ insertWithDeduplication<'split'>(tile, index, {
128
+ children,
129
+ constraints,
130
+ weights,
131
+ });
123
132
  }
124
133
  </script>
125
134
 
@@ -201,7 +210,7 @@
201
210
  : currentPos > resizerRect.top
202
211
  ) {
203
212
  if (currentDir !== lastDir) {
204
- startPos = previousPos;
213
+ startPos = currentPos;
205
214
  this.syncWeights();
206
215
  lastDir = currentDir;
207
216
  }
@@ -3,8 +3,8 @@ import type { Registry } from '../shared/registry.js';
3
3
  import { Draggable } from '../shared/dnd.svelte.js';
4
4
  import { type Constraint } from '../shared/constraints.js';
5
5
  import type { Direction } from '../shared/spatial.js';
6
- import type { Tile, TileProps, Tiles } from '../model.js';
7
- import type { TilerContext } from '../context.svelte.js';
6
+ import { type Tile, type TileInsertData, type TileProps, type Tiles } from '../model.js';
7
+ import type { TilerContext } from '../context.js';
8
8
  declare module '../model.js' {
9
9
  interface TileRegistry {
10
10
  split: {
@@ -35,8 +35,8 @@ type SplitContext<R extends string = string> = {
35
35
  };
36
36
  export declare function setup<R extends string>(ctx: SplitContext<R>): (options: SplitOptions<R>) => Tiles["split"];
37
37
  export declare function onRemoveChild(ctx: TilerContext, tile: Tiles['split'], i: number): void;
38
- export declare function onClear(ctx: TilerContext, tile: Tiles['split']): void;
39
- export declare function insertTile(node: Tiles['split'], index: number, { tile, constraints, weight }: SplitTileOptions): void;
38
+ export declare function onClear(_ctx: TilerContext, _tile: Tiles['split']): void;
39
+ export declare function onInsert(_ctx: TilerContext, tile: Tiles['split'], index: number, { children, constraints, weights, }: TileInsertData<'split'>): void;
40
40
  declare const Split: import("svelte").Component<TileProps<"split">, {}, "tile">;
41
41
  type Split = ReturnType<typeof Split>;
42
42
  export default Split;
@@ -1,9 +1,14 @@
1
1
  <script lang="ts" module>
2
2
  import { getContext, setContext, type Snippet } from 'svelte';
3
3
 
4
+ import type { Draggable } from '../shared/dnd.svelte.js';
4
5
  import type { Registry } from '../shared/registry.js';
5
- import type { Tile, Tiles } from '../model.js';
6
- import type { TilerContext } from '../context.svelte.js';
6
+ import {
7
+ insertWithDeduplication,
8
+ type Tile,
9
+ type Tiles,
10
+ } from '../model.js';
11
+ import type { TilerContext } from '../context.js';
7
12
 
8
13
  export type HeadersDirection = Direction | 'none';
9
14
 
@@ -18,6 +23,9 @@
18
23
  empty?: string;
19
24
  };
20
25
  }
26
+ interface TileInsertRequirements {
27
+ tabs: 'titles';
28
+ }
21
29
  }
22
30
 
23
31
  export interface TabsOptions<
@@ -57,7 +65,7 @@
57
65
  }
58
66
 
59
67
  interface SplitOptions {
60
- type: 'row' | 'column';
68
+ type: Direction;
61
69
  parent: Tile | undefined;
62
70
  pivot: Tiles['tabs'];
63
71
  offset: number;
@@ -69,7 +77,7 @@
69
77
  E extends string = string,
70
78
  A extends string = string,
71
79
  > {
72
- createSplit?: (options: SplitOptions) => Tile;
80
+ applySplit?: (options: SplitOptions) => void;
73
81
  actions?: Registry<A, Snippet<[Tiles['tabs']]> | undefined>;
74
82
  headers?: Registry<
75
83
  H,
@@ -93,7 +101,7 @@
93
101
  i: number
94
102
  ) {
95
103
  if (tile.children.length < 2) {
96
- ctx.destroy(tile);
104
+ ctx.remove(tile);
97
105
  return;
98
106
  }
99
107
  if (tile.selectedTab >= i) {
@@ -111,62 +119,30 @@
111
119
  }
112
120
  }
113
121
 
114
- export function insertTabs(
122
+ export function onInsert(
123
+ _ctx: TilerContext,
115
124
  tile: Tiles['tabs'],
116
125
  i: number,
117
- {
118
- titles,
119
- children,
120
- }: {
121
- titles: string[];
122
- children: Tile[];
123
- }
126
+ data: TileInsertData<'tabs'>
124
127
  ) {
125
- const newIds = new Set(children.map((c) => c.id));
126
- let write = 0;
127
- let shift = 0;
128
- const c = tile.children;
129
- const t = tile.titles;
130
- const l = t.length;
131
- for (let read = 0; read < l; read++) {
132
- if (!newIds.has(c[read].id)) {
133
- t[write] = t[read];
134
- c[write] = c[read];
135
- write++;
136
- } else if (read < i) {
137
- shift++;
138
- }
139
- }
140
- c.length = write;
141
- t.length = write;
142
- i -= shift;
143
- tile.children.splice(i, 0, ...children);
144
- tile.titles.splice(i, 0, ...titles);
145
- tile.selectedTab = i;
128
+ tile.selectedTab = insertWithDeduplication<'tabs'>(tile, i, {
129
+ titles: data.titles,
130
+ children: data.children,
131
+ });
146
132
  }
147
133
  </script>
148
134
 
149
135
  <script lang="ts">
150
- import {
151
- DndContext,
152
- Draggable,
153
- type DraggableOptions,
154
- type StopEvent,
155
- } from '../shared/dnd.svelte.js';
156
136
  import {
157
137
  getRectParts,
158
138
  type Direction,
159
139
  type EdgePart,
160
140
  } from '../shared/spatial.js';
161
- import { getTilerContext } from '../context.svelte.js';
162
- import type { TileProps } from '../model.js';
163
- import { TileDroppable } from '../dnd.js';
141
+ import { getTilerContext } from '../context.js';
142
+ import type { TileInsertData, TileProps } from '../model.js';
143
+ import { TileDragSource, TileDropTarget } from '../dnd.js';
164
144
 
165
- let {
166
- tile = $bindable(),
167
- parent = $bindable(),
168
- child,
169
- }: TileProps<'tabs'> = $props();
145
+ let { tile = $bindable(), parent, child }: TileProps<'tabs'> = $props();
170
146
 
171
147
  const ctx = getTilerContext();
172
148
  const tabsCtx = getContext<TabsContext | undefined>(TABS_CONTEXT_KEY);
@@ -182,35 +158,35 @@
182
158
  const empty = $derived(
183
159
  (tile.empty !== undefined && tabsCtx?.empty?.get(tile.empty)) || undefined
184
160
  );
185
- const edgeRatio = $derived(tabsCtx?.createSplit ? 0.1 : 0);
161
+ const edgeRatio = $derived(tabsCtx?.applySplit ? 0.1 : 0);
186
162
 
187
- class TabsDroppable extends TileDroppable<Tiles['tabs']> {
163
+ class TabsTileDropTarget extends TileDropTarget<Tiles['tabs']> {
188
164
  accepts(d: Draggable<Tile>): d is Draggable<Tiles['tabs']> {
189
165
  const t = d.data;
190
166
  return (
191
167
  t?.type === 'tabs' &&
192
168
  !(
169
+ this.isOwnChild(d) &&
193
170
  t.children.length === 1 &&
194
171
  tile.children.length === 1 &&
195
- t.children[0].id === tile.children[0].id &&
196
- d instanceof DraggableTab
172
+ t.children[0].id === tile.children[0].id
197
173
  )
198
174
  );
199
175
  }
200
176
  }
201
177
 
202
- class DroppableSurface extends TabsDroppable {
178
+ class SimpleTabsDropTarget extends TabsTileDropTarget {
203
179
  protected onDrop(tabs: Tiles['tabs']): void {
204
- insertTabs(tile, tile.children.length, tabs);
180
+ ctx.insertInto<'tabs'>(tile, tile.children.length, tabs);
205
181
  }
206
182
  }
207
183
 
208
- class DroppableRect extends TabsDroppable {
184
+ class SegmentedTabsTileDropTarget extends TabsTileDropTarget {
209
185
  #edgeRatio: number;
210
186
  hpart: EdgePart | undefined = $state.raw();
211
187
  vpart: EdgePart | undefined = $state.raw();
212
188
 
213
- constructor(ctx: DndContext<Tile>, edgeRatio: number) {
189
+ constructor(ctx: TilerContext, edgeRatio: number) {
214
190
  super(ctx, tile.id);
215
191
  this.#edgeRatio = edgeRatio;
216
192
  }
@@ -224,11 +200,11 @@
224
200
  }
225
201
  }
226
202
 
227
- class DroppableTab extends DroppableRect {
203
+ class SegmentedTabDropTarget extends SegmentedTabsTileDropTarget {
228
204
  #id: string;
229
205
  #index: number;
230
206
 
231
- constructor(ctx: DndContext<Tile>, id: string, index: number) {
207
+ constructor(ctx: TilerContext, id: string, index: number) {
232
208
  super(ctx, tile.headersDirection === 'none' ? 0 : 0.5);
233
209
  this.#id = id;
234
210
  this.#index = index;
@@ -242,25 +218,33 @@
242
218
  tile.headersDirection !== 'none'
243
219
  ? (tile.headersDirection === 'row' ? this.hpart : this.vpart) ===
244
220
  'end'
245
- : d instanceof DraggableTab && d.index <= i
221
+ : this.isOwnChild(d) && d.childIndex <= i
246
222
  ) {
247
223
  i++;
248
224
  }
249
- insertTabs(tile, i, tabs);
225
+ ctx.insertInto<'tabs'>(tile, i, tabs);
250
226
  }
251
227
  }
252
228
 
253
- class DroppableContent extends DroppableRect {
229
+ class SegmentedContentDropTarget extends SegmentedTabsTileDropTarget {
230
+ get isCenter() {
231
+ return this.hpart === 'center' && this.vpart === 'center';
232
+ }
233
+
234
+ getTargetTileId(): string | undefined {
235
+ return this.isCenter ? tile.id : parent?.id;
236
+ }
237
+
254
238
  protected onDrop(tabs: Tiles['tabs'], d: Draggable): void {
255
239
  const id = tabs.children[0].id;
256
- if (this.hpart === 'center' && this.vpart === 'center') {
240
+ if (this.isCenter) {
257
241
  let i = tile.children.findIndex((t) => t.id === id);
258
- if (i < 0 && d instanceof DraggableTab) {
259
- i = d.index;
242
+ if (i < 0 && this.isOwnChild(d)) {
243
+ i = d.childIndex;
260
244
  }
261
- insertTabs(tile, i < 0 ? tile.children.length : i, tabs);
262
- } else if (tabsCtx?.createSplit) {
263
- parent = tabsCtx.createSplit({
245
+ ctx.insertInto<'tabs'>(tile, i < 0 ? tile.children.length : i, tabs);
246
+ } else if (tabsCtx?.applySplit) {
247
+ tabsCtx.applySplit({
264
248
  parent,
265
249
  type:
266
250
  this.hpart === 'start' || this.hpart === 'end' ? 'row' : 'column',
@@ -279,26 +263,6 @@
279
263
  }
280
264
  }
281
265
 
282
- interface DraggableTabOptions extends DraggableOptions<Tile> {
283
- index: number;
284
- }
285
-
286
- class DraggableTab extends Draggable<Tile> {
287
- readonly index: number;
288
-
289
- constructor(ctx: DndContext<Tile>, options: DraggableTabOptions) {
290
- super(ctx, options);
291
- this.index = options.index;
292
- }
293
-
294
- protected onStop({ reason }: StopEvent): void {
295
- if (reason !== 'drop') {
296
- return;
297
- }
298
- ctx.removeChild(tile, this.index);
299
- }
300
- }
301
-
302
266
  function handleKeydown(e: KeyboardEvent & { currentTarget: HTMLElement }) {
303
267
  if (e.key === 'Enter' || e.key === ' ') {
304
268
  e.preventDefault();
@@ -306,9 +270,13 @@
306
270
  }
307
271
  }
308
272
 
309
- const droppableSpacer = $derived(new DroppableSurface(ctx.dnd, tile.id));
310
- const droppableContent = $derived(new DroppableContent(ctx.dnd, edgeRatio));
311
- const droppableEmpty = $derived(new DroppableContent(ctx.dnd, edgeRatio));
273
+ const droppableSpacer = $derived(new SimpleTabsDropTarget(ctx, tile.id));
274
+ const droppableContent = $derived(
275
+ new SegmentedContentDropTarget(ctx, edgeRatio)
276
+ );
277
+ const droppableEmpty = $derived(
278
+ new SegmentedContentDropTarget(ctx, edgeRatio)
279
+ );
312
280
  </script>
313
281
 
314
282
  {#snippet defaultTabHeader(t: Tiles['tabs'], index: number)}
@@ -320,9 +288,10 @@
320
288
  <div data-tabs-bar>
321
289
  <div data-tabs-list>
322
290
  {#each tile.children as t, i (t.id)}
323
- {@const droppable = new DroppableTab(ctx.dnd, t.id, i)}
324
- {@const draggable = new DraggableTab(ctx.dnd, {
325
- index: i,
291
+ {@const droppable = new SegmentedTabDropTarget(ctx, t.id, i)}
292
+ {@const draggable = new TileDragSource(ctx, {
293
+ parentTileId: tile.id,
294
+ childIndex: i,
326
295
  data: create({
327
296
  ...tile,
328
297
  tabs: [[tile.titles[i], t]],
@@ -1,7 +1,8 @@
1
1
  import { type Snippet } from 'svelte';
2
+ import type { Draggable } from '../shared/dnd.svelte.js';
2
3
  import type { Registry } from '../shared/registry.js';
3
- import type { Tile, Tiles } from '../model.js';
4
- import type { TilerContext } from '../context.svelte.js';
4
+ import { type Tile, type Tiles } from '../model.js';
5
+ import type { TilerContext } from '../context.js';
5
6
  export type HeadersDirection = Direction | 'none';
6
7
  declare module '../model.js' {
7
8
  interface TileRegistry {
@@ -14,6 +15,9 @@ declare module '../model.js' {
14
15
  empty?: string;
15
16
  };
16
17
  }
18
+ interface TileInsertRequirements {
19
+ tabs: 'titles';
20
+ }
17
21
  }
18
22
  export interface TabsOptions<H extends string, E extends string, A extends string> {
19
23
  tabs: [string, Tile][];
@@ -26,14 +30,14 @@ export interface TabsOptions<H extends string, E extends string, A extends strin
26
30
  }
27
31
  export declare function create<H extends string, E extends string, A extends string>(options: TabsOptions<H, E, A>): Tiles['tabs'];
28
32
  interface SplitOptions {
29
- type: 'row' | 'column';
33
+ type: Direction;
30
34
  parent: Tile | undefined;
31
35
  pivot: Tiles['tabs'];
32
36
  offset: number;
33
37
  adjacent: Tiles['tabs'];
34
38
  }
35
39
  interface TabsContext<H extends string = string, E extends string = string, A extends string = string> {
36
- createSplit?: (options: SplitOptions) => Tile;
40
+ applySplit?: (options: SplitOptions) => void;
37
41
  actions?: Registry<A, Snippet<[Tiles['tabs']]> | undefined>;
38
42
  headers?: Registry<H, Snippet<[Tiles['tabs'], number, Draggable<Tile>]> | undefined>;
39
43
  empty?: Registry<E, Snippet<[Tiles['tabs']]> | undefined>;
@@ -41,13 +45,9 @@ interface TabsContext<H extends string = string, E extends string = string, A ex
41
45
  export declare function setup<H extends string, E extends string, A extends string>(ctx: TabsContext<H, E, A>): (options: TabsOptions<H, E, A>) => Tiles["tabs"];
42
46
  export declare function onRemoveChild(ctx: TilerContext, tile: Tiles['tabs'], i: number): void;
43
47
  export declare function onClear(_ctx: TilerContext, tile: Tiles['tabs']): void;
44
- export declare function insertTabs(tile: Tiles['tabs'], i: number, { titles, children, }: {
45
- titles: string[];
46
- children: Tile[];
47
- }): void;
48
- import { Draggable } from '../shared/dnd.svelte.js';
48
+ export declare function onInsert(_ctx: TilerContext, tile: Tiles['tabs'], i: number, data: TileInsertData<'tabs'>): void;
49
49
  import { type Direction } from '../shared/spatial.js';
50
- import type { TileProps } from '../model.js';
51
- declare const Tabs: import("svelte").Component<TileProps<"tabs">, {}, "tile" | "parent">;
50
+ import type { TileInsertData, TileProps } from '../model.js';
51
+ declare const Tabs: import("svelte").Component<TileProps<"tabs">, {}, "tile">;
52
52
  type Tabs = ReturnType<typeof Tabs>;
53
53
  export default Tabs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-tiler",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "A small, unstyled library for building tiling user interfaces.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/x0k/svelte-tiler#readme",
@@ -61,14 +61,14 @@
61
61
  "eslint": "^9.39.2",
62
62
  "eslint-config-prettier": "^10.1.8",
63
63
  "eslint-plugin-svelte": "^3.14.0",
64
- "globals": "^17.1.0",
64
+ "globals": "^17.2.0",
65
65
  "marked": "^17.0.1",
66
66
  "playwright": "^1.58.0",
67
67
  "prettier": "^3.8.1",
68
68
  "prettier-plugin-svelte": "^3.4.1",
69
69
  "publint": "^0.3.17",
70
70
  "shiki": "^3.21.0",
71
- "svelte": "^5.48.3",
71
+ "svelte": "^5.49.1",
72
72
  "svelte-check": "^4.3.5",
73
73
  "typescript": "^5.9.3",
74
74
  "typescript-eslint": "^8.54.0",
@@ -1,57 +0,0 @@
1
- import { createContext } from 'svelte';
2
- import { DndContext } from "./shared/dnd.svelte.js";
3
- export const [getTilerContext, setTilerContext] = createContext();
4
- export class TilerContext {
5
- definitions;
6
- parents;
7
- effects;
8
- updateRootFn;
9
- dnd;
10
- constructor(options) {
11
- this.definitions = $derived(options.tiles);
12
- this.dnd = $derived(options.dnd ?? new DndContext());
13
- this.parents = $derived(options.parents ?? new WeakMap());
14
- this.effects = $derived(options.effects ?? {});
15
- }
16
- registerParent(tile, parent) {
17
- this.parents.set(tile, parent);
18
- }
19
- setUpdateRoot(tile, update) {
20
- this.parents.delete(tile);
21
- this.updateRootFn = update;
22
- }
23
- getTileEffect(tile) {
24
- return this.effects[tile.type];
25
- }
26
- getTileComponent(tile) {
27
- return this.definitions[tile.type].default;
28
- }
29
- replaceWith(tile, replace) {
30
- const parent = this.parents.get(tile);
31
- if (parent) {
32
- const index = parent.children.findIndex((c) => c.id === tile.id);
33
- if (index < 0) {
34
- throw new Error(`Invalid parent for ${JSON.stringify($state.snapshot(tile))} tile`);
35
- }
36
- parent.children[index] = replace;
37
- }
38
- else {
39
- this.updateRootFn?.(replace);
40
- }
41
- }
42
- removeChild(tile, index) {
43
- return this.definitions[tile.type].onRemoveChild(this, tile, index);
44
- }
45
- destroy(tile) {
46
- const parent = this.parents.get(tile);
47
- if (parent === undefined) {
48
- this.definitions[tile.type].onClear(this, tile);
49
- return;
50
- }
51
- const index = parent.children.findIndex((c) => c.id === tile.id);
52
- if (index < 0) {
53
- throw new Error(`Invalid parent for ${JSON.stringify($state.snapshot(tile))} tile`);
54
- }
55
- this.removeChild(parent, index);
56
- }
57
- }