@framesquared/layout 0.1.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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +496 -0
- package/dist/index.js +962 -0
- package/dist/index.js.map +1 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
// src/Layout.ts
|
|
2
|
+
var Layout = class {
|
|
3
|
+
type;
|
|
4
|
+
owner = null;
|
|
5
|
+
isRunning = false;
|
|
6
|
+
needsLayout = true;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.type = config.type ?? "auto";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sets the Container that owns this layout.
|
|
12
|
+
*/
|
|
13
|
+
setOwner(owner) {
|
|
14
|
+
this.owner = owner;
|
|
15
|
+
}
|
|
16
|
+
// -----------------------------------------------------------------------
|
|
17
|
+
// Lifecycle
|
|
18
|
+
// -----------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Called at the start of a layout run. Sets isRunning flag.
|
|
21
|
+
* Override to gather initial measurements.
|
|
22
|
+
*/
|
|
23
|
+
beginLayout(_context) {
|
|
24
|
+
this.isRunning = true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Main calculation phase. Override in subclasses to compute
|
|
28
|
+
* child sizes and positions.
|
|
29
|
+
*/
|
|
30
|
+
calculate(_context) {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Called after calculation is complete. Clears flags.
|
|
34
|
+
* Override to finalize DOM writes.
|
|
35
|
+
*/
|
|
36
|
+
completeLayout(_context) {
|
|
37
|
+
this.isRunning = false;
|
|
38
|
+
this.needsLayout = false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Called after the entire layout pass is done.
|
|
42
|
+
* Override for post-layout work (e.g., firing events).
|
|
43
|
+
*/
|
|
44
|
+
afterLayout() {
|
|
45
|
+
}
|
|
46
|
+
// -----------------------------------------------------------------------
|
|
47
|
+
// Size policy
|
|
48
|
+
// -----------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Returns the size policy for a child item.
|
|
51
|
+
* 'configured' = the item has an explicit width/height config.
|
|
52
|
+
* 'natural' = the item uses its natural content size.
|
|
53
|
+
* 'shrinkWrap' = the item wraps its content.
|
|
54
|
+
*/
|
|
55
|
+
getItemSizePolicy(item) {
|
|
56
|
+
const cfg = item._config ?? {};
|
|
57
|
+
return {
|
|
58
|
+
width: cfg.width !== void 0 ? "configured" : "natural",
|
|
59
|
+
height: cfg.height !== void 0 ? "configured" : "natural"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
// Rendering
|
|
64
|
+
// -----------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Renders child components into the target element.
|
|
67
|
+
* Base implementation renders each item sequentially.
|
|
68
|
+
*/
|
|
69
|
+
renderItems(items, target) {
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (!item.rendered) {
|
|
72
|
+
item.render(target);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/AutoLayout.ts
|
|
79
|
+
var AutoLayout = class extends Layout {
|
|
80
|
+
constructor(config = {}) {
|
|
81
|
+
super({ ...config, type: "auto" });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Auto layout: no calculation needed — items use natural CSS flow.
|
|
85
|
+
*/
|
|
86
|
+
calculate(_context) {
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* All items use natural sizing in auto layout.
|
|
90
|
+
*/
|
|
91
|
+
getItemSizePolicy(_item) {
|
|
92
|
+
return { width: "natural", height: "natural" };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/LayoutContext.ts
|
|
97
|
+
var LayoutContext = class {
|
|
98
|
+
/** Generic property cache (layout-level state). */
|
|
99
|
+
props = /* @__PURE__ */ new Map();
|
|
100
|
+
/** Per-element DOM measurement cache: element → (propName → value). */
|
|
101
|
+
domCache = /* @__PURE__ */ new WeakMap();
|
|
102
|
+
// -----------------------------------------------------------------------
|
|
103
|
+
// Generic props
|
|
104
|
+
// -----------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* Gets a layout-level property value.
|
|
107
|
+
*/
|
|
108
|
+
getProp(name) {
|
|
109
|
+
return this.props.get(name);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sets a layout-level property value.
|
|
113
|
+
*/
|
|
114
|
+
setProp(name, value) {
|
|
115
|
+
this.props.set(name, value);
|
|
116
|
+
}
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// DOM measurement caching
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Gets a cached DOM measurement, or reads it via `readFn` and caches the result.
|
|
122
|
+
* This prevents repeated layout-triggering reads on the same element.
|
|
123
|
+
*/
|
|
124
|
+
getDomProp(name, element, readFn) {
|
|
125
|
+
let cache = this.domCache.get(element);
|
|
126
|
+
if (!cache) {
|
|
127
|
+
cache = /* @__PURE__ */ new Map();
|
|
128
|
+
this.domCache.set(element, cache);
|
|
129
|
+
}
|
|
130
|
+
if (cache.has(name)) {
|
|
131
|
+
return cache.get(name);
|
|
132
|
+
}
|
|
133
|
+
const value = readFn();
|
|
134
|
+
cache.set(name, value);
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns true if a DOM measurement has been cached for the given element+prop.
|
|
139
|
+
*/
|
|
140
|
+
hasDomProp(name, element) {
|
|
141
|
+
const cache = this.domCache.get(element);
|
|
142
|
+
return cache?.has(name) ?? false;
|
|
143
|
+
}
|
|
144
|
+
// -----------------------------------------------------------------------
|
|
145
|
+
// Flush
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
/**
|
|
148
|
+
* Clears all cached data. Called at the start of a new layout run.
|
|
149
|
+
*/
|
|
150
|
+
flush() {
|
|
151
|
+
this.props.clear();
|
|
152
|
+
this.domCache = /* @__PURE__ */ new WeakMap();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/LayoutRunner.ts
|
|
157
|
+
var MAX_ITERATIONS = 10;
|
|
158
|
+
var LayoutRunner = class _LayoutRunner {
|
|
159
|
+
static instance = null;
|
|
160
|
+
/** Set of components that need a layout pass. */
|
|
161
|
+
pending = /* @__PURE__ */ new Set();
|
|
162
|
+
/** Whether a run is currently scheduled. */
|
|
163
|
+
scheduled = false;
|
|
164
|
+
/** Map of element → { observer, component } for ResizeObserver tracking. */
|
|
165
|
+
observed = /* @__PURE__ */ new Map();
|
|
166
|
+
/** Component → ResizeObserver for cleanup. */
|
|
167
|
+
componentObservers = /* @__PURE__ */ new Map();
|
|
168
|
+
constructor() {
|
|
169
|
+
}
|
|
170
|
+
static getInstance() {
|
|
171
|
+
if (!_LayoutRunner.instance) {
|
|
172
|
+
_LayoutRunner.instance = new _LayoutRunner();
|
|
173
|
+
}
|
|
174
|
+
return _LayoutRunner.instance;
|
|
175
|
+
}
|
|
176
|
+
// -----------------------------------------------------------------------
|
|
177
|
+
// Public API
|
|
178
|
+
// -----------------------------------------------------------------------
|
|
179
|
+
/**
|
|
180
|
+
* Marks a component for re-layout.
|
|
181
|
+
*/
|
|
182
|
+
invalidate(component) {
|
|
183
|
+
this.pending.add(component);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Returns true if the component is in the pending queue.
|
|
187
|
+
*/
|
|
188
|
+
isPending(component) {
|
|
189
|
+
return this.pending.has(component);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Performs a complete layout pass on all pending components.
|
|
193
|
+
* Uses iterative processing with cycle detection.
|
|
194
|
+
*/
|
|
195
|
+
run() {
|
|
196
|
+
let iteration = 0;
|
|
197
|
+
while (this.pending.size > 0 && iteration < MAX_ITERATIONS) {
|
|
198
|
+
iteration++;
|
|
199
|
+
const batch = [...this.pending];
|
|
200
|
+
this.pending.clear();
|
|
201
|
+
const ctx = new LayoutContext();
|
|
202
|
+
for (const component of batch) {
|
|
203
|
+
const layout = this.getLayout(component);
|
|
204
|
+
if (!layout || !layout.needsLayout) continue;
|
|
205
|
+
layout.beginLayout(ctx);
|
|
206
|
+
layout.calculate(ctx);
|
|
207
|
+
layout.completeLayout(ctx);
|
|
208
|
+
layout.afterLayout();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
this.pending.clear();
|
|
212
|
+
this.scheduled = false;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Schedules a layout run on the next animation frame.
|
|
216
|
+
* Multiple calls before the frame coalesce into a single run.
|
|
217
|
+
*/
|
|
218
|
+
scheduleRun() {
|
|
219
|
+
if (this.scheduled) return;
|
|
220
|
+
this.scheduled = true;
|
|
221
|
+
if (typeof requestAnimationFrame !== "undefined") {
|
|
222
|
+
requestAnimationFrame(() => this.run());
|
|
223
|
+
} else {
|
|
224
|
+
Promise.resolve().then(() => this.run());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Clears all pending items without running them.
|
|
229
|
+
*/
|
|
230
|
+
clear() {
|
|
231
|
+
this.pending.clear();
|
|
232
|
+
this.scheduled = false;
|
|
233
|
+
}
|
|
234
|
+
// -----------------------------------------------------------------------
|
|
235
|
+
// ResizeObserver integration
|
|
236
|
+
// -----------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* Starts watching an element for size changes.
|
|
239
|
+
* When the element resizes, the associated component is invalidated.
|
|
240
|
+
*/
|
|
241
|
+
observe(component, element) {
|
|
242
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
243
|
+
const existing = this.componentObservers.get(component);
|
|
244
|
+
if (existing && existing.element === element) return;
|
|
245
|
+
const observer = new ResizeObserver((_entries) => {
|
|
246
|
+
this.invalidate(component);
|
|
247
|
+
});
|
|
248
|
+
observer.observe(element);
|
|
249
|
+
this.observed.set(element, { observer, component });
|
|
250
|
+
this.componentObservers.set(component, { observer, element });
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Stops watching an element for size changes.
|
|
254
|
+
*/
|
|
255
|
+
unobserve(component, element) {
|
|
256
|
+
const entry = this.observed.get(element);
|
|
257
|
+
if (entry && entry.component === component) {
|
|
258
|
+
entry.observer.unobserve(element);
|
|
259
|
+
entry.observer.disconnect();
|
|
260
|
+
this.observed.delete(element);
|
|
261
|
+
this.componentObservers.delete(component);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// -----------------------------------------------------------------------
|
|
265
|
+
// Internal
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
getLayout(component) {
|
|
268
|
+
const layout = component._layoutInstance;
|
|
269
|
+
if (layout instanceof Layout) return layout;
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/box/BoxLayout.ts
|
|
275
|
+
var ALIGN_MAP = {
|
|
276
|
+
start: "flex-start",
|
|
277
|
+
center: "center",
|
|
278
|
+
end: "flex-end",
|
|
279
|
+
stretch: "stretch",
|
|
280
|
+
stretchmax: "stretch"
|
|
281
|
+
};
|
|
282
|
+
var PACK_MAP = {
|
|
283
|
+
start: "flex-start",
|
|
284
|
+
center: "center",
|
|
285
|
+
end: "flex-end",
|
|
286
|
+
"space-between": "space-between",
|
|
287
|
+
"space-around": "space-around",
|
|
288
|
+
"space-evenly": "space-evenly"
|
|
289
|
+
};
|
|
290
|
+
var BoxLayout = class extends Layout {
|
|
291
|
+
align;
|
|
292
|
+
pack;
|
|
293
|
+
gap;
|
|
294
|
+
overflow;
|
|
295
|
+
reverse;
|
|
296
|
+
constructor(config = {}) {
|
|
297
|
+
super(config);
|
|
298
|
+
this.align = config.align ?? "stretch";
|
|
299
|
+
this.pack = config.pack ?? "start";
|
|
300
|
+
this.gap = config.gap ?? 0;
|
|
301
|
+
this.overflow = config.overflow ?? "visible";
|
|
302
|
+
this.reverse = config.reverse ?? false;
|
|
303
|
+
}
|
|
304
|
+
// -----------------------------------------------------------------------
|
|
305
|
+
// Container configuration
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
/**
|
|
308
|
+
* Applies CSS Flexbox properties to the container element.
|
|
309
|
+
*/
|
|
310
|
+
configureContainer(el) {
|
|
311
|
+
el.style.display = "flex";
|
|
312
|
+
const dir = this.getDirection();
|
|
313
|
+
el.style.flexDirection = this.reverse ? `${dir}-reverse` : dir;
|
|
314
|
+
el.style.alignItems = ALIGN_MAP[this.align];
|
|
315
|
+
el.style.justifyContent = PACK_MAP[this.pack];
|
|
316
|
+
if (this.gap > 0) {
|
|
317
|
+
el.style.gap = `${this.gap}px`;
|
|
318
|
+
}
|
|
319
|
+
switch (this.overflow) {
|
|
320
|
+
case "wrap":
|
|
321
|
+
el.style.flexWrap = "wrap";
|
|
322
|
+
break;
|
|
323
|
+
case "hidden":
|
|
324
|
+
el.style.overflow = "hidden";
|
|
325
|
+
break;
|
|
326
|
+
case "scroll":
|
|
327
|
+
el.style.overflow = "auto";
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// -----------------------------------------------------------------------
|
|
332
|
+
// Item styles
|
|
333
|
+
// -----------------------------------------------------------------------
|
|
334
|
+
/**
|
|
335
|
+
* Applies flex CSS properties to each child item based on its config.
|
|
336
|
+
*/
|
|
337
|
+
applyItemStyles(items, _target) {
|
|
338
|
+
const primaryProp = this.getPrimaryAxisProp();
|
|
339
|
+
for (const item of items) {
|
|
340
|
+
const el = item.el;
|
|
341
|
+
if (!el) continue;
|
|
342
|
+
const cfg = item._config ?? {};
|
|
343
|
+
const flex = cfg.flex;
|
|
344
|
+
if (flex !== void 0 && flex > 0) {
|
|
345
|
+
el.style.flexGrow = String(flex);
|
|
346
|
+
el.style.flexShrink = "1";
|
|
347
|
+
el.style.flexBasis = "0px";
|
|
348
|
+
} else if (cfg[primaryProp] !== void 0) {
|
|
349
|
+
el.style.flexShrink = "0";
|
|
350
|
+
}
|
|
351
|
+
if (cfg.minWidth !== void 0) el.style.minWidth = `${cfg.minWidth}px`;
|
|
352
|
+
if (cfg.maxWidth !== void 0) el.style.maxWidth = `${cfg.maxWidth}px`;
|
|
353
|
+
if (cfg.minHeight !== void 0) el.style.minHeight = `${cfg.minHeight}px`;
|
|
354
|
+
if (cfg.maxHeight !== void 0) el.style.maxHeight = `${cfg.maxHeight}px`;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// -----------------------------------------------------------------------
|
|
358
|
+
// Layout lifecycle overrides
|
|
359
|
+
// -----------------------------------------------------------------------
|
|
360
|
+
renderItems(items, target) {
|
|
361
|
+
this.configureContainer(target);
|
|
362
|
+
super.renderItems(items, target);
|
|
363
|
+
this.applyItemStyles(items, target);
|
|
364
|
+
}
|
|
365
|
+
calculate(_context) {
|
|
366
|
+
}
|
|
367
|
+
// -----------------------------------------------------------------------
|
|
368
|
+
// Size policy
|
|
369
|
+
// -----------------------------------------------------------------------
|
|
370
|
+
getItemSizePolicy(item) {
|
|
371
|
+
const cfg = item._config ?? {};
|
|
372
|
+
const primaryProp = this.getPrimaryAxisProp();
|
|
373
|
+
const primaryPolicy = cfg.flex !== void 0 ? "shrinkWrap" : cfg[primaryProp] !== void 0 ? "configured" : "natural";
|
|
374
|
+
const crossProp = primaryProp === "width" ? "height" : "width";
|
|
375
|
+
const crossPolicy = cfg[crossProp] !== void 0 ? "configured" : "natural";
|
|
376
|
+
if (primaryProp === "width") {
|
|
377
|
+
return { width: primaryPolicy, height: crossPolicy };
|
|
378
|
+
}
|
|
379
|
+
return { width: crossPolicy, height: primaryPolicy };
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/box/HBoxLayout.ts
|
|
384
|
+
var HBoxLayout = class extends BoxLayout {
|
|
385
|
+
constructor(config = {}) {
|
|
386
|
+
super({ ...config, type: "hbox" });
|
|
387
|
+
}
|
|
388
|
+
getDirection() {
|
|
389
|
+
return "row";
|
|
390
|
+
}
|
|
391
|
+
getPrimaryAxisProp() {
|
|
392
|
+
return "width";
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/box/VBoxLayout.ts
|
|
397
|
+
var VBoxLayout = class extends BoxLayout {
|
|
398
|
+
constructor(config = {}) {
|
|
399
|
+
super({ ...config, type: "vbox" });
|
|
400
|
+
}
|
|
401
|
+
getDirection() {
|
|
402
|
+
return "column";
|
|
403
|
+
}
|
|
404
|
+
getPrimaryAxisProp() {
|
|
405
|
+
return "height";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/FitLayout.ts
|
|
410
|
+
var FitLayout = class extends Layout {
|
|
411
|
+
constructor(config = {}) {
|
|
412
|
+
super({ ...config, type: "fit" });
|
|
413
|
+
}
|
|
414
|
+
configureContainer(el) {
|
|
415
|
+
el.style.position = "relative";
|
|
416
|
+
el.style.overflow = "hidden";
|
|
417
|
+
}
|
|
418
|
+
applyItemStyles(items, _target) {
|
|
419
|
+
for (let i = 0; i < items.length; i++) {
|
|
420
|
+
const el = items[i].el;
|
|
421
|
+
if (!el) continue;
|
|
422
|
+
if (i === 0) {
|
|
423
|
+
el.style.width = "100%";
|
|
424
|
+
el.style.height = "100%";
|
|
425
|
+
el.style.boxSizing = "border-box";
|
|
426
|
+
} else {
|
|
427
|
+
el.style.display = "none";
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
renderItems(items, target) {
|
|
432
|
+
this.configureContainer(target);
|
|
433
|
+
super.renderItems(items, target);
|
|
434
|
+
this.applyItemStyles(items, target);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/CardLayout.ts
|
|
439
|
+
var CardLayout = class extends Layout {
|
|
440
|
+
activeIndex = 0;
|
|
441
|
+
listeners = /* @__PURE__ */ new Map();
|
|
442
|
+
constructor(config = {}) {
|
|
443
|
+
super({ ...config, type: "card" });
|
|
444
|
+
this.activeIndex = config.activeItem ?? 0;
|
|
445
|
+
}
|
|
446
|
+
/** Subscribe to events. */
|
|
447
|
+
on(event, fn) {
|
|
448
|
+
let set = this.listeners.get(event);
|
|
449
|
+
if (!set) {
|
|
450
|
+
set = /* @__PURE__ */ new Set();
|
|
451
|
+
this.listeners.set(event, set);
|
|
452
|
+
}
|
|
453
|
+
set.add(fn);
|
|
454
|
+
}
|
|
455
|
+
/** Unsubscribe. */
|
|
456
|
+
un(event, fn) {
|
|
457
|
+
this.listeners.get(event)?.delete(fn);
|
|
458
|
+
}
|
|
459
|
+
fire(name, ...args) {
|
|
460
|
+
const fns = this.listeners.get(name);
|
|
461
|
+
if (fns) for (const fn of fns) fn(...args);
|
|
462
|
+
}
|
|
463
|
+
configureContainer(el) {
|
|
464
|
+
el.style.position = "relative";
|
|
465
|
+
el.style.overflow = "hidden";
|
|
466
|
+
}
|
|
467
|
+
applyItemStyles(items, _target) {
|
|
468
|
+
for (let i = 0; i < items.length; i++) {
|
|
469
|
+
const el = items[i].el;
|
|
470
|
+
if (!el) continue;
|
|
471
|
+
el.style.width = "100%";
|
|
472
|
+
el.style.height = "100%";
|
|
473
|
+
el.style.boxSizing = "border-box";
|
|
474
|
+
el.style.display = i === this.activeIndex ? "" : "none";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
renderItems(items, target) {
|
|
478
|
+
this.configureContainer(target);
|
|
479
|
+
super.renderItems(items, target);
|
|
480
|
+
this.applyItemStyles(items, target);
|
|
481
|
+
}
|
|
482
|
+
// -- Navigation --
|
|
483
|
+
getActiveItem() {
|
|
484
|
+
return this.getOwnerItems()[this.activeIndex];
|
|
485
|
+
}
|
|
486
|
+
setActiveItem(item) {
|
|
487
|
+
const items = this.getOwnerItems();
|
|
488
|
+
const newIndex = typeof item === "number" ? item : items.indexOf(item);
|
|
489
|
+
if (newIndex < 0 || newIndex >= items.length || newIndex === this.activeIndex) return;
|
|
490
|
+
const oldItem = items[this.activeIndex];
|
|
491
|
+
const newItem = items[newIndex];
|
|
492
|
+
this.fire("deactivate", oldItem, this);
|
|
493
|
+
if (oldItem.el) oldItem.el.style.display = "none";
|
|
494
|
+
this.activeIndex = newIndex;
|
|
495
|
+
if (newItem.el) newItem.el.style.display = "";
|
|
496
|
+
this.fire("activate", newItem, this);
|
|
497
|
+
}
|
|
498
|
+
next() {
|
|
499
|
+
const items = this.getOwnerItems();
|
|
500
|
+
const next = (this.activeIndex + 1) % items.length;
|
|
501
|
+
this.setActiveItem(next);
|
|
502
|
+
return items[next];
|
|
503
|
+
}
|
|
504
|
+
prev() {
|
|
505
|
+
const items = this.getOwnerItems();
|
|
506
|
+
const prev = (this.activeIndex - 1 + items.length) % items.length;
|
|
507
|
+
this.setActiveItem(prev);
|
|
508
|
+
return items[prev];
|
|
509
|
+
}
|
|
510
|
+
getOwnerItems() {
|
|
511
|
+
return this.owner?.getItems?.() ?? [];
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/AnchorLayout.ts
|
|
516
|
+
var AnchorLayout = class extends Layout {
|
|
517
|
+
constructor(config = {}) {
|
|
518
|
+
super({ ...config, type: "anchor" });
|
|
519
|
+
}
|
|
520
|
+
configureContainer(el) {
|
|
521
|
+
el.style.position = "relative";
|
|
522
|
+
}
|
|
523
|
+
applyItemStyles(items, _target) {
|
|
524
|
+
for (const item of items) {
|
|
525
|
+
const el = item.el;
|
|
526
|
+
if (!el) continue;
|
|
527
|
+
const anchor = item._config?.anchor;
|
|
528
|
+
if (!anchor) continue;
|
|
529
|
+
const parts = anchor.trim().split(/\s+/);
|
|
530
|
+
const w = parts[0];
|
|
531
|
+
const h = parts[1];
|
|
532
|
+
if (w) el.style.width = parseAnchorValue(w);
|
|
533
|
+
if (h) el.style.height = parseAnchorValue(h);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
renderItems(items, target) {
|
|
537
|
+
this.configureContainer(target);
|
|
538
|
+
super.renderItems(items, target);
|
|
539
|
+
this.applyItemStyles(items, target);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function parseAnchorValue(val) {
|
|
543
|
+
if (val.endsWith("%")) return val;
|
|
544
|
+
const n = parseInt(val, 10);
|
|
545
|
+
if (!isNaN(n) && n < 0) return `calc(100% - ${Math.abs(n)}px)`;
|
|
546
|
+
if (!isNaN(n) && n > 0) return `${n}px`;
|
|
547
|
+
return val;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/BorderLayout.ts
|
|
551
|
+
var BorderLayout = class extends Layout {
|
|
552
|
+
constructor(config = {}) {
|
|
553
|
+
super({ ...config, type: "border" });
|
|
554
|
+
}
|
|
555
|
+
configureContainer(el) {
|
|
556
|
+
el.style.display = "grid";
|
|
557
|
+
el.style.width = "100%";
|
|
558
|
+
el.style.height = "100%";
|
|
559
|
+
}
|
|
560
|
+
applyItemStyles(items, target) {
|
|
561
|
+
const regions = /* @__PURE__ */ new Map();
|
|
562
|
+
for (const item of items) {
|
|
563
|
+
const region = item._config?.region;
|
|
564
|
+
if (region) regions.set(region, item);
|
|
565
|
+
}
|
|
566
|
+
const rows = [];
|
|
567
|
+
const areas = [];
|
|
568
|
+
const hasNorth = regions.has("north");
|
|
569
|
+
const hasSouth = regions.has("south");
|
|
570
|
+
const hasWest = regions.has("west");
|
|
571
|
+
const hasEast = regions.has("east");
|
|
572
|
+
const colSizes = [];
|
|
573
|
+
if (hasWest) {
|
|
574
|
+
const w = regions.get("west")?._config?.width;
|
|
575
|
+
colSizes.push(w ? `${w}px` : "200px");
|
|
576
|
+
}
|
|
577
|
+
colSizes.push("1fr");
|
|
578
|
+
if (hasEast) {
|
|
579
|
+
const w = regions.get("east")?._config?.width;
|
|
580
|
+
colSizes.push(w ? `${w}px` : "200px");
|
|
581
|
+
}
|
|
582
|
+
const cols = colSizes.length;
|
|
583
|
+
if (hasNorth) {
|
|
584
|
+
const h = regions.get("north")?._config?.height;
|
|
585
|
+
rows.push(h ? `${h}px` : "auto");
|
|
586
|
+
areas.push(Array(cols).fill("north"));
|
|
587
|
+
}
|
|
588
|
+
rows.push("1fr");
|
|
589
|
+
const middleRow = [];
|
|
590
|
+
if (hasWest) middleRow.push("west");
|
|
591
|
+
middleRow.push("center");
|
|
592
|
+
if (hasEast) middleRow.push("east");
|
|
593
|
+
areas.push(middleRow);
|
|
594
|
+
if (hasSouth) {
|
|
595
|
+
const h = regions.get("south")?._config?.height;
|
|
596
|
+
rows.push(h ? `${h}px` : "auto");
|
|
597
|
+
areas.push(Array(cols).fill("south"));
|
|
598
|
+
}
|
|
599
|
+
const el = target;
|
|
600
|
+
el.style.gridTemplateRows = rows.join(" ");
|
|
601
|
+
el.style.gridTemplateColumns = colSizes.join(" ");
|
|
602
|
+
el.style.gridTemplateAreas = areas.map((r) => `"${r.join(" ")}"`).join(" ");
|
|
603
|
+
for (const item of items) {
|
|
604
|
+
const itemEl = item.el;
|
|
605
|
+
if (!itemEl) continue;
|
|
606
|
+
const region = item._config?.region;
|
|
607
|
+
if (region) {
|
|
608
|
+
itemEl.style.gridArea = region;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
renderItems(items, target) {
|
|
613
|
+
this.configureContainer(target);
|
|
614
|
+
super.renderItems(items, target);
|
|
615
|
+
this.applyItemStyles(items, target);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/ColumnLayout.ts
|
|
620
|
+
var ColumnLayout = class extends Layout {
|
|
621
|
+
constructor(config = {}) {
|
|
622
|
+
super({ ...config, type: "column" });
|
|
623
|
+
}
|
|
624
|
+
configureContainer(el) {
|
|
625
|
+
el.style.display = "flex";
|
|
626
|
+
el.style.flexWrap = "wrap";
|
|
627
|
+
el.style.alignItems = "flex-start";
|
|
628
|
+
}
|
|
629
|
+
applyItemStyles(items, _target) {
|
|
630
|
+
for (const item of items) {
|
|
631
|
+
const el = item.el;
|
|
632
|
+
if (!el) continue;
|
|
633
|
+
const cfg = item._config ?? {};
|
|
634
|
+
const cw = cfg.columnWidth;
|
|
635
|
+
if (cw !== void 0 && cw > 0 && cw <= 1) {
|
|
636
|
+
el.style.width = `${cw * 100}%`;
|
|
637
|
+
el.style.boxSizing = "border-box";
|
|
638
|
+
} else if (cfg.width !== void 0) {
|
|
639
|
+
el.style.flexShrink = "0";
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
renderItems(items, target) {
|
|
644
|
+
this.configureContainer(target);
|
|
645
|
+
super.renderItems(items, target);
|
|
646
|
+
this.applyItemStyles(items, target);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/TableLayout.ts
|
|
651
|
+
var TableLayout = class extends Layout {
|
|
652
|
+
columns;
|
|
653
|
+
constructor(config = {}) {
|
|
654
|
+
super({ ...config, type: "table" });
|
|
655
|
+
this.columns = config.columns ?? 1;
|
|
656
|
+
}
|
|
657
|
+
configureContainer(el) {
|
|
658
|
+
el.style.display = "grid";
|
|
659
|
+
el.style.gridTemplateColumns = `repeat(${this.columns}, 1fr)`;
|
|
660
|
+
}
|
|
661
|
+
applyItemStyles(items, _target) {
|
|
662
|
+
for (const item of items) {
|
|
663
|
+
const el = item.el;
|
|
664
|
+
if (!el) continue;
|
|
665
|
+
const cfg = item._config ?? {};
|
|
666
|
+
if (cfg.colspan && cfg.colspan > 1) {
|
|
667
|
+
el.style.gridColumn = `span ${cfg.colspan}`;
|
|
668
|
+
}
|
|
669
|
+
if (cfg.rowspan && cfg.rowspan > 1) {
|
|
670
|
+
el.style.gridRow = `span ${cfg.rowspan}`;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
renderItems(items, target) {
|
|
675
|
+
this.configureContainer(target);
|
|
676
|
+
super.renderItems(items, target);
|
|
677
|
+
this.applyItemStyles(items, target);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/AbsoluteLayout.ts
|
|
682
|
+
var AbsoluteLayout = class extends Layout {
|
|
683
|
+
constructor(config = {}) {
|
|
684
|
+
super({ ...config, type: "absolute" });
|
|
685
|
+
}
|
|
686
|
+
configureContainer(el) {
|
|
687
|
+
el.style.position = "relative";
|
|
688
|
+
}
|
|
689
|
+
applyItemStyles(items, _target) {
|
|
690
|
+
for (const item of items) {
|
|
691
|
+
const el = item.el;
|
|
692
|
+
if (!el) continue;
|
|
693
|
+
const cfg = item._config ?? {};
|
|
694
|
+
el.style.position = "absolute";
|
|
695
|
+
el.style.left = `${cfg.x ?? 0}px`;
|
|
696
|
+
el.style.top = `${cfg.y ?? 0}px`;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
renderItems(items, target) {
|
|
700
|
+
this.configureContainer(target);
|
|
701
|
+
super.renderItems(items, target);
|
|
702
|
+
this.applyItemStyles(items, target);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// src/AccordionLayout.ts
|
|
707
|
+
var AccordionLayout = class extends Layout {
|
|
708
|
+
multi;
|
|
709
|
+
fill;
|
|
710
|
+
expandedSet = /* @__PURE__ */ new WeakSet();
|
|
711
|
+
constructor(config = {}) {
|
|
712
|
+
super({ ...config, type: "accordion" });
|
|
713
|
+
this.multi = config.multi ?? false;
|
|
714
|
+
this.fill = config.fill ?? true;
|
|
715
|
+
}
|
|
716
|
+
configureContainer(el) {
|
|
717
|
+
el.style.display = "flex";
|
|
718
|
+
el.style.flexDirection = "column";
|
|
719
|
+
el.style.overflow = "hidden";
|
|
720
|
+
}
|
|
721
|
+
applyItemStyles(items, _target) {
|
|
722
|
+
if (items.length > 0 && !items.some((i) => this.expandedSet.has(i))) {
|
|
723
|
+
this.expandedSet.add(items[0]);
|
|
724
|
+
}
|
|
725
|
+
for (const item of items) {
|
|
726
|
+
this.applyExpansionState(item);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
renderItems(items, target) {
|
|
730
|
+
this.configureContainer(target);
|
|
731
|
+
super.renderItems(items, target);
|
|
732
|
+
this.applyItemStyles(items, target);
|
|
733
|
+
}
|
|
734
|
+
// -- Public API --
|
|
735
|
+
isExpanded(item) {
|
|
736
|
+
return this.expandedSet.has(item);
|
|
737
|
+
}
|
|
738
|
+
expand(item) {
|
|
739
|
+
if (!this.multi) {
|
|
740
|
+
for (const other of this.getOwnerItems()) {
|
|
741
|
+
if (other !== item) {
|
|
742
|
+
this.expandedSet.delete(other);
|
|
743
|
+
this.applyExpansionState(other);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.expandedSet.add(item);
|
|
748
|
+
this.applyExpansionState(item);
|
|
749
|
+
}
|
|
750
|
+
collapse(item) {
|
|
751
|
+
this.expandedSet.delete(item);
|
|
752
|
+
this.applyExpansionState(item);
|
|
753
|
+
}
|
|
754
|
+
toggle(item) {
|
|
755
|
+
if (this.isExpanded(item)) {
|
|
756
|
+
this.collapse(item);
|
|
757
|
+
} else {
|
|
758
|
+
this.expand(item);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// -- Internal --
|
|
762
|
+
applyExpansionState(item) {
|
|
763
|
+
const el = item.el;
|
|
764
|
+
if (!el) return;
|
|
765
|
+
const expanded = this.expandedSet.has(item);
|
|
766
|
+
if (expanded) {
|
|
767
|
+
el.style.flexGrow = this.fill ? "1" : "0";
|
|
768
|
+
el.style.flexShrink = "0";
|
|
769
|
+
el.style.overflow = "";
|
|
770
|
+
el.classList.remove("x-accordion-collapsed");
|
|
771
|
+
el.classList.add("x-accordion-expanded");
|
|
772
|
+
} else {
|
|
773
|
+
el.style.flexGrow = "0";
|
|
774
|
+
el.style.flexShrink = "0";
|
|
775
|
+
el.style.overflow = "hidden";
|
|
776
|
+
el.classList.remove("x-accordion-expanded");
|
|
777
|
+
el.classList.add("x-accordion-collapsed");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
getOwnerItems() {
|
|
781
|
+
return this.owner?.getItems?.() ?? [];
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/ResponsivePlugin.ts
|
|
786
|
+
function toMediaQuery(expr) {
|
|
787
|
+
if (expr.startsWith("(") || expr.startsWith("@media")) return expr;
|
|
788
|
+
const parts = expr.split("&&").map((s) => s.trim());
|
|
789
|
+
const conditions = [];
|
|
790
|
+
for (const part of parts) {
|
|
791
|
+
const m = part.match(/^width\s*(>=?|<=?|==)\s*(\d+)$/);
|
|
792
|
+
if (!m) {
|
|
793
|
+
conditions.push(`(${part})`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const op = m[1];
|
|
797
|
+
const val = parseInt(m[2], 10);
|
|
798
|
+
switch (op) {
|
|
799
|
+
case "<":
|
|
800
|
+
conditions.push(`(max-width: ${val - 1}px)`);
|
|
801
|
+
break;
|
|
802
|
+
case "<=":
|
|
803
|
+
conditions.push(`(max-width: ${val}px)`);
|
|
804
|
+
break;
|
|
805
|
+
case ">":
|
|
806
|
+
conditions.push(`(min-width: ${val + 1}px)`);
|
|
807
|
+
break;
|
|
808
|
+
case ">=":
|
|
809
|
+
conditions.push(`(min-width: ${val}px)`);
|
|
810
|
+
break;
|
|
811
|
+
case "==":
|
|
812
|
+
conditions.push(`(width: ${val}px)`);
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return conditions.join(" and ");
|
|
817
|
+
}
|
|
818
|
+
function applyConfig(owner, config) {
|
|
819
|
+
for (const [key, value] of Object.entries(config)) {
|
|
820
|
+
switch (key) {
|
|
821
|
+
case "cls":
|
|
822
|
+
if (typeof value === "string") owner.addCls(value);
|
|
823
|
+
break;
|
|
824
|
+
case "hidden":
|
|
825
|
+
if (value) owner.hide();
|
|
826
|
+
else owner.show();
|
|
827
|
+
break;
|
|
828
|
+
case "disabled":
|
|
829
|
+
if (value) owner.disable();
|
|
830
|
+
else owner.enable();
|
|
831
|
+
break;
|
|
832
|
+
case "width":
|
|
833
|
+
if (typeof value === "number" || typeof value === "string") owner.setWidth(value);
|
|
834
|
+
break;
|
|
835
|
+
case "height":
|
|
836
|
+
if (typeof value === "number" || typeof value === "string") owner.setHeight(value);
|
|
837
|
+
break;
|
|
838
|
+
default:
|
|
839
|
+
owner._config[key] = value;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function removeClsConfig(owner, config) {
|
|
845
|
+
if (config.cls && typeof config.cls === "string") {
|
|
846
|
+
owner.removeCls(config.cls);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
var ResponsivePlugin = class {
|
|
850
|
+
config;
|
|
851
|
+
owner = null;
|
|
852
|
+
entries = [];
|
|
853
|
+
activeConfigs = /* @__PURE__ */ new Set();
|
|
854
|
+
constructor(config) {
|
|
855
|
+
this.config = config;
|
|
856
|
+
}
|
|
857
|
+
getOwner() {
|
|
858
|
+
return this.owner;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Initialise the plugin with its owner component.
|
|
862
|
+
* Sets up matchMedia listeners and applies initial state.
|
|
863
|
+
*/
|
|
864
|
+
init(owner) {
|
|
865
|
+
this.owner = owner;
|
|
866
|
+
for (const [expr, cfg] of Object.entries(this.config.responsiveConfig)) {
|
|
867
|
+
const query = toMediaQuery(expr);
|
|
868
|
+
const mql = window.matchMedia(query);
|
|
869
|
+
const handler = (e) => {
|
|
870
|
+
if (e.matches) {
|
|
871
|
+
applyConfig(owner, cfg);
|
|
872
|
+
this.activeConfigs.add(cfg);
|
|
873
|
+
} else {
|
|
874
|
+
removeClsConfig(owner, cfg);
|
|
875
|
+
this.activeConfigs.delete(cfg);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
this.entries.push({ query, mql, config: cfg, handler });
|
|
879
|
+
mql.addEventListener("change", handler);
|
|
880
|
+
if (mql.matches) {
|
|
881
|
+
applyConfig(owner, cfg);
|
|
882
|
+
this.activeConfigs.add(cfg);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Clean up all media query listeners.
|
|
888
|
+
*/
|
|
889
|
+
destroy() {
|
|
890
|
+
for (const entry of this.entries) {
|
|
891
|
+
entry.mql.removeEventListener("change", entry.handler);
|
|
892
|
+
}
|
|
893
|
+
this.entries.length = 0;
|
|
894
|
+
this.activeConfigs.clear();
|
|
895
|
+
this.owner = null;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/ResponsiveColumnLayout.ts
|
|
900
|
+
var ResponsiveColumnLayout = class extends Layout {
|
|
901
|
+
minColumnWidth;
|
|
902
|
+
gap;
|
|
903
|
+
maxColumns;
|
|
904
|
+
constructor(config) {
|
|
905
|
+
super({ ...config, type: "responsivecolumn" });
|
|
906
|
+
this.minColumnWidth = config.minColumnWidth;
|
|
907
|
+
this.gap = config.gap ?? 0;
|
|
908
|
+
this.maxColumns = config.maxColumns;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Configures the container element with CSS Grid responsive columns.
|
|
912
|
+
*/
|
|
913
|
+
configureContainer(el) {
|
|
914
|
+
el.style.display = "grid";
|
|
915
|
+
if (this.maxColumns) {
|
|
916
|
+
el.style.gridTemplateColumns = `repeat(auto-fill, minmax(min(${this.minColumnWidth}px, 100%), ${Math.floor(100 / this.maxColumns)}%))`;
|
|
917
|
+
} else {
|
|
918
|
+
el.style.gridTemplateColumns = `repeat(auto-fill, minmax(${this.minColumnWidth}px, 1fr))`;
|
|
919
|
+
}
|
|
920
|
+
if (this.gap > 0) {
|
|
921
|
+
el.style.gap = `${this.gap}px`;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Applies columnSpan to items that need to span multiple columns.
|
|
926
|
+
*/
|
|
927
|
+
applyItemStyles(items, _target) {
|
|
928
|
+
for (const item of items) {
|
|
929
|
+
const el = item.el;
|
|
930
|
+
if (!el) continue;
|
|
931
|
+
const span = item._config?.columnSpan;
|
|
932
|
+
if (span && span > 1) {
|
|
933
|
+
el.style.gridColumn = `span ${span}`;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
renderItems(items, target) {
|
|
938
|
+
this.configureContainer(target);
|
|
939
|
+
super.renderItems(items, target);
|
|
940
|
+
this.applyItemStyles(items, target);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
export {
|
|
944
|
+
AbsoluteLayout,
|
|
945
|
+
AccordionLayout,
|
|
946
|
+
AnchorLayout,
|
|
947
|
+
AutoLayout,
|
|
948
|
+
BorderLayout,
|
|
949
|
+
BoxLayout,
|
|
950
|
+
CardLayout,
|
|
951
|
+
ColumnLayout,
|
|
952
|
+
FitLayout,
|
|
953
|
+
HBoxLayout,
|
|
954
|
+
Layout,
|
|
955
|
+
LayoutContext,
|
|
956
|
+
LayoutRunner,
|
|
957
|
+
ResponsiveColumnLayout,
|
|
958
|
+
ResponsivePlugin,
|
|
959
|
+
TableLayout,
|
|
960
|
+
VBoxLayout
|
|
961
|
+
};
|
|
962
|
+
//# sourceMappingURL=index.js.map
|