@dou-ui/dashboard 0.0.0-stage → 0.1.0-alpha.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/README.md +10 -2
- package/dist/chart/chart-settings.js +6 -0
- package/dist/chart/chart-settings.js.map +1 -0
- package/dist/chart/chart-settings.vue_vue_type_script_setup_true_lang.js +122 -0
- package/dist/chart/chart-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/chart/chart.js +6 -0
- package/dist/chart/chart.js.map +1 -0
- package/dist/chart/chart.vue_vue_type_script_setup_true_lang.js +99 -0
- package/dist/chart/chart.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/chart/index.js +33 -0
- package/dist/chart/index.js.map +1 -0
- package/dist/collaboration/index.js +281 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/core/document.js +128 -0
- package/dist/core/document.js.map +1 -0
- package/dist/core/history.js +66 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/transaction.js +218 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/dashboard/dashboard.js +6 -0
- package/dist/dashboard/dashboard.js.map +1 -0
- package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js +1072 -0
- package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/export.js +77 -0
- package/dist/dashboard/export.js.map +1 -0
- package/dist/dashboard/filter-bar.js +6 -0
- package/dist/dashboard/filter-bar.js.map +1 -0
- package/dist/dashboard/filter-bar.vue_vue_type_script_setup_true_lang.js +150 -0
- package/dist/dashboard/filter-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/frame-context.js +5 -0
- package/dist/dashboard/frame-context.js.map +1 -0
- package/dist/dashboard/observers.js +48 -0
- package/dist/dashboard/observers.js.map +1 -0
- package/dist/dashboard/palette.js +6 -0
- package/dist/dashboard/palette.js.map +1 -0
- package/dist/dashboard/palette.vue_vue_type_script_setup_true_lang.js +54 -0
- package/dist/dashboard/palette.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/widget-body.js +6 -0
- package/dist/dashboard/widget-body.js.map +1 -0
- package/dist/dashboard/widget-body.vue_vue_type_script_setup_true_lang.js +96 -0
- package/dist/dashboard/widget-body.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/widget-frame.js +6 -0
- package/dist/dashboard/widget-frame.js.map +1 -0
- package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js +300 -0
- package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/dashboard/widget-settings.js +6 -0
- package/dist/dashboard/widget-settings.js.map +1 -0
- package/dist/dashboard/widget-settings.vue_vue_type_script_setup_true_lang.js +36 -0
- package/dist/dashboard/widget-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/data/context.js +5 -0
- package/dist/data/context.js.map +1 -0
- package/dist/data/filters.js +31 -0
- package/dist/data/filters.js.map +1 -0
- package/dist/data/store.js +214 -0
- package/dist/data/store.js.map +1 -0
- package/dist/grid/grid-handle.js +6 -0
- package/dist/grid/grid-handle.js.map +1 -0
- package/dist/grid/grid-handle.vue_vue_type_script_setup_true_lang.js +21 -0
- package/dist/grid/grid-handle.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/grid/grid-item.js +80 -0
- package/dist/grid/grid-item.js.map +1 -0
- package/dist/grid/grid-layout.js +6 -0
- package/dist/grid/grid-layout.js.map +1 -0
- package/dist/grid/grid-layout.vue_vue_type_script_setup_true_lang.js +767 -0
- package/dist/grid/grid-layout.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/grid/tiers.js +31 -0
- package/dist/grid/tiers.js.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/engine.js +284 -0
- package/dist/layout/engine.js.map +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/locale.js +245 -0
- package/dist/locale.js.map +1 -0
- package/dist/pivot/index.js +47 -0
- package/dist/pivot/index.js.map +1 -0
- package/dist/pivot/pivot-settings.js +6 -0
- package/dist/pivot/pivot-settings.js.map +1 -0
- package/dist/pivot/pivot-settings.vue_vue_type_script_setup_true_lang.js +64 -0
- package/dist/pivot/pivot-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/pivot/pivot.js +6 -0
- package/dist/pivot/pivot.js.map +1 -0
- package/dist/pivot/pivot.vue_vue_type_script_setup_true_lang.js +76 -0
- package/dist/pivot/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +12 -0
- package/dist/types/chart/chart-settings.vue.d.ts +11 -0
- package/dist/types/chart/chart.vue.d.ts +3 -0
- package/dist/types/chart/index.d.ts +9 -0
- package/dist/types/chart/types.d.ts +23 -0
- package/dist/types/collaboration/index.d.ts +53 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/core/collaboration.d.ts +51 -0
- package/dist/types/core/document.d.ts +35 -0
- package/dist/types/core/history.d.ts +15 -0
- package/dist/types/core/transaction.d.ts +86 -0
- package/dist/types/core/types.d.ts +27 -0
- package/dist/types/dashboard/dashboard.vue.d.ts +33 -0
- package/dist/types/dashboard/export.d.ts +11 -0
- package/dist/types/dashboard/filter-bar.vue.d.ts +19 -0
- package/dist/types/dashboard/frame-context.d.ts +54 -0
- package/dist/types/dashboard/observers.d.ts +15 -0
- package/dist/types/dashboard/palette.vue.d.ts +20 -0
- package/dist/types/dashboard/types.d.ts +190 -0
- package/dist/types/dashboard/widget-body.vue.d.ts +19 -0
- package/dist/types/dashboard/widget-frame.vue.d.ts +6 -0
- package/dist/types/dashboard/widget-settings.vue.d.ts +16 -0
- package/dist/types/data/context.d.ts +18 -0
- package/dist/types/data/filters.d.ts +9 -0
- package/dist/types/data/store.d.ts +48 -0
- package/dist/types/data/types.d.ts +67 -0
- package/dist/types/grid/grid-handle.vue.d.ts +18 -0
- package/dist/types/grid/grid-item.d.ts +78 -0
- package/dist/types/grid/grid-layout.vue.d.ts +46 -0
- package/dist/types/grid/tiers.d.ts +12 -0
- package/dist/types/grid/types.d.ts +113 -0
- package/dist/types/index.d.ts +295 -0
- package/dist/types/layout/engine.d.ts +50 -0
- package/dist/types/layout/index.d.ts +2 -0
- package/dist/types/layout/types.d.ts +39 -0
- package/dist/types/locale.d.ts +117 -0
- package/dist/types/pivot/index.d.ts +14 -0
- package/dist/types/pivot/pivot-settings.vue.d.ts +16 -0
- package/dist/types/pivot/pivot.vue.d.ts +7 -0
- package/dist/types/pivot/types.d.ts +11 -0
- package/dist/types/widget/context.d.ts +91 -0
- package/dist/types/widget/define.d.ts +50 -0
- package/dist/types/widgets/field-select.vue.d.ts +18 -0
- package/dist/types/widgets/index.d.ts +15 -0
- package/dist/types/widgets/settings-field.vue.d.ts +17 -0
- package/dist/types/widgets/shared.d.ts +20 -0
- package/dist/types/widgets/source-field.vue.d.ts +15 -0
- package/dist/types/widgets/stat-settings.vue.d.ts +11 -0
- package/dist/types/widgets/stat.vue.d.ts +3 -0
- package/dist/types/widgets/table-settings.vue.d.ts +11 -0
- package/dist/types/widgets/table.vue.d.ts +3 -0
- package/dist/types/widgets/text-settings.vue.d.ts +11 -0
- package/dist/types/widgets/text.vue.d.ts +3 -0
- package/dist/types/widgets/types.d.ts +38 -0
- package/dist/widget/context.js +88 -0
- package/dist/widget/context.js.map +1 -0
- package/dist/widget/define.js +32 -0
- package/dist/widget/define.js.map +1 -0
- package/dist/widgets/field-select.js +6 -0
- package/dist/widgets/field-select.js.map +1 -0
- package/dist/widgets/field-select.vue_vue_type_script_setup_true_lang.js +54 -0
- package/dist/widgets/field-select.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/index.js +74 -0
- package/dist/widgets/index.js.map +1 -0
- package/dist/widgets/settings-field.js +6 -0
- package/dist/widgets/settings-field.js.map +1 -0
- package/dist/widgets/settings-field.vue_vue_type_script_setup_true_lang.js +28 -0
- package/dist/widgets/settings-field.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/shared.js +68 -0
- package/dist/widgets/shared.js.map +1 -0
- package/dist/widgets/source-field.js +6 -0
- package/dist/widgets/source-field.js.map +1 -0
- package/dist/widgets/source-field.vue_vue_type_script_setup_true_lang.js +47 -0
- package/dist/widgets/source-field.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/stat-settings.js +6 -0
- package/dist/widgets/stat-settings.js.map +1 -0
- package/dist/widgets/stat-settings.vue_vue_type_script_setup_true_lang.js +107 -0
- package/dist/widgets/stat-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/stat.js +6 -0
- package/dist/widgets/stat.js.map +1 -0
- package/dist/widgets/stat.vue_vue_type_script_setup_true_lang.js +72 -0
- package/dist/widgets/stat.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/table-settings.js +6 -0
- package/dist/widgets/table-settings.js.map +1 -0
- package/dist/widgets/table-settings.vue_vue_type_script_setup_true_lang.js +90 -0
- package/dist/widgets/table-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/table.js +6 -0
- package/dist/widgets/table.js.map +1 -0
- package/dist/widgets/table.vue_vue_type_script_setup_true_lang.js +91 -0
- package/dist/widgets/table.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/text-settings.js +6 -0
- package/dist/widgets/text-settings.js.map +1 -0
- package/dist/widgets/text-settings.vue_vue_type_script_setup_true_lang.js +63 -0
- package/dist/widgets/text-settings.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/widgets/text.js +6 -0
- package/dist/widgets/text.js.map +1 -0
- package/dist/widgets/text.vue_vue_type_script_setup_true_lang.js +16 -0
- package/dist/widgets/text.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { pickFilters, stableKey } from "./filters.js";
|
|
2
|
+
import { shallowRef } from "vue";
|
|
3
|
+
// src/data/store.ts
|
|
4
|
+
/** How many loaded results nobody shows are kept, to come back to without loading. */
|
|
5
|
+
var KEEP = 32;
|
|
6
|
+
function optionsOf(source) {
|
|
7
|
+
return typeof source === "function" ? { load: source } : source;
|
|
8
|
+
}
|
|
9
|
+
/** Whether two definitions of a source load the same way. */
|
|
10
|
+
function sameSource(a, b) {
|
|
11
|
+
if (!a || !b) return a === b;
|
|
12
|
+
const x = optionsOf(a);
|
|
13
|
+
const y = optionsOf(b);
|
|
14
|
+
return x.load === y.load && x.interval === y.interval && (x.filters ?? []).join("\0") === (y.filters ?? []).join("\0");
|
|
15
|
+
}
|
|
16
|
+
var isAbort = (error) => !!error && typeof error === "object" && error.name === "AbortError";
|
|
17
|
+
/**
|
|
18
|
+
* Loads the dashboard's sources: once for each source and set of filters,
|
|
19
|
+
* however many widgets show it. Results nobody shows any more are kept a
|
|
20
|
+
* while; loads nobody waits for are aborted.
|
|
21
|
+
*/
|
|
22
|
+
function createSourceStore(sources) {
|
|
23
|
+
const entries = /* @__PURE__ */ new Map();
|
|
24
|
+
const server = typeof window === "undefined";
|
|
25
|
+
let hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
26
|
+
function onVisibility() {
|
|
27
|
+
hidden = document.visibilityState === "hidden";
|
|
28
|
+
if (hidden) return;
|
|
29
|
+
for (const entry of entries.values()) if (entry.due && entry.users > 0) {
|
|
30
|
+
entry.due = false;
|
|
31
|
+
load(entry, true);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
|
|
35
|
+
function schedule(entry) {
|
|
36
|
+
clearTimeout(entry.timer);
|
|
37
|
+
entry.timer = void 0;
|
|
38
|
+
const every = optionsOf(sources()?.[entry.name] ?? (() => void 0)).interval;
|
|
39
|
+
if (!every || every <= 0 || entry.users === 0) return;
|
|
40
|
+
const wait = Math.max(0, entry.loadedAt + every - Date.now());
|
|
41
|
+
entry.timer = setTimeout(() => {
|
|
42
|
+
entry.timer = void 0;
|
|
43
|
+
if (entry.users === 0) return;
|
|
44
|
+
if (hidden) entry.due = true;
|
|
45
|
+
else load(entry, true);
|
|
46
|
+
}, wait);
|
|
47
|
+
}
|
|
48
|
+
function load(entry, refresh = false) {
|
|
49
|
+
const source = sources()?.[entry.name];
|
|
50
|
+
entry.controller?.abort();
|
|
51
|
+
clearTimeout(entry.timer);
|
|
52
|
+
const previous = entry.state.value;
|
|
53
|
+
const data = "data" in previous ? previous.data : void 0;
|
|
54
|
+
if (!source) {
|
|
55
|
+
entry.state.value = {
|
|
56
|
+
status: "error",
|
|
57
|
+
error: /* @__PURE__ */ new Error(`[@dou-ui/dashboard] no source is named "${entry.name}"`)
|
|
58
|
+
};
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
entry.controller = controller;
|
|
63
|
+
entry.refreshing.value = refresh && previous.status === "ready";
|
|
64
|
+
if (!entry.refreshing.value) entry.state.value = {
|
|
65
|
+
status: "loading",
|
|
66
|
+
data
|
|
67
|
+
};
|
|
68
|
+
const done = (next) => {
|
|
69
|
+
if (entry.controller !== controller) return;
|
|
70
|
+
entry.controller = void 0;
|
|
71
|
+
entry.loadedAt = Date.now();
|
|
72
|
+
entry.refreshing.value = false;
|
|
73
|
+
entry.state.value = next;
|
|
74
|
+
schedule(entry);
|
|
75
|
+
};
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = optionsOf(source).load({
|
|
79
|
+
filters: entry.filters,
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
source: entry.name
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
done({
|
|
85
|
+
status: "error",
|
|
86
|
+
data,
|
|
87
|
+
error
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
Promise.resolve(result).then((value) => done({
|
|
92
|
+
status: "ready",
|
|
93
|
+
data: value
|
|
94
|
+
}), (error) => {
|
|
95
|
+
if (controller.signal.aborted && isAbort(error)) return;
|
|
96
|
+
done({
|
|
97
|
+
status: "error",
|
|
98
|
+
data,
|
|
99
|
+
error
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Drops the oldest results nobody shows, past `KEEP`. */
|
|
104
|
+
function trim() {
|
|
105
|
+
const idle = [];
|
|
106
|
+
for (const entry of entries.values()) if (entry.users === 0) idle.push(entry);
|
|
107
|
+
if (idle.length <= KEEP) return;
|
|
108
|
+
idle.sort((a, b) => a.idleSince - b.idleSince);
|
|
109
|
+
for (const entry of idle.slice(0, idle.length - KEEP)) entries.delete(entry.key);
|
|
110
|
+
}
|
|
111
|
+
/** The key a source and the filters in effect load under. */
|
|
112
|
+
function keyOf(name, filters) {
|
|
113
|
+
const source = sources()?.[name];
|
|
114
|
+
const scoped = pickFilters(filters, source ? optionsOf(source).filters : void 0);
|
|
115
|
+
return {
|
|
116
|
+
key: `${name}\u0000${stableKey(scoped)}`,
|
|
117
|
+
scoped
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Loads a newly shown entry, or schedules the refresh of a kept result. */
|
|
121
|
+
function start(entry) {
|
|
122
|
+
const status = entry.state.value.status;
|
|
123
|
+
if (status === "idle" || status === "error" && !entry.controller) load(entry);
|
|
124
|
+
else if (entry.users === 1 && !entry.controller) {
|
|
125
|
+
const every = optionsOf(sources()?.[entry.name] ?? (() => void 0)).interval;
|
|
126
|
+
if (every && Date.now() - entry.loadedAt >= every) load(entry, true);
|
|
127
|
+
else schedule(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Starts showing a source with the filters in effect; loads it unless a
|
|
132
|
+
* result (or a load) for the same source and filters is there already.
|
|
133
|
+
* Call `release` when it is no longer shown.
|
|
134
|
+
*/
|
|
135
|
+
function use(name, filters) {
|
|
136
|
+
const { key, scoped } = keyOf(name, filters);
|
|
137
|
+
let entry = entries.get(key);
|
|
138
|
+
if (!entry) {
|
|
139
|
+
entry = {
|
|
140
|
+
key,
|
|
141
|
+
name,
|
|
142
|
+
filters: scoped,
|
|
143
|
+
state: shallowRef({ status: "idle" }),
|
|
144
|
+
refreshing: shallowRef(false),
|
|
145
|
+
users: 0,
|
|
146
|
+
loadedAt: 0,
|
|
147
|
+
due: false,
|
|
148
|
+
idleSince: 0
|
|
149
|
+
};
|
|
150
|
+
entries.set(key, entry);
|
|
151
|
+
}
|
|
152
|
+
entry.users++;
|
|
153
|
+
if (!server) start(entry);
|
|
154
|
+
const used = entry;
|
|
155
|
+
let released = false;
|
|
156
|
+
return {
|
|
157
|
+
entry: used,
|
|
158
|
+
release() {
|
|
159
|
+
if (released) return;
|
|
160
|
+
released = true;
|
|
161
|
+
used.users--;
|
|
162
|
+
if (used.users > 0) return;
|
|
163
|
+
used.idleSince = Date.now();
|
|
164
|
+
clearTimeout(used.timer);
|
|
165
|
+
used.timer = void 0;
|
|
166
|
+
used.due = false;
|
|
167
|
+
if (used.controller) {
|
|
168
|
+
used.controller.abort();
|
|
169
|
+
used.controller = void 0;
|
|
170
|
+
used.refreshing.value = false;
|
|
171
|
+
if (used.state.value.status === "loading") entries.delete(used.key);
|
|
172
|
+
}
|
|
173
|
+
trim();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Loads again: one source, or every source; results nobody shows are dropped. */
|
|
178
|
+
function reload(name) {
|
|
179
|
+
for (const entry of entries.values()) {
|
|
180
|
+
if (name !== void 0 && entry.name !== name) continue;
|
|
181
|
+
if (entry.users > 0) load(entry, true);
|
|
182
|
+
else entries.delete(entry.key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Loads one entry again, e.g. after an error. */
|
|
186
|
+
function retry(entry) {
|
|
187
|
+
const own = entries.get(entry.key);
|
|
188
|
+
if (own) load(own);
|
|
189
|
+
}
|
|
190
|
+
/** Whether a source shown is loading. */
|
|
191
|
+
function busy() {
|
|
192
|
+
for (const entry of entries.values()) if (entry.users > 0 && entry.controller) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
function dispose() {
|
|
196
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
|
|
197
|
+
for (const entry of entries.values()) {
|
|
198
|
+
clearTimeout(entry.timer);
|
|
199
|
+
entry.controller?.abort();
|
|
200
|
+
}
|
|
201
|
+
entries.clear();
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
use,
|
|
205
|
+
reload,
|
|
206
|
+
retry,
|
|
207
|
+
busy,
|
|
208
|
+
dispose,
|
|
209
|
+
keyOf
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export { createSourceStore, optionsOf, sameSource };
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../src/data/store.ts"],"sourcesContent":["import { shallowRef } from 'vue'\nimport type { ShallowRef } from 'vue'\nimport { pickFilters, stableKey } from './filters'\nimport type {\n DashboardFilters,\n DashboardSourceLoad,\n DashboardSourceOptions,\n DashboardSources,\n} from './types'\n\nexport type SourceState =\n | { status: 'idle' }\n | { status: 'loading'; data?: unknown; error?: unknown }\n | { status: 'ready'; data: unknown }\n | { status: 'error'; data?: unknown; error: unknown }\n\n/** One source loaded with one set of filters, shared by every widget showing it. */\nexport interface SourceEntry {\n readonly key: string\n readonly name: string\n readonly filters: DashboardFilters\n readonly state: ShallowRef<SourceState>\n /** Whether a load is running for data already shown (a refresh), not for new data. */\n readonly refreshing: ShallowRef<boolean>\n}\n\ninterface Entry extends SourceEntry {\n users: number\n controller?: AbortController\n loadedAt: number\n timer?: ReturnType<typeof setTimeout>\n /** A refresh came due while the page was hidden. */\n due: boolean\n /** When it was last let go, for dropping the oldest unused entries. */\n idleSince: number\n}\n\n/** How many loaded results nobody shows are kept, to come back to without loading. */\nconst KEEP = 32\n\nexport function optionsOf(\n source: DashboardSourceLoad<any> | DashboardSourceOptions<any>,\n): DashboardSourceOptions<any> {\n return typeof source === 'function' ? { load: source } : source\n}\n\n/** Whether two definitions of a source load the same way. */\nexport function sameSource(\n a: DashboardSourceLoad<any> | DashboardSourceOptions<any> | undefined,\n b: DashboardSourceLoad<any> | DashboardSourceOptions<any> | undefined,\n) {\n if (!a || !b) return a === b\n const x = optionsOf(a)\n const y = optionsOf(b)\n return (\n x.load === y.load &&\n x.interval === y.interval &&\n (x.filters ?? []).join('\\u0000') === (y.filters ?? []).join('\\u0000')\n )\n}\n\nconst isAbort = (error: unknown) =>\n !!error && typeof error === 'object' && (error as { name?: string }).name === 'AbortError'\n\n/**\n * Loads the dashboard's sources: once for each source and set of filters,\n * however many widgets show it. Results nobody shows any more are kept a\n * while; loads nobody waits for are aborted.\n */\nexport function createSourceStore(sources: () => DashboardSources | undefined) {\n const entries = new Map<string, Entry>()\n // nothing loads while rendering on the server: the page is sent before a\n // load could finish, and a refresh timer would keep the server process alive\n const server = typeof window === 'undefined'\n let hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'\n\n function onVisibility() {\n hidden = document.visibilityState === 'hidden'\n if (hidden) return\n for (const entry of entries.values()) {\n if (entry.due && entry.users > 0) {\n entry.due = false\n load(entry, true)\n }\n }\n }\n if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibility)\n\n function schedule(entry: Entry) {\n clearTimeout(entry.timer)\n entry.timer = undefined\n const every = optionsOf(sources()?.[entry.name] ?? (() => undefined)).interval\n if (!every || every <= 0 || entry.users === 0) return\n const wait = Math.max(0, entry.loadedAt + every - Date.now())\n entry.timer = setTimeout(() => {\n entry.timer = undefined\n if (entry.users === 0) return\n if (hidden) entry.due = true\n else load(entry, true)\n }, wait)\n }\n\n function load(entry: Entry, refresh = false) {\n const source = sources()?.[entry.name]\n entry.controller?.abort()\n clearTimeout(entry.timer)\n const previous = entry.state.value\n const data = 'data' in previous ? previous.data : undefined\n if (!source) {\n entry.state.value = {\n status: 'error',\n error: new Error(`[@dou-ui/dashboard] no source is named \"${entry.name}\"`),\n }\n return\n }\n const controller = new AbortController()\n entry.controller = controller\n entry.refreshing.value = refresh && previous.status === 'ready'\n if (!entry.refreshing.value) entry.state.value = { status: 'loading', data }\n const done = (next: SourceState) => {\n if (entry.controller !== controller) return\n entry.controller = undefined\n entry.loadedAt = Date.now()\n entry.refreshing.value = false\n entry.state.value = next\n schedule(entry)\n }\n let result: unknown\n try {\n result = optionsOf(source).load({\n filters: entry.filters,\n signal: controller.signal,\n source: entry.name,\n })\n } catch (error) {\n done({ status: 'error', data, error })\n return\n }\n Promise.resolve(result).then(\n (value) => done({ status: 'ready', data: value }),\n (error) => {\n if (controller.signal.aborted && isAbort(error)) return\n done({ status: 'error', data, error })\n },\n )\n }\n\n /** Drops the oldest results nobody shows, past `KEEP`. */\n function trim() {\n const idle: Entry[] = []\n for (const entry of entries.values()) if (entry.users === 0) idle.push(entry)\n if (idle.length <= KEEP) return\n idle.sort((a, b) => a.idleSince - b.idleSince)\n for (const entry of idle.slice(0, idle.length - KEEP)) entries.delete(entry.key)\n }\n\n /** The key a source and the filters in effect load under. */\n function keyOf(name: string, filters: DashboardFilters) {\n const source = sources()?.[name]\n const scoped = pickFilters(filters, source ? optionsOf(source).filters : undefined)\n return { key: `${name}\\u0000${stableKey(scoped)}`, scoped }\n }\n\n /** Loads a newly shown entry, or schedules the refresh of a kept result. */\n function start(entry: Entry) {\n const status = entry.state.value.status\n if (status === 'idle' || (status === 'error' && !entry.controller)) load(entry)\n else if (entry.users === 1 && !entry.controller) {\n const every = optionsOf(sources()?.[entry.name] ?? (() => undefined)).interval\n // a kept result older than its refresh interval loads again\n if (every && Date.now() - entry.loadedAt >= every) load(entry, true)\n else schedule(entry)\n }\n }\n\n /**\n * Starts showing a source with the filters in effect; loads it unless a\n * result (or a load) for the same source and filters is there already.\n * Call `release` when it is no longer shown.\n */\n function use(name: string, filters: DashboardFilters) {\n const { key, scoped } = keyOf(name, filters)\n let entry = entries.get(key)\n if (!entry) {\n entry = {\n key,\n name,\n filters: scoped,\n state: shallowRef<SourceState>({ status: 'idle' }),\n refreshing: shallowRef(false),\n users: 0,\n loadedAt: 0,\n due: false,\n idleSince: 0,\n }\n entries.set(key, entry)\n }\n entry.users++\n // on the server it stays loading until the browser takes over\n if (!server) start(entry)\n const used = entry\n let released = false\n return {\n entry: used as SourceEntry,\n release() {\n if (released) return\n released = true\n used.users--\n if (used.users > 0) return\n used.idleSince = Date.now()\n clearTimeout(used.timer)\n used.timer = undefined\n used.due = false\n if (used.controller) {\n // nobody waits for it: stop it, and forget a result that never came\n used.controller.abort()\n used.controller = undefined\n used.refreshing.value = false\n if (used.state.value.status === 'loading') entries.delete(used.key)\n }\n trim()\n },\n }\n }\n\n /** Loads again: one source, or every source; results nobody shows are dropped. */\n function reload(name?: string) {\n for (const entry of entries.values()) {\n if (name !== undefined && entry.name !== name) continue\n if (entry.users > 0) load(entry, true)\n else entries.delete(entry.key)\n }\n }\n\n /** Loads one entry again, e.g. after an error. */\n function retry(entry: SourceEntry) {\n const own = entries.get(entry.key)\n if (own) load(own)\n }\n\n /** Whether a source shown is loading. */\n function busy() {\n for (const entry of entries.values()) if (entry.users > 0 && entry.controller) return true\n return false\n }\n\n function dispose() {\n if (typeof document !== 'undefined')\n document.removeEventListener('visibilitychange', onVisibility)\n for (const entry of entries.values()) {\n clearTimeout(entry.timer)\n entry.controller?.abort()\n }\n entries.clear()\n }\n\n return { use, reload, retry, busy, dispose, keyOf }\n}\n\nexport type SourceStore = ReturnType<typeof createSourceStore>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import grid_handle_vue_vue_type_script_setup_true_lang_default from "./grid-handle.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/grid/grid-handle.vue
|
|
3
|
+
var grid_handle_default = grid_handle_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { grid_handle_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=grid-handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-handle.js","names":["_sfc_main"],"sources":["../../src/grid/grid-handle.vue"],"sourcesContent":["<script setup lang=\"ts\">\ndefineOptions({ name: 'DouGridLayoutHandle' })\n\nwithDefaults(\n defineProps<{\n /** The element to render, such as `header`. */\n as?: string\n }>(),\n { as: 'div' },\n)\ndefineSlots<{ default?: () => unknown }>()\n</script>\n\n<template>\n <component :is=\"as\" class=\"dou-grid-layout__handle\" data-dou-grid-handle>\n <slot />\n </component>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createBlock, defineComponent, openBlock, renderSlot, resolveDynamicComponent, withCtx } from "vue";
|
|
2
|
+
// src/grid/grid-handle.vue?vue&type=script&setup=true&lang.ts
|
|
3
|
+
var grid_handle_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
4
|
+
name: "DouGridLayoutHandle",
|
|
5
|
+
__name: "grid-handle",
|
|
6
|
+
props: { as: { default: "div" } },
|
|
7
|
+
setup(__props) {
|
|
8
|
+
return (_ctx, _cache) => {
|
|
9
|
+
return openBlock(), createBlock(resolveDynamicComponent(__props.as), {
|
|
10
|
+
class: "dou-grid-layout__handle",
|
|
11
|
+
"data-dou-grid-handle": ""
|
|
12
|
+
}, {
|
|
13
|
+
default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
|
|
14
|
+
_: 3
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
export { grid_handle_vue_vue_type_script_setup_true_lang_default as default };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=grid-handle.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-handle.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createBlock","_resolveDynamicComponent","_withCtx","_renderSlot"],"sources":["../../src/grid/grid-handle.vue"],"sourcesContent":["<script setup lang=\"ts\">\ndefineOptions({ name: 'DouGridLayoutHandle' })\n\nwithDefaults(\n defineProps<{\n /** The element to render, such as `header`. */\n as?: string\n }>(),\n { as: 'div' },\n)\ndefineSlots<{ default?: () => unknown }>()\n</script>\n\n<template>\n <component :is=\"as\" class=\"dou-grid-layout__handle\" data-dou-grid-handle>\n <slot />\n </component>\n</template>\n"],"mappings":";;;;;;;;GAcE,OAAAA,UAAA,GAAAC,YAEYC,wBAFI,QAAA,EAAE,GAAA;IAAE,OAAM;IAA0B,wBAAA;;IAClD,SAAAC,cAAQ,CAARC,WAAQ,KAAA,QAAA,SAAA,C"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineComponent, h, inject } from "vue";
|
|
2
|
+
// src/grid/grid-item.ts
|
|
3
|
+
var gridItemKey = Symbol("dou-grid-item");
|
|
4
|
+
/**
|
|
5
|
+
* One item of the grid. A component of its own so that moving one item
|
|
6
|
+
* re-renders that item alone: the others get the same `item` object back
|
|
7
|
+
* from the layout engine, and their slot content isn't run again.
|
|
8
|
+
*/
|
|
9
|
+
var grid_item_default = defineComponent({
|
|
10
|
+
name: "DouGridLayoutItem",
|
|
11
|
+
props: {
|
|
12
|
+
item: {
|
|
13
|
+
type: Object,
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
state: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: "idle"
|
|
19
|
+
},
|
|
20
|
+
editable: Boolean,
|
|
21
|
+
/** -1, 0, or undefined when the item itself isn't focusable. */
|
|
22
|
+
tabindex: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: void 0
|
|
25
|
+
},
|
|
26
|
+
label: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true
|
|
29
|
+
},
|
|
30
|
+
roleDescription: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true
|
|
33
|
+
},
|
|
34
|
+
handles: {
|
|
35
|
+
type: Array,
|
|
36
|
+
default: () => []
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
setup(props) {
|
|
40
|
+
const context = inject(gridItemKey);
|
|
41
|
+
return () => {
|
|
42
|
+
const { item, state, editable } = props;
|
|
43
|
+
const movable = editable && !item.locked;
|
|
44
|
+
const children = [context.slots.item?.({
|
|
45
|
+
item,
|
|
46
|
+
dragging: state === "dragging",
|
|
47
|
+
resizing: state === "resizing"
|
|
48
|
+
})];
|
|
49
|
+
if (movable) for (const edge of props.handles) children.push(h("span", {
|
|
50
|
+
class: `dou-grid-layout__resize is-${edge}`,
|
|
51
|
+
"data-grid-resize": edge,
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
title: edge === "se" ? context.resizeLabel() : void 0
|
|
54
|
+
}));
|
|
55
|
+
return h("div", {
|
|
56
|
+
class: ["dou-grid-layout__item", {
|
|
57
|
+
"is-movable": movable,
|
|
58
|
+
"is-locked": item.locked,
|
|
59
|
+
"is-dragging": state === "dragging",
|
|
60
|
+
"is-resizing": state === "resizing"
|
|
61
|
+
}],
|
|
62
|
+
style: {
|
|
63
|
+
"--dou-grid-x": item.x,
|
|
64
|
+
"--dou-grid-y": item.y,
|
|
65
|
+
"--dou-grid-w": item.w,
|
|
66
|
+
"--dou-grid-h": item.h
|
|
67
|
+
},
|
|
68
|
+
"data-grid-id": item.id,
|
|
69
|
+
tabindex: props.tabindex,
|
|
70
|
+
role: editable ? "group" : void 0,
|
|
71
|
+
"aria-roledescription": editable ? props.roleDescription : void 0,
|
|
72
|
+
"aria-label": editable ? props.label : void 0,
|
|
73
|
+
"aria-describedby": movable ? context.hintId : void 0
|
|
74
|
+
}, children);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
export { grid_item_default as default, gridItemKey };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=grid-item.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-item.js","names":[],"sources":["../../src/grid/grid-item.ts"],"sourcesContent":["import {\n defineComponent,\n h,\n inject,\n type InjectionKey,\n type PropType,\n type Slots,\n type VNodeArrayChildren,\n} from 'vue'\nimport type { GridItem } from '../layout/types'\nimport type { GridResizeHandle } from './types'\n\nexport interface GridItemContext {\n slots: Slots\n /** The id the description of every item is read from. */\n hintId: string\n resizeLabel: () => string\n}\n\nexport const gridItemKey: InjectionKey<GridItemContext> = Symbol('dou-grid-item')\n\n/**\n * One item of the grid. A component of its own so that moving one item\n * re-renders that item alone: the others get the same `item` object back\n * from the layout engine, and their slot content isn't run again.\n */\nexport default defineComponent({\n name: 'DouGridLayoutItem',\n props: {\n item: { type: Object as PropType<GridItem>, required: true },\n state: { type: String as PropType<'idle' | 'dragging' | 'resizing'>, default: 'idle' },\n editable: Boolean,\n /** -1, 0, or undefined when the item itself isn't focusable. */\n tabindex: { type: Number, default: undefined },\n label: { type: String, required: true },\n roleDescription: { type: String, required: true },\n handles: { type: Array as PropType<GridResizeHandle[]>, default: () => [] },\n },\n setup(props) {\n const context = inject(gridItemKey)!\n return () => {\n const { item, state, editable } = props\n const movable = editable && !item.locked\n const children: VNodeArrayChildren = [\n context.slots.item?.({\n item,\n dragging: state === 'dragging',\n resizing: state === 'resizing',\n }),\n ]\n if (movable)\n for (const edge of props.handles)\n children.push(\n h('span', {\n class: `dou-grid-layout__resize is-${edge}`,\n 'data-grid-resize': edge,\n 'aria-hidden': 'true',\n title: edge === 'se' ? context.resizeLabel() : undefined,\n }),\n )\n return h(\n 'div',\n {\n class: [\n 'dou-grid-layout__item',\n {\n 'is-movable': movable,\n 'is-locked': item.locked,\n 'is-dragging': state === 'dragging',\n 'is-resizing': state === 'resizing',\n },\n ],\n style: {\n '--dou-grid-x': item.x,\n '--dou-grid-y': item.y,\n '--dou-grid-w': item.w,\n '--dou-grid-h': item.h,\n },\n 'data-grid-id': item.id,\n tabindex: props.tabindex,\n role: editable ? 'group' : undefined,\n 'aria-roledescription': editable ? props.roleDescription : undefined,\n 'aria-label': editable ? props.label : undefined,\n 'aria-describedby': movable ? context.hintId : undefined,\n },\n children,\n )\n }\n },\n})\n"],"mappings":";;AAmBA,IAAa,cAA6C,OAAO,eAAe;;;;;;AAOhF,IAAA,oBAAe,gBAAgB;CAC7B,MAAM;CACN,OAAO;EACL,MAAM;GAAE,MAAM;GAA8B,UAAU;EAAK;EAC3D,OAAO;GAAE,MAAM;GAAsD,SAAS;EAAO;EACrF,UAAU;;EAEV,UAAU;GAAE,MAAM;GAAQ,SAAS,KAAA;EAAU;EAC7C,OAAO;GAAE,MAAM;GAAQ,UAAU;EAAK;EACtC,iBAAiB;GAAE,MAAM;GAAQ,UAAU;EAAK;EAChD,SAAS;GAAE,MAAM;GAAuC,eAAe,CAAC;EAAE;CAC5E;CACA,MAAM,OAAO;EACX,MAAM,UAAU,OAAO,WAAW;EAClC,aAAa;GACX,MAAM,EAAE,MAAM,OAAO,aAAa;GAClC,MAAM,UAAU,YAAY,CAAC,KAAK;GAClC,MAAM,WAA+B,CACnC,QAAQ,MAAM,OAAO;IACnB;IACA,UAAU,UAAU;IACpB,UAAU,UAAU;GACtB,CAAC,CACH;GACA,IAAI,SACF,KAAK,MAAM,QAAQ,MAAM,SACvB,SAAS,KACP,EAAE,QAAQ;IACR,OAAO,8BAA8B;IACrC,oBAAoB;IACpB,eAAe;IACf,OAAO,SAAS,OAAO,QAAQ,YAAY,IAAI,KAAA;GACjD,CAAC,CACH;GACJ,OAAO,EACL,OACA;IACE,OAAO,CACL,yBACA;KACE,cAAc;KACd,aAAa,KAAK;KAClB,eAAe,UAAU;KACzB,eAAe,UAAU;IAC3B,CACF;IACA,OAAO;KACL,gBAAgB,KAAK;KACrB,gBAAgB,KAAK;KACrB,gBAAgB,KAAK;KACrB,gBAAgB,KAAK;IACvB;IACA,gBAAgB,KAAK;IACrB,UAAU,MAAM;IAChB,MAAM,WAAW,UAAU,KAAA;IAC3B,wBAAwB,WAAW,MAAM,kBAAkB,KAAA;IAC3D,cAAc,WAAW,MAAM,QAAQ,KAAA;IACvC,oBAAoB,UAAU,QAAQ,SAAS,KAAA;GACjD,GACA,QACF;EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import grid_layout_vue_vue_type_script_setup_true_lang_default from "./grid-layout.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/grid/grid-layout.vue
|
|
3
|
+
var grid_layout_default = grid_layout_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { grid_layout_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=grid-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-layout.js","names":["_sfc_main"],"sources":["../../src/grid/grid-layout.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { holdPointer, useElementSize, type PointerHold } from 'dou-ui'\nimport { computed, onBeforeUnmount, provide, ref, shallowRef, useId, useSlots, watch } from 'vue'\nimport {\n deriveLayout,\n findPlace,\n fitItem,\n layoutHeight,\n moveItem,\n moveItemBy,\n normalizeLayout,\n resizeItem,\n resizeItemBy,\n} from '../layout/engine'\nimport type { GridItem, GridLayoutOptions } from '../layout/types'\nimport { format, useDashboardMessages } from '../locale'\nimport GridItemView, { gridItemKey } from './grid-item'\nimport { DEFAULT_BREAKPOINTS, DEFAULT_COLUMNS, gridTiers, type GridTier } from './tiers'\nimport type {\n GridChangeReason,\n GridDropItem,\n GridItemSlotProps,\n GridLayouts,\n GridLayoutEmits,\n GridLayoutExpose,\n GridLayoutProps,\n GridResizeHandle,\n} from './types'\n\ndefineOptions({ name: 'DouGridLayout' })\n\nconst props = withDefaults(defineProps<Omit<GridLayoutProps, 'modelValue' | 'layouts'>>(), {\n columns: () => DEFAULT_COLUMNS,\n breakpoints: () => DEFAULT_BREAKPOINTS,\n rowHeight: 80,\n gap: 16,\n compact: 'vertical',\n resize: true,\n})\nconst emit = defineEmits<Omit<GridLayoutEmits, 'update:modelValue' | 'update:layouts'>>()\ndefineSlots<{\n item?: (props: GridItemSlotProps) => unknown\n placeholder?: (props: { item: GridItem }) => unknown\n}>()\n\nconst model = defineModel<GridItem[]>({ default: () => [] })\nconst layouts = defineModel<GridLayouts>('layouts', { default: () => ({}) })\nconst messages = useDashboardMessages(() => props.messages)\nconst slots = useSlots()\nconst hintId = `${useId()}-hint`\nprovide(gridItemKey, { slots, hintId, resizeLabel: () => messages.value.resize })\n\n/** How far a mouse moves before a press becomes a move, in px. */\nconst DRAG_DISTANCE = 4\n/** How long a finger rests on an item before it is picked up, in ms. */\nconst HOLD_DELAY = 400\n/** How far a finger may move while resting, in px; further is a scroll. */\nconst HOLD_SLOP = 8\n/** The band along the top and bottom of the scrolling area that scrolls it during a drag, in px. */\nconst EDGE = 56\n/** The fastest that band scrolls, in px per frame. */\nconst EDGE_SPEED = 20\n/** How long items keep their transition after a drop, in ms (a little over the motion token). */\nconst SETTLE = 320\n\nconst root = ref<HTMLElement>()\n\n// ---------------------------------------------------------------------------\n// Breakpoints: the grid's own width picks how many columns it has\n\ntype Tier = GridTier\n\nconst tiers = computed(() => gridTiers(props.columns, props.breakpoints))\nconst { width: gridWidth } = useElementSize(root)\nconst tier = computed(() => {\n const list = tiers.value\n // not measured yet (or on the server): the widest\n if (!gridWidth.value) return list[0]!\n return list.find((t) => gridWidth.value >= t.min) ?? list.at(-1)!\n})\n/** The widest breakpoint: the one `v-model` holds. */\nconst isMain = computed(() => tier.value === tiers.value[0])\nconst optionsOf = (t: Tier): GridLayoutOptions => ({\n columns: Math.max(1, Math.round(t.columns)),\n compact: props.compact,\n})\nconst options = computed(() => optionsOf(tier.value))\nconst mainLayout = computed(() => normalizeLayout(model.value, optionsOf(tiers.value[0]!)))\nconst settledRaw = computed<readonly GridItem[]>(() => {\n if (isMain.value) return mainLayout.value\n const main = mainLayout.value\n const derived = deriveLayout(main, optionsOf(tiers.value[0]!).columns, options.value)\n const saved = layouts.value[tier.value.name]\n if (!saved?.length) return derived\n const places = new Map(saved.map((place) => [place.id, place]))\n return normalizeLayout(\n main.map((item, i) => {\n const place = places.get(item.id)\n return place ? { ...item, x: place.x, y: place.y, w: place.w, h: place.h } : derived[i]!\n }),\n options.value,\n )\n})\n/**\n * The items drawn last time are reused where nothing about them changed, so\n * a change to one item draws only that one again, even when the layout had\n * to be worked out anew (e.g. from a document that wasn't compacted).\n */\nlet drawnById = new Map<string, GridItem>()\nconst ITEM_KEYS = ['x', 'y', 'w', 'h', 'minW', 'maxW', 'minH', 'maxH', 'locked'] as const\nconst settled = computed<readonly GridItem[]>(() => {\n const next = new Map<string, GridItem>()\n const list = settledRaw.value.map((item) => {\n const old = drawnById.get(item.id)\n const keep = old && ITEM_KEYS.every((key) => old[key] === item[key]) ? old : item\n next.set(item.id, keep)\n return keep\n })\n drawnById = next\n return list\n})\nwatch(\n () => tier.value.name,\n (name, old) => old !== undefined && emit('breakpointChange', name),\n)\n/** The layout while a move or resize is under way; shown instead of `settled`. */\nconst preview = shallowRef<readonly GridItem[] | null>(null)\nconst layout = computed(() => preview.value ?? settled.value)\nconst gaps = computed<[number, number]>(() =>\n Array.isArray(props.gap) ? props.gap : [props.gap, props.gap],\n)\nconst handles = computed<GridResizeHandle[]>(() => {\n const list: GridResizeHandle[] =\n props.resize === false ? [] : props.resize === true ? ['e', 's', 'se'] : props.resize\n // one column: only the height can change\n return options.value.columns === 1 ? list.filter((edge) => edge !== 'e') : list\n})\n\ninterface Session {\n id: string\n kind: 'move' | 'resize' | 'drop'\n source: 'pointer' | 'keyboard'\n /** The layout when it started; pointer moves are always worked out from it. */\n base: readonly GridItem[]\n /** The item as it was. */\n item: GridItem\n}\n\ninterface PointerSession extends Session {\n source: 'pointer'\n edge: GridResizeHandle | null\n el: HTMLElement\n startX: number\n startY: number\n scroller: Element | null\n scrollLeft: number\n scrollTop: number\n /** The scroller's box on screen, measured once. */\n view: { top: number; bottom: number }\n col: number\n colStep: number\n rowStep: number\n /** The item's box when it started, in px from the grid's corner. */\n left: number\n top: number\n width: number\n height: number\n /** The cell or size last worked out, so pointer moves within one cell do nothing. */\n cell: string\n /** For a drop: what `startDrop` was given. */\n drop?: { item: GridDropItem; data: unknown }\n}\n\nconst session = shallowRef<Session | PointerSession | null>(null)\nconst settling = ref(false)\nlet settleTimer: ReturnType<typeof setTimeout> | undefined\n\nconst isPointer = (s: Session | null): s is PointerSession => s?.source === 'pointer'\n\n/**\n * Items as drawn: the one under the pointer keeps its old place, and\n * follows the pointer from there; one being dropped in shows only as the\n * place it would take.\n */\nconst drawn = computed(() => {\n const s = session.value\n if (!isPointer(s)) return layout.value\n if (s.kind === 'drop') return layout.value.filter((item) => item.id !== s.id)\n return layout.value.map((item) => (item.id === s.id ? s.item : item))\n})\n/** Where the item under the pointer would land. */\nconst ghost = computed(() => {\n const s = session.value\n return isPointer(s) ? layout.value.find((item) => item.id === s.id) : undefined\n})\n\nfunction stateOf(id: string) {\n const s = session.value\n if (s?.id !== id) return 'idle'\n return s.kind === 'resize' ? 'resizing' : 'dragging'\n}\n\nconst rootStyle = computed(() => ({\n '--dou-grid-columns': options.value.columns,\n '--dou-grid-row': `${props.rowHeight}px`,\n '--dou-grid-gap-x': `${gaps.value[0]}px`,\n '--dou-grid-gap-y': `${gaps.value[1]}px`,\n '--dou-grid-rows': layoutHeight(layout.value),\n}))\n\nconst cellStyle = (item: GridItem) => ({\n '--dou-grid-x': item.x,\n '--dou-grid-y': item.y,\n '--dou-grid-w': item.w,\n '--dou-grid-h': item.h,\n})\n\nconst labelOf = (item: GridItem) => props.itemLabel?.(item) ?? item.id\n/** Where an item is, read out: its cell and size, or with one column its place in the list. */\nfunction positionText(item: GridItem, items: readonly GridItem[]) {\n if (options.value.columns > 1)\n return format(messages.value.position, { x: item.x + 1, y: item.y + 1, w: item.w, h: item.h })\n let n = 1\n for (const other of items) if (other.y < item.y) n++\n return format(messages.value.order, { n, count: items.length })\n}\n\n// ---------------------------------------------------------------------------\n// Announcements\n\nconst live = ref('')\nfunction say(text: string) {\n // cleared first, so the same words twice are read twice\n live.value = ''\n requestAnimationFrame(() => (live.value = text))\n}\n\n// ---------------------------------------------------------------------------\n// Focus: one item is in the tab order, the arrow keys move between them\n\nconst focusId = ref<string>()\nconst firstId = computed(() => {\n let first: GridItem | undefined\n for (const item of settled.value)\n if (!first || item.y < first.y || (item.y === first.y && item.x < first.x)) first = item\n return first?.id\n})\nconst tabbableId = computed(() =>\n focusId.value && settled.value.some((item) => item.id === focusId.value)\n ? focusId.value\n : firstId.value,\n)\nconst tabindexOf = (item: GridItem) =>\n props.editable ? (item.id === tabbableId.value ? 0 : -1) : undefined\n\nfunction itemElement(id: string) {\n for (const child of root.value?.children ?? [])\n if (child instanceof HTMLElement && child.dataset.gridId === id) return child\n return undefined\n}\n\n/** The item element an event happened on: a direct child of this grid, not of a grid inside an item. */\nfunction ownItem(target: EventTarget | null) {\n const el = target instanceof Element ? target.closest<HTMLElement>('[data-grid-id]') : null\n return el && el.parentElement === root.value ? el : null\n}\n\nfunction onFocusIn(event: FocusEvent) {\n const el = ownItem(event.target)\n if (el) focusId.value = el.dataset.gridId\n}\n\n/** The nearest item in the direction of an arrow key. */\nfunction neighbour(from: GridItem, dx: number, dy: number) {\n const cx = from.x + from.w / 2\n const cy = from.y + from.h / 2\n let best: GridItem | undefined\n let bestScore = Infinity\n for (const item of settled.value) {\n if (item === from) continue\n const ix = item.x + item.w / 2 - cx\n const iy = item.y + item.h / 2 - cy\n const along = dx ? ix * dx : iy * dy\n if (along <= 0) continue\n const across = Math.abs(dx ? iy : ix)\n const score = along + across * 2\n if (score < bestScore) [best, bestScore] = [item, score]\n }\n return best\n}\n\n// ---------------------------------------------------------------------------\n// Ending a move or resize\n\nfunction commit(next: readonly GridItem[] | null, s: Session, reason: GridChangeReason) {\n if (!next || (next.length === s.base.length && next.every((item, i) => item === s.base[i])))\n return false\n const breakpoint = tier.value.name\n const drop = (s as PointerSession).drop\n let added: GridItem | undefined\n if (isMain.value) {\n const items = next.slice()\n added = drop && items.find((other) => other.id === s.id)\n model.value = items\n emit('change', { items, id: s.id, reason, breakpoint })\n } else {\n // a narrower breakpoint keeps its own places; a new item also joins the\n // widest layout, in the first spot there that fits its own size\n if (drop) {\n const main = mainLayout.value\n const wide = optionsOf(tiers.value[0]!)\n const fitted = fitItem({ ...drop.item, x: 0, y: 0 }, wide.columns)\n added = { ...fitted, ...findPlace(main, fitted, wide) }\n model.value = [...main, added]\n }\n layouts.value = {\n ...layouts.value,\n [breakpoint]: next.map(({ id, x, y, w, h }) => ({ id, x, y, w, h })),\n }\n emit('change', { items: next.slice(), id: s.id, reason, breakpoint })\n }\n if (drop && added) emit('drop', { item: added, data: drop.data, breakpoint })\n return true\n}\n\nfunction settle() {\n settling.value = true\n clearTimeout(settleTimer)\n settleTimer = setTimeout(() => (settling.value = false), SETTLE)\n}\n\nfunction finish(keep: boolean) {\n const s = session.value\n if (!s) return\n const next = preview.value\n if (isPointer(s)) stopPointer(s)\n session.value = null\n preview.value = null\n settle()\n const reason = s.source === 'keyboard' ? 'keyboard' : s.kind\n const changed = keep && commit(next, s, reason)\n emit('dragEnd', { id: s.id, kind: s.kind, cancelled: !keep || (s.kind === 'drop' && !changed) })\n // the keyboard hears every outcome; the pointer only a change, or a drag\n // undone (a new item that wasn't dropped has nowhere to go back to)\n if (s.source !== 'keyboard' && (keep || s.kind === 'drop') && !changed) return\n const items = (changed ? next : s.base) ?? s.base\n const item = items.find((other) => other.id === s.id) ?? s.item\n const label = labelOf(item)\n say(\n changed || (keep && s.source === 'keyboard')\n ? format(messages.value.dropped, { item: label, position: positionText(item, items) })\n : format(messages.value.cancelled, { item: label }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Pointer\n\ninterface Press {\n event: PointerEvent\n el: HTMLElement\n id: string\n kind: 'move' | 'resize' | 'drop'\n edge: GridResizeHandle | null\n hold: PointerHold\n timer?: ReturnType<typeof setTimeout>\n /** For a drop: the new item and its data. */\n drop?: { item: GridDropItem; data: unknown }\n}\n\nlet press: Press | null = null\nlet pointer = { x: 0, y: 0 }\nlet frame = 0\n\nconst NO_DRAG =\n 'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"], [data-dou-nodrag]'\n\nfunction onPointerDown(event: PointerEvent) {\n if (!props.editable || session.value || press || !event.isPrimary) return\n if (event.pointerType === 'mouse' && event.button !== 0) return\n const target = event.target as Element\n const el = ownItem(target)\n const id = el?.dataset.gridId\n const item = id === undefined ? undefined : settled.value.find((other) => other.id === id)\n if (!el || !id || !item || item.locked) return\n const handle = target.closest<HTMLElement>('[data-grid-resize]')\n let kind: Press['kind'] = 'move'\n let edge: GridResizeHandle | null = null\n if (handle && handle.parentElement === el) {\n kind = 'resize'\n edge = handle.dataset.gridResize as GridResizeHandle\n } else {\n if (props.handle) {\n const part = target.closest('[data-dou-grid-handle]')\n if (!part || part.closest('[data-grid-id]') !== el) return\n }\n if (target.closest(NO_DRAG)) return\n }\n listen({ event, el, id, kind, edge, hold: holdPointer(event, root.value) })\n}\n\n/** Follows a press until it becomes a drag (or a scroll, or nothing). */\nfunction listen(next: Press) {\n const { event } = next\n press = next\n pointer = { x: event.clientX, y: event.clientY }\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', onPointerCancel)\n if (next.kind === 'resize') {\n // a handle does nothing else, so there is nothing to wait for\n event.preventDefault()\n begin()\n } else if (event.pointerType !== 'mouse') {\n next.timer = setTimeout(begin, HOLD_DELAY)\n }\n}\n\nfunction startDrop(event: PointerEvent, item: GridDropItem, data?: unknown) {\n if (!props.editable || session.value || press || !event.isPrimary || !root.value) return\n if (event.pointerType === 'mouse' && event.button !== 0) return\n // an id already on the grid would replace that item\n if (settled.value.some((other) => other.id === item.id)) return\n listen({\n event,\n el: root.value,\n id: item.id,\n kind: 'drop',\n edge: null,\n hold: holdPointer(event, root.value),\n drop: { item, data },\n })\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!press || event.pointerId !== press.event.pointerId) return\n pointer = { x: event.clientX, y: event.clientY }\n if (!session.value) {\n const distance = Math.hypot(pointer.x - press.event.clientX, pointer.y - press.event.clientY)\n if (press.event.pointerType === 'mouse') {\n if (distance >= DRAG_DISTANCE) begin()\n } else if (distance > HOLD_SLOP) {\n // the finger is scrolling the page\n dropPress()\n }\n return\n }\n track()\n}\n\nfunction onPointerUp(event: PointerEvent) {\n if (!press || event.pointerId !== press.event.pointerId) return\n if (session.value) finish(true)\n else dropPress()\n}\n\nfunction onPointerCancel(event: PointerEvent) {\n if (!press || event.pointerId !== press.event.pointerId) return\n if (session.value) finish(false)\n else dropPress()\n}\n\nfunction onWindowKeydown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || !isPointer(session.value)) return\n event.preventDefault()\n event.stopPropagation()\n finish(false)\n}\n\nfunction onScroll() {\n if (isPointer(session.value)) track()\n}\n\nfunction dropPress() {\n if (!press) return\n clearTimeout(press.timer)\n press.hold.release()\n press = null\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', onPointerCancel)\n}\n\nfunction scrollParent(el: Element): Element | null {\n for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {\n const { overflowY } = getComputedStyle(node)\n if (/(auto|scroll|overlay)/.test(overflowY) && node.scrollHeight > node.clientHeight)\n return node\n }\n return null\n}\n\nconst scrollOf = (scroller: Element | null) =>\n scroller\n ? { left: scroller.scrollLeft, top: scroller.scrollTop }\n : { left: window.scrollX, top: window.scrollY }\n\nfunction begin() {\n const p = press\n const el = root.value\n if (!p || !el || session.value) return\n clearTimeout(p.timer)\n const item = p.drop\n ? fitItem({ ...p.drop.item, x: 0, y: 0 }, options.value.columns)\n : settled.value.find((other) => other.id === p.id)\n if (!item) return dropPress()\n const rect = el.getBoundingClientRect()\n const [gx, gy] = gaps.value\n const columns = options.value.columns\n const col = (rect.width - gx * (columns - 1)) / columns\n const scroller = scrollParent(el)\n const scroll = scrollOf(scroller)\n const box = scroller?.getBoundingClientRect()\n session.value = {\n id: p.id,\n kind: p.kind,\n source: 'pointer',\n edge: p.edge,\n base: settled.value,\n item,\n el: p.el,\n // a drop measures from the grid's corner, a move from where the press was\n startX: p.drop ? rect.left : p.event.clientX,\n startY: p.drop ? rect.top : p.event.clientY,\n scroller,\n scrollLeft: scroll.left,\n scrollTop: scroll.top,\n view: box ? { top: box.top, bottom: box.bottom } : { top: 0, bottom: window.innerHeight },\n col,\n colStep: col + gx,\n rowStep: props.rowHeight + gy,\n left: item.x * (col + gx),\n top: item.y * (props.rowHeight + gy),\n width: item.w * col + (item.w - 1) * gx,\n height: item.h * props.rowHeight + (item.h - 1) * gy,\n cell: '',\n drop: p.drop,\n }\n preview.value = settled.value\n p.hold.grab()\n if (p.event.pointerType === 'mouse') {\n try {\n // the grabbing cursor follows the pointer outside the grid\n el.setPointerCapture(p.event.pointerId)\n } catch {\n // the pointer is already gone\n }\n }\n window.addEventListener('keydown', onWindowKeydown, true)\n window.addEventListener('scroll', onScroll, { capture: true, passive: true })\n emit('dragStart', { id: p.id, kind: p.kind })\n frame = requestAnimationFrame(autoscroll)\n track()\n}\n\nfunction stopPointer(s: PointerSession) {\n cancelAnimationFrame(frame)\n for (const name of ['dx', 'dy', 'dw', 'dh']) s.el.style.removeProperty(`--dou-grid-${name}`)\n window.removeEventListener('keydown', onWindowKeydown, true)\n window.removeEventListener('scroll', onScroll, true)\n const touch = press?.event.pointerType !== 'mouse'\n dropPress()\n // the click that ends a drag isn't a click on whatever is under the pointer\n window.addEventListener('click', swallow, { capture: true, once: true })\n setTimeout(() => window.removeEventListener('click', swallow, true), touch ? 400 : 0)\n}\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))\n/** The length of `n` cells of `size` with `gap` between them. */\nconst span = (n: number, size: number, gap: number) => n * size + (n - 1) * gap\nfunction swallow(event: Event) {\n event.stopPropagation()\n event.preventDefault()\n}\n\n/** Follows the pointer: moves the item with it, and works out the layout when it reaches another cell. */\nfunction track() {\n const s = session.value\n if (!isPointer(s)) return\n const scroll = scrollOf(s.scroller)\n const dx = pointer.x - s.startX + scroll.left - s.scrollLeft\n const dy = pointer.y - s.startY + scroll.top - s.scrollTop\n const { columns } = options.value\n const [gx, gy] = gaps.value\n const item = s.item\n if (s.kind === 'drop') {\n // dx, dy: the pointer from the grid's corner; the item is held by its middle\n const height = layoutHeight(s.base) * s.rowStep\n const over =\n dx > -s.colStep / 2 &&\n dx < s.colStep * columns &&\n dy > -s.rowStep / 2 &&\n dy < height + s.rowStep * (item.h + 1)\n const x = clamp(Math.round((dx - s.width / 2) / s.colStep), 0, columns - item.w)\n const y = Math.max(0, Math.round((dy - s.height / 2) / s.rowStep))\n const cell = over ? `${x},${y}` : 'out'\n if (cell === s.cell) return\n s.cell = cell\n preview.value = over\n ? moveItem([...s.base, { ...item, x, y }], s.id, { x, y }, options.value)\n : s.base\n return\n }\n if (s.kind === 'move') {\n s.el.style.setProperty('--dou-grid-dx', `${dx}px`)\n s.el.style.setProperty('--dou-grid-dy', `${dy}px`)\n const x = clamp(Math.round((s.left + dx) / s.colStep), 0, columns - item.w)\n const y = Math.max(0, Math.round((s.top + dy) / s.rowStep))\n const cell = `${x},${y}`\n if (cell === s.cell) return\n s.cell = cell\n preview.value = moveItem(s.base, s.id, { x, y }, options.value)\n return\n }\n // resizing: the box follows the pointer within the item's limits, the size snaps to cells\n const maxW = Math.min(columns - item.x, item.maxW ?? Infinity)\n const minW = Math.min(maxW, Math.max(1, item.minW ?? 1))\n const maxH = item.maxH ?? Infinity\n const minH = Math.min(maxH, Math.max(1, item.minH ?? 1))\n const width =\n s.edge === 's' ? s.width : clamp(s.width + dx, span(minW, s.col, gx), span(maxW, s.col, gx))\n const height =\n s.edge === 'e'\n ? s.height\n : clamp(s.height + dy, span(minH, props.rowHeight, gy), span(maxH, props.rowHeight, gy))\n s.el.style.setProperty('--dou-grid-dw', `${width - s.width}px`)\n s.el.style.setProperty('--dou-grid-dh', `${height - s.height}px`)\n const w = Math.round((width + gx) / s.colStep)\n const h = Math.round((height + gy) / s.rowStep)\n const cell = `${w}x${h}`\n if (cell === s.cell) return\n s.cell = cell\n preview.value = resizeItem(s.base, s.id, { w, h }, options.value)\n}\n\nfunction autoscroll() {\n const s = session.value\n if (!isPointer(s)) return\n frame = requestAnimationFrame(autoscroll)\n const { top, bottom } = s.view\n let speed = 0\n if (pointer.y < top + EDGE) speed = -Math.min(1, (top + EDGE - pointer.y) / EDGE)\n else if (pointer.y > bottom - EDGE) speed = Math.min(1, (pointer.y - bottom + EDGE) / EDGE)\n if (!speed) return\n // the scroll event that follows moves the item along\n if (s.scroller) s.scroller.scrollTop += speed * EDGE_SPEED\n else window.scrollBy(0, speed * EDGE_SPEED)\n}\n\n// ---------------------------------------------------------------------------\n// Keyboard\n\nconst ARROWS: Record<string, [number, number]> = {\n ArrowLeft: [-1, 0],\n ArrowRight: [1, 0],\n ArrowUp: [0, -1],\n ArrowDown: [0, 1],\n}\n\nfunction onKeyDown(event: KeyboardEvent) {\n if (!props.editable || event.target !== ownItem(event.target)) return\n const id = (event.target as HTMLElement).dataset.gridId!\n const s = session.value\n const arrow = ARROWS[event.key]\n if (s?.source === 'keyboard' && s.id === id) {\n if (arrow && !event.altKey && !event.metaKey && !event.ctrlKey) {\n const current = preview.value ?? s.base\n const next = event.shiftKey\n ? resizeItemBy(current, id, arrow[0], arrow[1], options.value)\n : moveItemBy(current, id, arrow[0], arrow[1], options.value)\n preview.value = next\n const item = next.find((other) => other.id === id)\n if (item) say(positionText(item, next))\n } else if (event.key === 'Enter' || event.key === ' ') {\n finish(true)\n } else if (event.key === 'Escape') {\n finish(false)\n } else if (event.key !== 'Tab') {\n return\n }\n if (event.key !== 'Tab') {\n event.preventDefault()\n event.stopPropagation()\n }\n return\n }\n if (s) return\n const item = settled.value.find((other) => other.id === id)\n if (!item) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (item.locked) return say(format(messages.value.locked, { item: labelOf(item) }))\n session.value = { id, kind: 'move', source: 'keyboard', base: settled.value, item }\n preview.value = settled.value\n emit('dragStart', { id, kind: 'move' })\n say(\n format(messages.value.grabbed, {\n item: labelOf(item),\n position: positionText(item, settled.value),\n }),\n )\n } else if (arrow && !event.shiftKey && !event.altKey && !event.metaKey && !event.ctrlKey) {\n const next = neighbour(item, arrow[0], arrow[1])\n if (!next) return\n event.preventDefault()\n itemElement(next.id)?.focus()\n }\n}\n\n/** Leaving a picked-up item puts it back. */\nfunction onFocusOut(event: FocusEvent) {\n const s = session.value\n if (s?.source !== 'keyboard') return\n const el = ownItem(event.target)\n if (el?.dataset.gridId === s.id && !el.contains(event.relatedTarget as Node | null)) finish(false)\n}\n\n// ---------------------------------------------------------------------------\n\nwatch(\n () => props.editable,\n (editable) => {\n if (!editable) {\n finish(false)\n dropPress()\n }\n },\n)\n\nonBeforeUnmount(() => {\n finish(false)\n dropPress()\n clearTimeout(settleTimer)\n})\n\ndefineExpose<GridLayoutExpose>({\n startDrop,\n get breakpoint() {\n return tier.value.name\n },\n focusItem(id) {\n const el = itemElement(id)\n if (!el) return\n if (props.editable) el.focus()\n else\n el.querySelector<HTMLElement>(\n 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n )?.focus()\n },\n scrollToItem(id, scrollOptions) {\n itemElement(id)?.scrollIntoView({ block: 'nearest', inline: 'nearest', ...scrollOptions })\n },\n})\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"dou-grid-layout\"\n :class=\"{\n 'is-editable': editable,\n 'has-handle': handle,\n 'is-arranging': !!session || settling,\n 'is-pointer-dragging': session?.source === 'pointer',\n 'is-resizing': session?.kind === 'resize' && session.source === 'pointer',\n }\"\n :style=\"rootStyle\"\n :role=\"editable ? 'group' : undefined\"\n :aria-label=\"label ?? (editable ? messages.layout : undefined)\"\n @pointerdown=\"onPointerDown\"\n @keydown=\"onKeyDown\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <div\n v-if=\"ghost\"\n class=\"dou-grid-layout__placeholder\"\n :style=\"cellStyle(ghost)\"\n aria-hidden=\"true\"\n >\n <slot name=\"placeholder\" :item=\"ghost\" />\n </div>\n <GridItemView\n v-for=\"item in drawn\"\n :key=\"item.id\"\n :item=\"item\"\n :state=\"stateOf(item.id)\"\n :editable=\"editable\"\n :tabindex=\"tabindexOf(item)\"\n :label=\"labelOf(item)\"\n :role-description=\"messages.item\"\n :handles=\"handles\"\n />\n <span v-if=\"editable\" :id=\"hintId\" hidden>{{ messages.itemHint }}</span>\n <div v-if=\"editable\" class=\"dou-grid-layout__live\" aria-live=\"assertive\" aria-atomic=\"true\">\n {{ live }}\n </div>\n </div>\n</template>\n"],"mappings":""}
|