@simai-studio/ui 5.20261023.214 → 5.20261025.207
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/README.md +4 -4
- package/distr/component/button-group/css/button-group.min.css +1 -1
- package/distr/core/contracts/public-variables.v1.json +88 -0
- package/distr/core/contracts/third-party-notices.v1.json +1 -1
- package/distr/core/css/core.css +3 -2
- package/distr/core/js/composition/overlay.mjs +472 -24
- package/distr/core/js/composition/sortable-inserter.mjs +534 -0
- package/distr/core/js/core-loader.js +1 -1
- package/distr/core/js/core-loader.min.js +1 -1
- package/distr/core/js/core-rules.js +242 -185
- package/distr/core/js/core-rules.min.js +1 -1
- package/distr/core/js/open-surface-mark.js +88 -0
- package/distr/core/js/template-utilities.generated.json +3 -0
- package/distr/rule/js/rule.min.js +1 -1
- package/distr/rule/rule.json +47 -1
- package/distr/smart/color/css/color.min.css +1 -0
- package/distr/smart/color/js/color.min.js +36 -17
- package/distr/smart/color/smart.manifest.json +45 -4
- package/distr/smart/color/template/default.js +13 -2
- package/distr/smart/composition-overlay/css/composition-overlay.min.css +1 -1
- package/distr/smart/composition-overlay/js/composition-overlay.min.js +1 -1
- package/distr/smart/composition-overlay/smart.manifest.json +178 -11
- package/distr/smart/dropdown/css/dropdown.min.css +1 -0
- package/distr/smart/dropdown/js/dropdown.min.js +41 -21
- package/distr/smart/dropdown/smart.manifest.json +62 -3
- package/distr/smart/dropdown/template/default.js +5 -1
- package/distr/smart/field-renderers/js/field-renderers.min.js +54 -42
- package/distr/smart/image-field/css/image-field.min.css +1 -0
- package/distr/smart/image-field/js/image-field.min.js +62 -0
- package/distr/smart/image-field/smart.manifest.json +519 -0
- package/distr/smart/image-field/studio.fixture.json +15 -0
- package/distr/smart/image-field/studio.scenario.json +22 -0
- package/distr/smart/image-field/template/default.js +125 -0
- package/distr/smart/inputs/css/inputs.min.css +1 -0
- package/distr/smart/inputs/js/inputs.min.js +93 -75
- package/distr/smart/inputs/smart.manifest.json +62 -3
- package/distr/smart/inputs/template/default.js +8 -2
- package/distr/smart/sortable/css/sortable.min.css +1 -1
- package/distr/smart/sortable/js/sortable.min.js +1 -1
- package/distr/smart/sortable/smart.manifest.json +101 -14
- package/distr/smart/switch/css/switch.min.css +1 -0
- package/distr/smart/switch/js/switch.min.js +31 -13
- package/distr/smart/switch/smart.manifest.json +62 -3
- package/distr/smart/switch/template/default.js +2 -0
- package/distr/smart/textarea/css/textarea.min.css +1 -0
- package/distr/smart/textarea/js/textarea.min.js +51 -32
- package/distr/smart/textarea/smart.manifest.json +62 -3
- package/distr/smart/textarea/template/default.js +8 -1
- package/package-manifest.json +50 -39
- package/package.json +5 -4
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// The insertion point of sf-sortable and the inserter it opens. A block editor
|
|
2
|
+
// asks for a "+" exactly where the next block goes -- between two rows, before
|
|
3
|
+
// the first, after the last -- that opens a searchable list of the kinds of
|
|
4
|
+
// thing that can go there (simai/ui-source#21; ui-mail redesign F4, 2026-10-22:
|
|
5
|
+
// "Добавить" always appended to the end, so every insert needed a later move).
|
|
6
|
+
// It is the same need in every editor built on a list -- mail, Docara studio,
|
|
7
|
+
// admin forms -- so it belongs to the list, not to each editor.
|
|
8
|
+
//
|
|
9
|
+
// The component reports the choice and creates nothing: the host hears
|
|
10
|
+
// sf-sortable-insert with the place and the kind, builds the item and puts it
|
|
11
|
+
// in the list, exactly as it applies sf-sortable-intent. The inserter is the
|
|
12
|
+
// Framework's standard menu (sf-context-menu, opened with openFor and its own
|
|
13
|
+
// local search), so its rows, its keys, its top layer and its Escape are the
|
|
14
|
+
// ones every other menu has; nothing here draws a second popover.
|
|
15
|
+
//
|
|
16
|
+
// It is opt-in: a list without inserter items gets a controller that draws
|
|
17
|
+
// nothing and listens to nothing, so every list written before is the list it
|
|
18
|
+
// was, its drag included. The drag module itself (sortable.mjs) is not changed:
|
|
19
|
+
// the element is extended here (withSortableInserter).
|
|
20
|
+
import { pageText } from '../page-language.js';
|
|
21
|
+
import { activeSortableSession } from './sortable.mjs';
|
|
22
|
+
|
|
23
|
+
export const SORTABLE_INSERT_EVENT = 'sf-sortable-insert';
|
|
24
|
+
|
|
25
|
+
const ITEM = '[data-sf-sortable-item]';
|
|
26
|
+
|
|
27
|
+
const MESSAGES = {
|
|
28
|
+
ru: {
|
|
29
|
+
point: 'Вставить: позиция {index} из {total}',
|
|
30
|
+
menu: 'Вставить',
|
|
31
|
+
search: 'Найти',
|
|
32
|
+
empty: 'Ничего не найдено',
|
|
33
|
+
inserted: 'Вставлено: {item}, позиция {index} в «{list}».',
|
|
34
|
+
hint: 'Плюс — вставить после.',
|
|
35
|
+
},
|
|
36
|
+
en: {
|
|
37
|
+
point: 'Insert: position {index} of {total}',
|
|
38
|
+
menu: 'Insert',
|
|
39
|
+
search: 'Find',
|
|
40
|
+
empty: 'Nothing found',
|
|
41
|
+
inserted: 'Inserted {item} at position {index} in “{list}”.',
|
|
42
|
+
hint: 'Plus to insert after.',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const format = (template, values) => template.replace(/\{(\w+)\}/gu, (_, key) => String(values[key] ?? ''));
|
|
47
|
+
const text = (key) => pageText(MESSAGES, key, undefined, 'sf-sortable');
|
|
48
|
+
|
|
49
|
+
function items(list) {
|
|
50
|
+
return [...list.querySelectorAll(ITEM)].filter((item) => item.closest('sf-sortable') === list);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listName(list) {
|
|
54
|
+
return list.getAttribute('label') || list.getAttribute('aria-label') || list.getAttribute('group') || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The same test the drag uses: two items side by side on one row make a grid,
|
|
58
|
+
// and a grid is read row by row.
|
|
59
|
+
function isWrapped(all) {
|
|
60
|
+
for (let position = 1; position < all.length; position += 1) {
|
|
61
|
+
const previous = all[position - 1].getBoundingClientRect();
|
|
62
|
+
const current = all[position].getBoundingClientRect();
|
|
63
|
+
if (current.top < previous.bottom - 1 && current.left >= previous.right - 1) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Where the place `index` is, in viewport coordinates: a line across the list
|
|
70
|
+
* (axis "y", a vertical list), down it (axis "x", a horizontal one), or down the
|
|
71
|
+
* side of one cell of a grid. `at` is the line's position on its axis -- the
|
|
72
|
+
* middle of the gap between two items, so the point sits on the gap and not on
|
|
73
|
+
* either item -- and `start`/`end` its extent across.
|
|
74
|
+
*/
|
|
75
|
+
export function insertionGeometry(list, all, index) {
|
|
76
|
+
const box = list.getBoundingClientRect();
|
|
77
|
+
const inner = {
|
|
78
|
+
left: box.left + list.clientLeft, top: box.top + list.clientTop,
|
|
79
|
+
width: list.clientWidth, height: list.clientHeight,
|
|
80
|
+
};
|
|
81
|
+
const horizontal = list.getAttribute('orientation') === 'horizontal';
|
|
82
|
+
const before = all[index]?.getBoundingClientRect();
|
|
83
|
+
const after = all[index - 1]?.getBoundingClientRect();
|
|
84
|
+
if (!horizontal && isWrapped(all)) {
|
|
85
|
+
const cell = before ?? after;
|
|
86
|
+
if (!cell) return { axis: 'y', at: inner.top + inner.height / 2, start: inner.left, end: inner.left + inner.width };
|
|
87
|
+
const sameRow = before && after && after.top < before.bottom - 1 && before.left >= after.right - 1;
|
|
88
|
+
const at = before ? (sameRow ? (after.right + before.left) / 2 : before.left) : after.right;
|
|
89
|
+
return { axis: 'x', at, start: cell.top, end: cell.bottom };
|
|
90
|
+
}
|
|
91
|
+
if (horizontal) {
|
|
92
|
+
const at = before && after ? (after.right + before.left) / 2 : before ? before.left : after ? after.right : inner.left + inner.width / 2;
|
|
93
|
+
return { axis: 'x', at, start: inner.top, end: inner.top + inner.height };
|
|
94
|
+
}
|
|
95
|
+
const at = before && after ? (after.bottom + before.top) / 2 : before ? before.top : after ? after.bottom : inner.top + inner.height / 2;
|
|
96
|
+
return { axis: 'y', at, start: inner.left, end: inner.left + inner.width };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The place nearest a pointer, and how far it is: across a vertical list only
|
|
101
|
+
* the height counts, along a horizontal one only the width; a grid's place is
|
|
102
|
+
* a side of a cell and counts only on that cell's row.
|
|
103
|
+
*/
|
|
104
|
+
export function nearestInsertion(list, all, x, y) {
|
|
105
|
+
let best = null;
|
|
106
|
+
for (let index = 0; index <= all.length; index += 1) {
|
|
107
|
+
const place = insertionGeometry(list, all, index);
|
|
108
|
+
const along = place.axis === 'y' ? y : x;
|
|
109
|
+
const across = place.axis === 'y' ? x : y;
|
|
110
|
+
if (across < place.start || across > place.end) continue;
|
|
111
|
+
const distance = Math.abs(along - place.at);
|
|
112
|
+
if (!best || distance < best.distance) best = { index, distance, place };
|
|
113
|
+
}
|
|
114
|
+
return best;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toEntries(list) {
|
|
118
|
+
return list.inserterItems.flatMap((item) => {
|
|
119
|
+
if (!item || typeof item !== 'object') return [];
|
|
120
|
+
if (item.type === 'separator') return [{ type: 'separator' }];
|
|
121
|
+
if (item.type === 'heading') return [{ type: 'heading', text: String(item.label ?? '') }];
|
|
122
|
+
if (item.value === undefined || item.value === null) return [];
|
|
123
|
+
return [{
|
|
124
|
+
id: String(item.value), text: String(item.label ?? item.value),
|
|
125
|
+
icon: item.icon || undefined, caption: item.caption || undefined,
|
|
126
|
+
keywords: item.keywords || undefined, disabled: Boolean(item.disabled),
|
|
127
|
+
}];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The controller one list owns. connect/disconnect follow the list; refresh
|
|
133
|
+
* follows its inserter items; ensure follows a host redraw of its children.
|
|
134
|
+
*/
|
|
135
|
+
export function createSortableInserter(list) {
|
|
136
|
+
let connected = false;
|
|
137
|
+
let active = false;
|
|
138
|
+
let wiring = null;
|
|
139
|
+
let control = null;
|
|
140
|
+
let button = null;
|
|
141
|
+
let menu = null;
|
|
142
|
+
let menuIndex = 0;
|
|
143
|
+
// The place the point stands at, and why it is shown: a pointer near a gap,
|
|
144
|
+
// a focused row (its place after it), the point's own focus or its menu.
|
|
145
|
+
let point = 0;
|
|
146
|
+
let hover = null;
|
|
147
|
+
let rowFocus = null;
|
|
148
|
+
|
|
149
|
+
const document_ = () => globalThis.document;
|
|
150
|
+
const count = () => items(list).length;
|
|
151
|
+
const clamp = (index) => Math.max(0, Math.min(count(), index));
|
|
152
|
+
|
|
153
|
+
function build() {
|
|
154
|
+
control = document_().createElement('div');
|
|
155
|
+
control.className = 'sf-sortable-insert';
|
|
156
|
+
control.setAttribute('data-sf-sortable-insert', '');
|
|
157
|
+
// A native icon button of the Framework's smallest rung: its height, its
|
|
158
|
+
// glyph and its padding are the ladder's (1/3), and its look is the quiet
|
|
159
|
+
// surface one a row action has, so it reads the same on any canvas.
|
|
160
|
+
button = document_().createElement('button');
|
|
161
|
+
button.type = 'button';
|
|
162
|
+
button.className = 'sf-icon-button sf-icon-button--icon sf-icon-button--surface sf-icon-button--on-surface sf-icon-button--size-1/3 sf-sortable-insert-button';
|
|
163
|
+
button.setAttribute('aria-haspopup', 'menu');
|
|
164
|
+
button.setAttribute('aria-expanded', 'false');
|
|
165
|
+
const glyph = document_().createElement('i');
|
|
166
|
+
glyph.className = 'sf-icon';
|
|
167
|
+
glyph.setAttribute('aria-hidden', 'true');
|
|
168
|
+
glyph.textContent = 'add';
|
|
169
|
+
button.append(glyph);
|
|
170
|
+
control.append(button);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The point after the last item in the document, so it is the list's last tab
|
|
174
|
+
// stop: Tab from the last row reaches it and Tab from it leaves the list, and
|
|
175
|
+
// nothing is trapped. A host that appends its redrawn items after the
|
|
176
|
+
// component's own nodes would otherwise put the point in front of them.
|
|
177
|
+
function ensure() {
|
|
178
|
+
if (!active || !control) return;
|
|
179
|
+
const last = items(list).at(-1);
|
|
180
|
+
const misplaced = control.parentNode !== list
|
|
181
|
+
|| Boolean(last && (control.compareDocumentPosition(last) & globalThis.Node.DOCUMENT_POSITION_FOLLOWING));
|
|
182
|
+
if (misplaced) {
|
|
183
|
+
const focused = document_().activeElement === button;
|
|
184
|
+
list.append(control);
|
|
185
|
+
// Moving a focused node drops its focus; the person keeps their place.
|
|
186
|
+
if (focused) button.focus({ preventScroll: true });
|
|
187
|
+
}
|
|
188
|
+
// The rows moved under it: the place is measured again.
|
|
189
|
+
place();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function shownReason() {
|
|
193
|
+
if (activeSortableSession()) return null;
|
|
194
|
+
if (menu?.isOpen) return 'menu';
|
|
195
|
+
if (document_().activeElement === button) return 'focus';
|
|
196
|
+
if (hover !== null) return 'hover';
|
|
197
|
+
if (rowFocus !== null) return 'row';
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function place() {
|
|
202
|
+
if (!active || !control) return;
|
|
203
|
+
const reason = shownReason();
|
|
204
|
+
if (reason === 'hover') point = hover;
|
|
205
|
+
else if (reason === 'row') point = rowFocus;
|
|
206
|
+
point = clamp(point);
|
|
207
|
+
const all = items(list);
|
|
208
|
+
const geometry = insertionGeometry(list, all, point);
|
|
209
|
+
const box = list.getBoundingClientRect();
|
|
210
|
+
const originX = box.left + list.clientLeft - list.scrollLeft;
|
|
211
|
+
const originY = box.top + list.clientTop - list.scrollTop;
|
|
212
|
+
control.dataset.axis = geometry.axis;
|
|
213
|
+
if (geometry.axis === 'y') {
|
|
214
|
+
Object.assign(control.style, { left: `${geometry.start - originX}px`, top: `${geometry.at - originY}px`, width: `${geometry.end - geometry.start}px`, height: '0px' });
|
|
215
|
+
} else {
|
|
216
|
+
Object.assign(control.style, { left: `${geometry.at - originX}px`, top: `${geometry.start - originY}px`, width: '0px', height: `${geometry.end - geometry.start}px` });
|
|
217
|
+
}
|
|
218
|
+
control.toggleAttribute('data-shown', Boolean(reason));
|
|
219
|
+
control.dataset.index = String(point);
|
|
220
|
+
button.setAttribute('aria-label', format(text('point'), { index: point + 1, total: all.length + 1 }));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Pointer: the point comes to the gap the pointer is near, within half the
|
|
224
|
+
// point's own height of it -- near enough to mean the gap, not the row.
|
|
225
|
+
function onPointerMove(event) {
|
|
226
|
+
if (event.buttons || activeSortableSession() || menu?.isOpen) return;
|
|
227
|
+
if (control.contains(event.target)) return;
|
|
228
|
+
const zone = Math.max(button.offsetHeight, button.offsetWidth) / 2;
|
|
229
|
+
const nearest = nearestInsertion(list, items(list), event.clientX, event.clientY);
|
|
230
|
+
const next = nearest && nearest.distance <= zone ? nearest.index : null;
|
|
231
|
+
if (next === hover) return;
|
|
232
|
+
hover = next;
|
|
233
|
+
place();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function onPointerLeave() {
|
|
237
|
+
if (hover === null) return;
|
|
238
|
+
hover = null;
|
|
239
|
+
place();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A focused row shows the place after it: the place its "+" key opens, and
|
|
243
|
+
// the one a tap on a touch screen, which has no hover, can reach.
|
|
244
|
+
function onFocusIn(event) {
|
|
245
|
+
const item = event.target.closest?.(ITEM);
|
|
246
|
+
if (event.target === button) {
|
|
247
|
+
place();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
rowFocus = item && item.closest('sf-sortable') === list ? items(list).indexOf(item) + 1 : null;
|
|
251
|
+
place();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onFocusOut() {
|
|
255
|
+
globalThis.setTimeout(() => {
|
|
256
|
+
if (!connected) return;
|
|
257
|
+
if (!list.contains(document_().activeElement)) rowFocus = null;
|
|
258
|
+
place();
|
|
259
|
+
}, 0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The point's own keys: the arrows of the list's direction walk the places,
|
|
263
|
+
// Home and End go to the first and the last, and each step is said. Enter
|
|
264
|
+
// and Space are the button's own.
|
|
265
|
+
function onButtonKeyDown(event) {
|
|
266
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
267
|
+
const horizontal = list.getAttribute('orientation') === 'horizontal';
|
|
268
|
+
const rtl = globalThis.getComputedStyle(list).direction === 'rtl';
|
|
269
|
+
const steps = horizontal
|
|
270
|
+
? { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1, ArrowUp: -1, ArrowDown: 1 }
|
|
271
|
+
: { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -1, ArrowRight: 1 };
|
|
272
|
+
let next = null;
|
|
273
|
+
if (event.key in steps) next = point + steps[event.key];
|
|
274
|
+
else if (event.key === 'Home') next = 0;
|
|
275
|
+
else if (event.key === 'End') next = count();
|
|
276
|
+
if (next === null) return;
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
hover = null;
|
|
279
|
+
rowFocus = null;
|
|
280
|
+
point = clamp(next);
|
|
281
|
+
place();
|
|
282
|
+
list.announce?.(format(text('point'), { index: point + 1, total: count() + 1 }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onButtonClick(event) {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
if (menu?.isOpen) {
|
|
288
|
+
menu.close();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
open(point);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function standardMenu() {
|
|
295
|
+
if (menu?.isConnected) return menu;
|
|
296
|
+
const registry = globalThis.customElements;
|
|
297
|
+
if (!registry.get('sf-context-menu')) {
|
|
298
|
+
globalThis.SF?.Loader?.ensurePlugin?.('cl-context-menu');
|
|
299
|
+
await Promise.race([registry.whenDefined('sf-context-menu'), new Promise((done) => { globalThis.setTimeout(done, 3000); })]);
|
|
300
|
+
}
|
|
301
|
+
if (!registry.get('sf-context-menu')) return null;
|
|
302
|
+
menu = document_().createElement('sf-context-menu');
|
|
303
|
+
menu.setAttribute('popup', '');
|
|
304
|
+
menu.addEventListener('sf-context-menu-select', onSelect);
|
|
305
|
+
menu.addEventListener('sf-context-menu-close', () => { button?.setAttribute('aria-expanded', 'false'); place(); });
|
|
306
|
+
// Enter in the search takes the first row it left: whoever typed the name
|
|
307
|
+
// of what they want has chosen it. The menu's own Enter is a row's.
|
|
308
|
+
menu.addEventListener('keydown', (event) => {
|
|
309
|
+
if (event.key !== 'Enter' || event.defaultPrevented || event.target !== menu.searchField?.()) return;
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
menu.syncMenuItems?.();
|
|
312
|
+
menu.availableMenuItems?.()[0]?.click();
|
|
313
|
+
});
|
|
314
|
+
(list.closest('dialog[open]') || document_().body).append(menu);
|
|
315
|
+
return menu;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Opens the inserter for the place `index` (0 before the first item). */
|
|
319
|
+
async function open(index) {
|
|
320
|
+
if (!active || activeSortableSession()) return false;
|
|
321
|
+
point = clamp(index);
|
|
322
|
+
hover = null;
|
|
323
|
+
place();
|
|
324
|
+
const surface = await standardMenu();
|
|
325
|
+
if (!surface || !active) return false;
|
|
326
|
+
menuIndex = point;
|
|
327
|
+
surface.setAttribute('aria-label', listName(list) ? `${text('menu')}: ${listName(list)}` : text('menu'));
|
|
328
|
+
surface.search = { placeholder: text('search'), empty: text('empty') };
|
|
329
|
+
surface.entries = toEntries(list);
|
|
330
|
+
const opened = await surface.openFor(button, { side: 'block-end', align: 'start', focus: 'first' });
|
|
331
|
+
place();
|
|
332
|
+
return opened;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A choice is the host's to apply: it hears the place and the kind, and a
|
|
336
|
+
// refusal (preventDefault) leaves the inserter open for another choice. The
|
|
337
|
+
// menu is closed here rather than by itself, so a host that moved focus to
|
|
338
|
+
// the item it just inserted keeps it there.
|
|
339
|
+
function onSelect(event) {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
const entry = event.detail?.entry;
|
|
342
|
+
if (!entry || entry.id === undefined || entry.id === null) return;
|
|
343
|
+
const index = menuIndex;
|
|
344
|
+
const accepted = list.dispatchEvent(new globalThis.CustomEvent(SORTABLE_INSERT_EVENT, {
|
|
345
|
+
bubbles: true, composed: true, cancelable: true,
|
|
346
|
+
detail: Object.freeze({ index, item: entry.id, to: list.getAttribute('id') || list.getAttribute('group') || '' }),
|
|
347
|
+
}));
|
|
348
|
+
if (!accepted) return;
|
|
349
|
+
list.announce?.(format(text('inserted'), { item: entry.text ?? entry.id, index: index + 1, list: listName(list) }));
|
|
350
|
+
// The next insert goes after this one, so a run of choices builds in order.
|
|
351
|
+
point = index + 1;
|
|
352
|
+
const focusMoved = !menu.contains(document_().activeElement);
|
|
353
|
+
menu.close({ restoreFocus: !focusMoved });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function onItemKeyDown(event, item) {
|
|
357
|
+
if (!active || event.key !== '+' || event.altKey || event.ctrlKey || event.metaKey) return false;
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
open(items(list).indexOf(item) + 1);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function onSortableState() {
|
|
364
|
+
hover = null;
|
|
365
|
+
place();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mount() {
|
|
369
|
+
if (!control) build();
|
|
370
|
+
wiring = new globalThis.AbortController();
|
|
371
|
+
const options = { signal: wiring.signal };
|
|
372
|
+
list.setAttribute('data-sf-sortable-inserter', '');
|
|
373
|
+
list.addEventListener('pointermove', onPointerMove, options);
|
|
374
|
+
list.addEventListener('pointerleave', onPointerLeave, options);
|
|
375
|
+
list.addEventListener('focusin', onFocusIn, options);
|
|
376
|
+
list.addEventListener('focusout', onFocusOut, options);
|
|
377
|
+
list.addEventListener('sf-sortable-state', onSortableState, options);
|
|
378
|
+
button.addEventListener('keydown', onButtonKeyDown, options);
|
|
379
|
+
button.addEventListener('click', onButtonClick, options);
|
|
380
|
+
// The icon button's look and the glyph's font are component stylesheets:
|
|
381
|
+
// asked for here, and named as relations of the rule, so a page without
|
|
382
|
+
// them still gets them.
|
|
383
|
+
globalThis.SF?.Loader?.ensurePlugin?.('icon-buttons');
|
|
384
|
+
globalThis.SF?.Loader?.ensurePlugin?.('icons');
|
|
385
|
+
active = true;
|
|
386
|
+
ensure();
|
|
387
|
+
place();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function unmount() {
|
|
391
|
+
active = false;
|
|
392
|
+
wiring?.abort();
|
|
393
|
+
wiring = null;
|
|
394
|
+
menu?.close?.({ restoreFocus: false });
|
|
395
|
+
control?.remove();
|
|
396
|
+
list.removeAttribute('data-sf-sortable-inserter');
|
|
397
|
+
hover = null;
|
|
398
|
+
rowFocus = null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function refresh() {
|
|
402
|
+
const wanted = connected && list.inserterItems.length > 0;
|
|
403
|
+
if (wanted && !active) mount();
|
|
404
|
+
else if (!wanted && active) unmount();
|
|
405
|
+
else if (wanted && menu?.isOpen) menu.entries = toEntries(list);
|
|
406
|
+
list.updateInstructions?.();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
connect() { connected = true; refresh(); },
|
|
411
|
+
disconnect() {
|
|
412
|
+
connected = false;
|
|
413
|
+
if (active) unmount();
|
|
414
|
+
menu?.remove();
|
|
415
|
+
menu = null;
|
|
416
|
+
},
|
|
417
|
+
refresh,
|
|
418
|
+
ensure,
|
|
419
|
+
open,
|
|
420
|
+
onItemKeyDown,
|
|
421
|
+
instructions: () => (active ? text('hint') : ''),
|
|
422
|
+
get active() { return active; },
|
|
423
|
+
get index() { return point; },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* sf-sortable with an inserter: the drag element of sortable.mjs, extended.
|
|
429
|
+
* It is an extension rather than an edit of that module because the
|
|
430
|
+
* composition overlay's bundle carries sortable.mjs too (for the shared drag
|
|
431
|
+
* session) and has no use for an inserter; extending it here keeps the
|
|
432
|
+
* inserter in the sf-sortable bundle alone and the drag module as it was.
|
|
433
|
+
*/
|
|
434
|
+
export function withSortableInserter(Base) {
|
|
435
|
+
return class SfSortable extends Base {
|
|
436
|
+
static get observedAttributes() {
|
|
437
|
+
return [...(super.observedAttributes ?? []), 'inserter-items'];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
constructor() {
|
|
441
|
+
super();
|
|
442
|
+
this.inserterController = null;
|
|
443
|
+
this._inserterHint = '';
|
|
444
|
+
// Set before the element was defined, the value sat on the instance and
|
|
445
|
+
// would hide the accessor; it is taken over here, as sf-context-menu
|
|
446
|
+
// takes over its entries.
|
|
447
|
+
if (Object.prototype.hasOwnProperty.call(this, 'inserterItems')) {
|
|
448
|
+
const early = this.inserterItems;
|
|
449
|
+
delete this.inserterItems;
|
|
450
|
+
this._inserterItems = Array.isArray(early) ? early : null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// What the inserter offers: [{value, label, icon?, caption?, keywords?,
|
|
455
|
+
// disabled?}], as a property or as JSON in the inserter-items attribute.
|
|
456
|
+
// An empty list is no inserter, and the list is the list it was before.
|
|
457
|
+
get inserterItems() {
|
|
458
|
+
if (Array.isArray(this._inserterItems)) return this._inserterItems;
|
|
459
|
+
const raw = this.getAttribute('inserter-items');
|
|
460
|
+
if (!raw) return [];
|
|
461
|
+
try {
|
|
462
|
+
const parsed = JSON.parse(raw);
|
|
463
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
464
|
+
} catch {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
set inserterItems(value) {
|
|
470
|
+
this._inserterItems = Array.isArray(value) ? value : null;
|
|
471
|
+
this.inserterController?.refresh();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
attributeChangedCallback(name, previous, next) {
|
|
475
|
+
super.attributeChangedCallback?.(name, previous, next);
|
|
476
|
+
if (name === 'inserter-items' && previous !== next) this.inserterController?.refresh();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
connectedCallback() {
|
|
480
|
+
super.connectedCallback();
|
|
481
|
+
if (!this.inserterController) this.inserterController = createSortableInserter(this);
|
|
482
|
+
this.inserterController.connect();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
disconnectedCallback() {
|
|
486
|
+
this.inserterController?.disconnect();
|
|
487
|
+
super.disconnectedCallback();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// The instructions every item points at say the inserter's key while the
|
|
491
|
+
// list has one. The drag module writes its own sentence; the hint follows it.
|
|
492
|
+
updateInstructions() {
|
|
493
|
+
if (!this.instructions) return;
|
|
494
|
+
const current = this.instructions.textContent;
|
|
495
|
+
const own = this._inserterHint && current.endsWith(` ${this._inserterHint}`)
|
|
496
|
+
? current.slice(0, -(this._inserterHint.length + 1))
|
|
497
|
+
: current;
|
|
498
|
+
const hint = this.inserterController?.instructions() || '';
|
|
499
|
+
const next = hint ? `${own} ${hint}` : own;
|
|
500
|
+
this._inserterHint = hint;
|
|
501
|
+
// Written only when it changes: the list observes its own subtree, and a
|
|
502
|
+
// write would wake the observer that called this.
|
|
503
|
+
if (next !== current) this.instructions.textContent = next;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// A host redraw takes the component's nodes out with its own, or leaves the
|
|
507
|
+
// insertion point in front of the items it appends: both come back.
|
|
508
|
+
ensureAccessibilityNodes() {
|
|
509
|
+
super.ensureAccessibilityNodes();
|
|
510
|
+
this.updateInstructions();
|
|
511
|
+
this.inserterController?.ensure();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// The inserter's key on a row ("+": insert after it) comes before the
|
|
515
|
+
// drag's keys; anything else is the drag's.
|
|
516
|
+
onKeyDown(event) {
|
|
517
|
+
const item = event.target.closest?.(ITEM);
|
|
518
|
+
if (!activeSortableSession() && item && item.closest('sf-sortable') === this && event.target === item
|
|
519
|
+
&& this.inserterController?.onItemKeyDown(event, item)) return;
|
|
520
|
+
super.onKeyDown(event);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// The registry sf-sortable is defined in, through which it is defined with its
|
|
526
|
+
// inserter: defineSortable takes the registry it defines into.
|
|
527
|
+
export function inserterRegistry(registry = globalThis.customElements) {
|
|
528
|
+
return {
|
|
529
|
+
get: (name) => registry.get(name),
|
|
530
|
+
define: (name, Base, options) => registry.define(name, withSortableInserter(Base), options),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export default { SORTABLE_INSERT_EVENT, createSortableInserter, inserterRegistry, insertionGeometry, nearestInsertion, withSortableInserter };
|
|
@@ -7086,7 +7086,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"version":1,"aliases":{"column-gap/de
|
|
|
7086
7086
|
/***/ "e7acc025fab1"
|
|
7087
7087
|
(module) {
|
|
7088
7088
|
|
|
7089
|
-
module.exports = /*#__PURE__*/JSON.parse('{"cl-alert":["display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default"],"cl-avatar":["align-items/default","background-color/default","display/default","element-position/default","flex-direction/default","flex/default","height/default","justify-content/default","overflow/default","place-items/default","position/default","text-overflow/default","transition-property/default","white-space/default","width/default"],"cl-avatars":["align-items/default","display/default","flex-direction/default","position/default"],"cl-badges":["display/default","flex-direction/default","flex-wrap/default","justify-content/default","place-items/default","position/default","sr-only/default"],"cl-breadcrumbs":["align-items/default","container/default","display/default","position/default","visibility/default"],"cl-button-group":["align-items/default","display/default","isolate/default","pointer-events/default","position/default","text-align/default"],"cl-buttons":["align-items/default","display/default","flex/default","pointer-events/default","position/default"],"cl-checkbox":["cursor/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-close":["display/default","justify-content/default","place-items/default","position/default"],"cl-color":["display/default","position/default"],"cl-context-menu":["display/default","flex-direction/default","flex/default","position/default","text-decoration/default"],"cl-copy-button":["display/default","position/default"],"cl-country-code":["display/default","flex-direction/default","place-items/default","position/default","transition-property/default"],"cl-datepicker":["align-content/default","align-items/default","display/default","element-position/default","flex-direction/default","font-transform/default","gap/default","grid-template-columns/default","isolate/default","justify-content/default","overflow/default","position/default","resize/default"],"cl-download-file":["cursor/default","display/default","position/default","transition-property/default"],"cl-drawer":["display/default","isolate/default","justify-content/default","margin/default","place-items/default","position/default","resize/default","visibility/default"],"cl-dropdown":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default","resize/default","transition-property/default","visibility/default"],"cl-empty-state":["display/default","isolate/default","position/default"],"cl-file-preview":["isolate/default","position/default"],"cl-file-upload":["align-items/default","align-self/default","cursor/default","display/default","flex-direction/default","flex/default","gap/default","grid-template-columns/default","justify-content/default","justify-items/default","position/default","width/default"],"cl-gallery":["align-items/default","background-color/default","border-radius/default","border-width/default","box-shadow/default","display/default","filter-blur/default","flex-direction/default","gap/default","height/default","justify-content/default","margin/default","max-width/default","min-height/default","min-width/default","padding/default","position/default","text-color/default","width/default"],"cl-help-mark":["display/default","isolate/default","position/default"],"cl-icon-buttons":["align-items/default","display/default","pointer-events/default","position/default"],"cl-icons":["font-weight/default","position/default"],"cl-inputs":["align-items/default","display/default","flex-direction/default","position/default","text-align/default","transition-property/default"],"cl-list-item":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","gap/default","justify-content/default","place-items/default","position/default"],"cl-modal":["align-items/default","backdrop-filter-blur/default","border-radius/default","border-width/default","display/default","element-position/default","filter-blur/default","flex-direction/default","flex-wrap/default","font-size/default","gap/default","height/default","justify-content/default","margin/default","min-width/default","overflow/default","padding/default","place-items/default","position/default","text-color/default","title/default","width/default","z-index/default"],"cl-page-header":["align-items/default","display/default","flex-direction/default","flex-grow/default","flex-wrap/default","gap/default","margin/default","min-width/default","position/default","text-overflow/default"],"cl-pagination":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default"],"cl-popover":["display/default","position/default"],"cl-profile-picker":["display/default","position/default"],"cl-progress-bar":["align-items/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-progress-scale":["align-items/default","display/default","flex-direction/default","grid-template-columns/default","overflow/default","position/default"],"cl-radio":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-range-slider":["flex/default","isolate/default","position/default"],"cl-reference-link":["align-items/default","display/default","position/default"],"cl-resizer":["display/default","position/default","resize/default","text-max-width/default"],"cl-skeleton":["animation/default","display/default","flex/default","position/default"],"cl-slider":["background-color/default","border-radius/default","display/default","padding/default","position/default","text-color/default"],"cl-spinner":["animation/default","position/default","stroke-color/default"],"cl-steps":["cursor/default","display/default","flex-direction/default","justify-content/default","place-items/default","position/default"],"cl-switch":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-tabs":["container/default","display/default","flex-direction/default","position/default","resize/default","visibility/default"],"cl-tags":["background-size/default","display/default","flex-direction/default","flex-wrap/default","justify-content/default","min-width/default","place-items/default","position/default"],"cl-textarea":["display/default","flex-direction/default","isolate/default","position/default","transition-property/default"],"cl-theme-generator":["position/default"],"cl-toast":["align-items/default","display/default","flex-direction/default","flex/default","justify-content/default","place-items/default","position/default"],"cl-toggle":["display/default","justify-content/default","place-items/default","position/default","transition-property/default"],"cl-tooltip":["display/default","flex-direction/default","position/default"],"cl-tree":["display/default","flex-direction/default","position/default","visibility/default"],"cl-tree-item":["align-items/default","container/default","display/default","flex-direction/default","position/default"]}');
|
|
7089
|
+
module.exports = /*#__PURE__*/JSON.parse('{"cl-alert":["display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default"],"cl-avatar":["align-items/default","background-color/default","display/default","element-position/default","flex-direction/default","flex/default","height/default","justify-content/default","overflow/default","place-items/default","position/default","text-overflow/default","transition-property/default","white-space/default","width/default"],"cl-avatars":["align-items/default","display/default","flex-direction/default","position/default"],"cl-badges":["display/default","flex-direction/default","flex-wrap/default","justify-content/default","place-items/default","position/default","sr-only/default"],"cl-breadcrumbs":["align-items/default","container/default","display/default","position/default","visibility/default"],"cl-button-group":["align-items/default","display/default","isolate/default","pointer-events/default","position/default","text-align/default"],"cl-buttons":["align-items/default","display/default","flex/default","pointer-events/default","position/default"],"cl-checkbox":["cursor/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-close":["display/default","justify-content/default","place-items/default","position/default"],"cl-color":["display/default","position/default"],"cl-context-menu":["display/default","flex-direction/default","flex/default","position/default","text-decoration/default"],"cl-copy-button":["display/default","position/default"],"cl-country-code":["display/default","flex-direction/default","place-items/default","position/default","transition-property/default"],"cl-datepicker":["align-content/default","align-items/default","display/default","element-position/default","flex-direction/default","font-transform/default","gap/default","grid-template-columns/default","isolate/default","justify-content/default","overflow/default","position/default","resize/default"],"cl-download-file":["cursor/default","display/default","position/default","transition-property/default"],"cl-drawer":["display/default","isolate/default","justify-content/default","margin/default","place-items/default","position/default","resize/default","visibility/default"],"cl-dropdown":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default","resize/default","transition-property/default","visibility/default"],"cl-empty-state":["display/default","isolate/default","position/default"],"cl-file-preview":["isolate/default","position/default"],"cl-file-upload":["align-items/default","align-self/default","cursor/default","display/default","flex-direction/default","flex/default","gap/default","grid-template-columns/default","justify-content/default","justify-items/default","position/default","width/default"],"cl-gallery":["align-items/default","background-color/default","border-radius/default","border-width/default","box-shadow/default","display/default","filter-blur/default","flex-direction/default","gap/default","height/default","justify-content/default","margin/default","max-width/default","min-height/default","min-width/default","padding/default","position/default","text-color/default","width/default"],"cl-help-mark":["display/default","isolate/default","position/default"],"cl-icon-buttons":["align-items/default","display/default","pointer-events/default","position/default"],"cl-icons":["font-weight/default","position/default"],"cl-image-field":["position/default"],"cl-inputs":["align-items/default","display/default","flex-direction/default","position/default","text-align/default","transition-property/default"],"cl-list-item":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","gap/default","justify-content/default","place-items/default","position/default"],"cl-modal":["align-items/default","backdrop-filter-blur/default","border-radius/default","border-width/default","display/default","element-position/default","filter-blur/default","flex-direction/default","flex-wrap/default","font-size/default","gap/default","height/default","justify-content/default","margin/default","min-width/default","overflow/default","padding/default","place-items/default","position/default","text-color/default","title/default","width/default","z-index/default"],"cl-page-header":["align-items/default","display/default","flex-direction/default","flex-grow/default","flex-wrap/default","gap/default","margin/default","min-width/default","position/default","text-overflow/default"],"cl-pagination":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default"],"cl-popover":["display/default","position/default"],"cl-profile-picker":["display/default","position/default"],"cl-progress-bar":["align-items/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-progress-scale":["align-items/default","display/default","flex-direction/default","grid-template-columns/default","overflow/default","position/default"],"cl-radio":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-range-slider":["flex/default","isolate/default","position/default"],"cl-reference-link":["align-items/default","display/default","position/default"],"cl-resizer":["display/default","position/default","resize/default","text-max-width/default"],"cl-skeleton":["animation/default","display/default","flex/default","position/default"],"cl-slider":["background-color/default","border-radius/default","display/default","padding/default","position/default","text-color/default"],"cl-spinner":["animation/default","position/default","stroke-color/default"],"cl-steps":["cursor/default","display/default","flex-direction/default","justify-content/default","place-items/default","position/default"],"cl-switch":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-tabs":["container/default","display/default","flex-direction/default","position/default","resize/default","visibility/default"],"cl-tags":["background-size/default","display/default","flex-direction/default","flex-wrap/default","justify-content/default","min-width/default","place-items/default","position/default"],"cl-textarea":["display/default","flex-direction/default","isolate/default","position/default","transition-property/default"],"cl-theme-generator":["position/default"],"cl-toast":["align-items/default","display/default","flex-direction/default","flex/default","justify-content/default","place-items/default","position/default"],"cl-toggle":["display/default","justify-content/default","place-items/default","position/default","transition-property/default"],"cl-tooltip":["display/default","flex-direction/default","position/default"],"cl-tree":["display/default","flex-direction/default","position/default","visibility/default"],"cl-tree-item":["align-items/default","container/default","display/default","flex-direction/default","position/default"]}');
|
|
7090
7090
|
|
|
7091
7091
|
/***/ }
|
|
7092
7092
|
|