@simai-studio/view 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +183 -0
- package/README.md +75 -2
- package/dist/package-receipt.json +972 -0
- package/dist/set.json +141 -0
- package/dist/smart/rule.js +278 -0
- package/dist/smart/rule.js.gz +0 -0
- package/dist/smart/view/aliases.json +129 -0
- package/dist/smart/view/aliases.json.gz +0 -0
- package/dist/smart/view/component.json +1628 -0
- package/dist/smart/view/component.json.gz +0 -0
- package/dist/smart/view/css/view.css +208 -0
- package/dist/smart/view/css/view.css.gz +0 -0
- package/dist/smart/view/css/view.min.css +2 -0
- package/dist/smart/view/css/view.min.css.gz +0 -0
- package/dist/smart/view/examples.json +668 -0
- package/dist/smart/view/examples.json.gz +0 -0
- package/dist/smart/view/js/view.js +3955 -0
- package/dist/smart/view/js/view.js.gz +0 -0
- package/dist/smart/view/js/view.min.js +1 -0
- package/dist/smart/view/js/view.min.js.gz +0 -0
- package/dist/smart/view/lang/en.json +61 -0
- package/dist/smart/view/lang/en.json.gz +0 -0
- package/dist/smart/view/lang/ru.json +61 -0
- package/dist/smart/view/lang/ru.json.gz +0 -0
- package/dist/smart/view/probe.json +165 -0
- package/dist/smart/view/probe.json.gz +0 -0
- package/dist/smart/view/smart.manifest.json +1577 -0
- package/dist/smart/view/smart.manifest.json.gz +0 -0
- package/dist/smart/view/static/default/embedded.html +96 -0
- package/dist/smart/view/static/default/embedded.html.gz +0 -0
- package/dist/smart/view/static/default/open-record.html +242 -0
- package/dist/smart/view/static/default/open-record.html.gz +0 -0
- package/dist/smart/view/static/default/reference.html +242 -0
- package/dist/smart/view/static/default/reference.html.gz +0 -0
- package/dist/smart/view/static/sheets.json +282 -0
- package/dist/smart/view/static/sheets.json.gz +0 -0
- package/dist/smart/view-properties/aliases.json +174 -0
- package/dist/smart/view-properties/aliases.json.gz +0 -0
- package/dist/smart/view-properties/component.json +1906 -0
- package/dist/smart/view-properties/component.json.gz +0 -0
- package/dist/smart/view-properties/css/view-properties.css +1158 -0
- package/dist/smart/view-properties/css/view-properties.css.gz +0 -0
- package/dist/smart/view-properties/css/view-properties.min.css +2 -0
- package/dist/smart/view-properties/css/view-properties.min.css.gz +0 -0
- package/dist/smart/view-properties/examples.json +841 -0
- package/dist/smart/view-properties/examples.json.gz +0 -0
- package/dist/smart/view-properties/js/view-properties.js +10551 -0
- package/dist/smart/view-properties/js/view-properties.js.gz +0 -0
- package/dist/smart/view-properties/js/view-properties.min.js +1392 -0
- package/dist/smart/view-properties/js/view-properties.min.js.gz +0 -0
- package/dist/smart/view-properties/lang/en.json +89 -0
- package/dist/smart/view-properties/lang/en.json.gz +0 -0
- package/dist/smart/view-properties/lang/ru.json +89 -0
- package/dist/smart/view-properties/lang/ru.json.gz +0 -0
- package/dist/smart/view-properties/probe.json +188 -0
- package/dist/smart/view-properties/probe.json.gz +0 -0
- package/dist/smart/view-properties/skeleton.html +13 -0
- package/dist/smart/view-properties/skeleton.html.gz +0 -0
- package/dist/smart/view-properties/smart.manifest.json +1593 -0
- package/dist/smart/view-properties/smart.manifest.json.gz +0 -0
- package/dist/smart/view-properties/static/default/full.html +2 -0
- package/dist/smart/view-properties/static/default/full.html.gz +0 -0
- package/dist/smart/view-properties/static/default/panel-closed.html +4 -0
- package/dist/smart/view-properties/static/default/panel-closed.html.gz +0 -0
- package/dist/smart/view-properties/static/default/panel.html +4 -0
- package/dist/smart/view-properties/static/default/panel.html.gz +0 -0
- package/dist/smart/view-properties/static/default/reference.html +2 -0
- package/dist/smart/view-properties/static/default/reference.html.gz +0 -0
- package/dist/smart/view-properties/static/sheets.json +426 -0
- package/dist/smart/view-properties/static/sheets.json.gz +0 -0
- package/dist/smart/view-properties/template/default.css +992 -0
- package/dist/smart/view-properties/template/default.css.gz +0 -0
- package/dist/smart/view-properties/template/default.js +1658 -0
- package/dist/smart/view-properties/template/default.js.gz +0 -0
- package/dist/smart/view-table/aliases.json +448 -0
- package/dist/smart/view-table/aliases.json.gz +0 -0
- package/dist/smart/view-table/component.json +2746 -0
- package/dist/smart/view-table/component.json.gz +0 -0
- package/dist/smart/view-table/css/view-table.css +3658 -0
- package/dist/smart/view-table/css/view-table.css.gz +0 -0
- package/dist/smart/view-table/css/view-table.min.css +2 -0
- package/dist/smart/view-table/css/view-table.min.css.gz +0 -0
- package/dist/smart/view-table/examples.json +499 -0
- package/dist/smart/view-table/examples.json.gz +0 -0
- package/dist/smart/view-table/js/view-table.js +14166 -0
- package/dist/smart/view-table/js/view-table.js.gz +0 -0
- package/dist/smart/view-table/js/view-table.min.js +1440 -0
- package/dist/smart/view-table/js/view-table.min.js.gz +0 -0
- package/dist/smart/view-table/lang/en.json +132 -0
- package/dist/smart/view-table/lang/en.json.gz +0 -0
- package/dist/smart/view-table/lang/ru.json +132 -0
- package/dist/smart/view-table/lang/ru.json.gz +0 -0
- package/dist/smart/view-table/probe.json +275 -0
- package/dist/smart/view-table/probe.json.gz +0 -0
- package/dist/smart/view-table/smart.manifest.json +2531 -0
- package/dist/smart/view-table/smart.manifest.json.gz +0 -0
- package/dist/smart/view-table/static/default/current.html +141 -0
- package/dist/smart/view-table/static/default/current.html.gz +0 -0
- package/dist/smart/view-table/static/default/current.skeleton.html +141 -0
- package/dist/smart/view-table/static/default/current.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/default/empty.html +149 -0
- package/dist/smart/view-table/static/default/empty.html.gz +0 -0
- package/dist/smart/view-table/static/default/loading.html +180 -0
- package/dist/smart/view-table/static/default/loading.html.gz +0 -0
- package/dist/smart/view-table/static/default/many.html +180 -0
- package/dist/smart/view-table/static/default/many.html.gz +0 -0
- package/dist/smart/view-table/static/default/reference.html +136 -0
- package/dist/smart/view-table/static/default/reference.html.gz +0 -0
- package/dist/smart/view-table/static/default/reference.skeleton.html +136 -0
- package/dist/smart/view-table/static/default/reference.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/default/typed-cells.html +229 -0
- package/dist/smart/view-table/static/default/typed-cells.html.gz +0 -0
- package/dist/smart/view-table/static/default/typed-cells.skeleton.html +229 -0
- package/dist/smart/view-table/static/default/typed-cells.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/sheets.json +480 -0
- package/dist/smart/view-table/static/sheets.json.gz +0 -0
- package/dist/smart/view-table/studio.fixture.json +17 -0
- package/dist/smart/view-table/studio.fixture.json.gz +0 -0
- package/dist/smart/view-table/studio.scenario.json +30 -0
- package/dist/smart/view-table/studio.scenario.json.gz +0 -0
- package/dist/smart/view-table/template/default.css +1301 -0
- package/dist/smart/view-table/template/default.css.gz +0 -0
- package/dist/smart/view-table/template/default.js +1215 -0
- package/dist/smart/view-table/template/default.js.gz +0 -0
- package/dist/smart/view-table/template/link-cell-contract.js +48 -0
- package/dist/smart/view-table/template/link-cell-contract.js.gz +0 -0
- package/package.json +29 -4
|
@@ -0,0 +1,3955 @@
|
|
|
1
|
+
/******/ (() => { // webpackBootstrap
|
|
2
|
+
/******/ "use strict";
|
|
3
|
+
/******/ var __webpack_modules__ = ({
|
|
4
|
+
|
|
5
|
+
/***/ "51250bf01ab0"
|
|
6
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ createPreferences: () => (/* binding */ createPreferences),
|
|
11
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
// SF.Preferences: what a person changes about how a thing looks -- column
|
|
14
|
+
// order, widths, sort, density, a menu's column widths -- kept one way for
|
|
15
|
+
// every component (owner, 2026-10-14; ui-control
|
|
16
|
+
// control/designs/2026-10-14-view-preferences.md).
|
|
17
|
+
//
|
|
18
|
+
// - A preference is a plain JSON value under a key `<component>/<instance>`,
|
|
19
|
+
// e.g. `sf-table/cms.solutions`. No key, nothing is kept.
|
|
20
|
+
// - It is read synchronously, so a component applies it before its first
|
|
21
|
+
// paint: the skeleton is already in the person's order.
|
|
22
|
+
// - The browser keeps a copy always (localStorage `sf-pref:<key>`), the cache
|
|
23
|
+
// a first paint can read. A host that keeps preferences for its user
|
|
24
|
+
// (Larena) plugs in with useHost(); its values win over the cache, and every
|
|
25
|
+
// write goes to it as well, debounced per key.
|
|
26
|
+
// - A page can preload the host's values: preload({key: value}), or a
|
|
27
|
+
// <script type="application/json" data-sf-preferences> (or id
|
|
28
|
+
// "sf-preferences") in the page, read before the first value is.
|
|
29
|
+
//
|
|
30
|
+
// The registry lives on globalThis.SF: every bundle carries its own copy of
|
|
31
|
+
// this module, and two copies with two caches would disagree.
|
|
32
|
+
const PREFIX = 'sf-pref:';
|
|
33
|
+
const DELAY = 400;
|
|
34
|
+
const KEY = /^[\w.*:-]+(?:\/[\w.*:/-]+)?$/u;
|
|
35
|
+
|
|
36
|
+
function storage() {
|
|
37
|
+
try {
|
|
38
|
+
return globalThis.localStorage ?? null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validKey(key) {
|
|
45
|
+
return typeof key === 'string' && key.length > 0 && key.length <= 200 && KEY.test(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function same(a, b) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function plain(value) {
|
|
57
|
+
return value === null || value === undefined ? null : JSON.parse(JSON.stringify(value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createPreferences() {
|
|
61
|
+
const host = {
|
|
62
|
+
adapter: null,
|
|
63
|
+
values: new Map()
|
|
64
|
+
};
|
|
65
|
+
const listeners = new Map();
|
|
66
|
+
const timers = new Map(); // The page's own block of the host's values, read once, before the first
|
|
67
|
+
// read: a component may connect before this module's start-up ran.
|
|
68
|
+
|
|
69
|
+
let inlineRead = false;
|
|
70
|
+
|
|
71
|
+
const readInline = () => {
|
|
72
|
+
if (inlineRead) return;
|
|
73
|
+
const node = globalThis.document?.querySelector?.('script[type="application/json"][data-sf-preferences], script#sf-preferences[type="application/json"]');
|
|
74
|
+
if (!node) return;
|
|
75
|
+
inlineRead = true;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
api.preload(JSON.parse(node.textContent || '{}'));
|
|
79
|
+
} catch {
|
|
80
|
+
/* not JSON: ignored */
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const notify = (key, value) => {
|
|
85
|
+
for (const listener of listeners.get(key) ?? []) {
|
|
86
|
+
try {
|
|
87
|
+
listener(plain(value), key);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('SF.Preferences listener failed', key, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const readCache = key => {
|
|
95
|
+
const store = storage();
|
|
96
|
+
if (!store) return null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const raw = store.getItem(PREFIX + key);
|
|
100
|
+
return raw === null ? null : JSON.parse(raw);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const writeCache = (key, value) => {
|
|
107
|
+
const store = storage();
|
|
108
|
+
if (!store) return;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (value === null) store.removeItem(PREFIX + key);else store.setItem(PREFIX + key, JSON.stringify(value));
|
|
112
|
+
} catch {// A full or refused storage keeps the value for this page only.
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const saveToHost = key => {
|
|
117
|
+
clearTimeout(timers.get(key));
|
|
118
|
+
timers.delete(key);
|
|
119
|
+
const adapter = host.adapter;
|
|
120
|
+
if (!adapter) return;
|
|
121
|
+
const value = host.values.has(key) ? host.values.get(key) : readCache(key);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const answer = value === null ? adapter.remove?.(key) : adapter.save?.(key, plain(value));
|
|
125
|
+
Promise.resolve(answer).catch(error => console.warn('SF.Preferences: the host did not keep', key, error));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn('SF.Preferences: the host did not keep', key, error);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const schedule = key => {
|
|
132
|
+
if (!host.adapter) return;
|
|
133
|
+
clearTimeout(timers.get(key));
|
|
134
|
+
timers.set(key, setTimeout(() => saveToHost(key), DELAY));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const api = {
|
|
138
|
+
// The value for a key: the host's when it gave one, else this browser's.
|
|
139
|
+
read(key) {
|
|
140
|
+
if (!validKey(key)) return null;
|
|
141
|
+
readInline();
|
|
142
|
+
if (host.values.has(key)) return plain(host.values.get(key));
|
|
143
|
+
return plain(readCache(key));
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Several keys from the most general to the most particular, merged field
|
|
147
|
+
// by field: `sf-table/*` (the person's tables) under `sf-table/<instance>`.
|
|
148
|
+
readMerged(...keys) {
|
|
149
|
+
const merged = {};
|
|
150
|
+
let found = false;
|
|
151
|
+
|
|
152
|
+
for (const key of keys.flat()) {
|
|
153
|
+
const value = api.read(key);
|
|
154
|
+
|
|
155
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
156
|
+
Object.assign(merged, value);
|
|
157
|
+
found = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return found ? merged : null;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
write(key, value) {
|
|
165
|
+
if (!validKey(key)) return false;
|
|
166
|
+
const next = plain(value); // The value already kept is not written again: a component restoring
|
|
167
|
+
// what the host gave it (sf-resizer's value) sent it back to the host on
|
|
168
|
+
// every load (docs trial 5).
|
|
169
|
+
|
|
170
|
+
if (JSON.stringify(api.read(key)) === JSON.stringify(next)) return true;
|
|
171
|
+
if (host.values.has(key)) host.values.set(key, next);
|
|
172
|
+
writeCache(key, next);
|
|
173
|
+
schedule(key);
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// «Сбросить вид»: gone from both.
|
|
178
|
+
remove(key) {
|
|
179
|
+
if (!validKey(key)) return false;
|
|
180
|
+
if (host.values.has(key)) host.values.set(key, null);
|
|
181
|
+
writeCache(key, null);
|
|
182
|
+
schedule(key);
|
|
183
|
+
notify(key, null);
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
subscribe(key, listener) {
|
|
188
|
+
if (!validKey(key) || typeof listener !== 'function') return () => {};
|
|
189
|
+
if (!listeners.has(key)) listeners.set(key, new Set());
|
|
190
|
+
listeners.get(key).add(listener);
|
|
191
|
+
return () => listeners.get(key)?.delete(listener);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// The host's values for this person. Each that differs from this
|
|
195
|
+
// browser's copy replaces it and is announced, so a component re-applies.
|
|
196
|
+
preload(values) {
|
|
197
|
+
if (!values || typeof values !== 'object') return;
|
|
198
|
+
|
|
199
|
+
for (const [key, value] of Object.entries(values)) {
|
|
200
|
+
if (!validKey(key)) continue;
|
|
201
|
+
const next = plain(value);
|
|
202
|
+
const before = api.read(key);
|
|
203
|
+
host.values.set(key, next);
|
|
204
|
+
writeCache(key, next);
|
|
205
|
+
if (!same(before, next)) notify(key, next);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// {save(key, value), remove(key), load?(keys) → Promise<{key: value}>}.
|
|
210
|
+
useHost(adapter) {
|
|
211
|
+
if (!adapter || typeof adapter.save !== 'function') throw new TypeError('A preferences host saves: {save(key, value)}');
|
|
212
|
+
host.adapter = adapter;
|
|
213
|
+
return api;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Asks the host for keys it has not given yet (a component that appears
|
|
217
|
+
// after the page loaded).
|
|
218
|
+
async load(keys) {
|
|
219
|
+
const wanted = [].concat(keys).filter(key => validKey(key) && !host.values.has(key));
|
|
220
|
+
if (!wanted.length || typeof host.adapter?.load !== 'function') return;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
api.preload(await host.adapter.load(wanted));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn('SF.Preferences: the host did not answer', wanted, error);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
get hosted() {
|
|
230
|
+
return Boolean(host.adapter);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// Writes still waiting go now (a page that is leaving).
|
|
234
|
+
flush() {
|
|
235
|
+
for (const key of [...timers.keys()]) saveToHost(key);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
}; // Another tab changed a preference.
|
|
239
|
+
|
|
240
|
+
globalThis.addEventListener?.('storage', event => {
|
|
241
|
+
if (!event.key?.startsWith(PREFIX) || event.storageArea !== storage()) return;
|
|
242
|
+
const key = event.key.slice(PREFIX.length);
|
|
243
|
+
if (host.values.has(key)) return;
|
|
244
|
+
let value = null;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
value = event.newValue === null ? null : JSON.parse(event.newValue);
|
|
248
|
+
} catch {
|
|
249
|
+
value = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
notify(key, value);
|
|
253
|
+
});
|
|
254
|
+
globalThis.addEventListener?.('pagehide', () => api.flush());
|
|
255
|
+
readInline();
|
|
256
|
+
return Object.freeze(api);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (typeof globalThis !== 'undefined') {
|
|
260
|
+
globalThis.SF = globalThis.SF || {};
|
|
261
|
+
if (!globalThis.SF.Preferences) globalThis.SF.Preferences = createPreferences();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const Preferences = globalThis.SF?.Preferences;
|
|
265
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Preferences);
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
/***/ },
|
|
269
|
+
|
|
270
|
+
/***/ "17828be41e75"
|
|
271
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
272
|
+
|
|
273
|
+
__webpack_require__.r(__webpack_exports__);
|
|
274
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
275
|
+
/* harmony export */ answerActions: () => (/* binding */ answerActions),
|
|
276
|
+
/* harmony export */ outcomeOf: () => (/* binding */ outcomeOf),
|
|
277
|
+
/* harmony export */ settingSetters: () => (/* binding */ settingSetters)
|
|
278
|
+
/* harmony export */ });
|
|
279
|
+
// The actions a manifest declares, on a class that already has the methods
|
|
280
|
+
// (standard simai.smart-component 1.3.8 §4.4). elementFromManifest makes a
|
|
281
|
+
// method per action, but a component whose class predates its manifest keeps
|
|
282
|
+
// its own methods, which shadow the generated ones. This wraps those methods
|
|
283
|
+
// once per class so that, as the standard asks, each:
|
|
284
|
+
//
|
|
285
|
+
// - takes its `with` parameters as one object, as well as positionally;
|
|
286
|
+
// - answers an outcome: {outcome: applied | refused | conflict | unavailable
|
|
287
|
+
// | cancelled, reason?, message?}. A host port's answer ({answer, reason,
|
|
288
|
+
// message}, vocabulary §3.1) is read as the outcome it is; a method that
|
|
289
|
+
// answered nothing, or the element itself, answered applied.
|
|
290
|
+
//
|
|
291
|
+
// Helpers the class calls internally go through the same wrapper; nothing in
|
|
292
|
+
// the package reads their old return value (the element, or the port's
|
|
293
|
+
// answer under `answer`), and `answer` stays on the outcome for one that does.
|
|
294
|
+
const camel = name => String(name).replace(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase());
|
|
295
|
+
|
|
296
|
+
const isPlain = value => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
297
|
+
|
|
298
|
+
function outcomeOf(result, element) {
|
|
299
|
+
if (result === undefined || result === null || result === true || result === element) return {
|
|
300
|
+
outcome: "applied"
|
|
301
|
+
};
|
|
302
|
+
if (result === false) return {
|
|
303
|
+
outcome: "refused",
|
|
304
|
+
reason: "refused"
|
|
305
|
+
};
|
|
306
|
+
if (typeof result === "object" && typeof result.outcome === "string") return result;
|
|
307
|
+
|
|
308
|
+
if (typeof result === "object" && typeof result.answer === "string") {
|
|
309
|
+
const {
|
|
310
|
+
answer,
|
|
311
|
+
reason,
|
|
312
|
+
message
|
|
313
|
+
} = result;
|
|
314
|
+
return { ...result,
|
|
315
|
+
outcome: answer,
|
|
316
|
+
...(reason ? {
|
|
317
|
+
reason
|
|
318
|
+
} : {}),
|
|
319
|
+
...(message ? {
|
|
320
|
+
message
|
|
321
|
+
} : {})
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
outcome: "applied",
|
|
327
|
+
value: result
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function answerActions(Class, manifest) {
|
|
331
|
+
const proto = Class.prototype;
|
|
332
|
+
|
|
333
|
+
for (const [name, spec] of Object.entries(manifest.actions || {})) {
|
|
334
|
+
const method = camel(name);
|
|
335
|
+
const run = proto[method];
|
|
336
|
+
if (typeof run !== "function" || run.sfAction) continue;
|
|
337
|
+
const names = Object.keys(spec?.with || {});
|
|
338
|
+
|
|
339
|
+
const wrapped = function manifestAction(...args) {
|
|
340
|
+
// One object naming only declared parameters is the object form.
|
|
341
|
+
if (args.length === 1 && isPlain(args[0])) {
|
|
342
|
+
const given = Object.keys(args[0]);
|
|
343
|
+
if (!names.length && !given.length) args = [];else if (names.length && given.length && given.every(key => names.includes(key) || names.map(camel).includes(key))) {
|
|
344
|
+
args = names.map(key => args[0][key] ?? args[0][camel(key)]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = run.apply(this, args);
|
|
349
|
+
if (result && typeof result.then === "function") return result.then(value => outcomeOf(value, this));
|
|
350
|
+
return outcomeOf(result, this);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
wrapped.sfAction = true;
|
|
354
|
+
Object.defineProperty(proto, method, {
|
|
355
|
+
configurable: true,
|
|
356
|
+
writable: true,
|
|
357
|
+
value: wrapped
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return Class;
|
|
362
|
+
} // A setting the class reads through a getter of its own (an attribute read
|
|
363
|
+
// with its parsing) is writable as a property too, as §4 makes every setting:
|
|
364
|
+
// an assembly binds by property when the element has one (SF.Assembly's
|
|
365
|
+
// adaptElement), and a getter alone threw «has only a getter» at the first
|
|
366
|
+
// binding. The setter writes the attribute the getter reads.
|
|
367
|
+
|
|
368
|
+
function settingSetters(Class, manifest) {
|
|
369
|
+
const proto = Class.prototype;
|
|
370
|
+
|
|
371
|
+
for (const [name, spec] of Object.entries(manifest.settings || {})) {
|
|
372
|
+
if (["list", "object", "state"].includes(spec?.type)) continue;
|
|
373
|
+
const property = camel(name);
|
|
374
|
+
let owner = proto;
|
|
375
|
+
let found = null;
|
|
376
|
+
|
|
377
|
+
while (owner && owner !== globalThis.HTMLElement?.prototype && owner !== Object.prototype) {
|
|
378
|
+
found = Object.getOwnPropertyDescriptor(owner, property);
|
|
379
|
+
if (found) break;
|
|
380
|
+
owner = Object.getPrototypeOf(owner);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!found || !found.get || found.set) continue;
|
|
384
|
+
Object.defineProperty(owner, property, {
|
|
385
|
+
configurable: true,
|
|
386
|
+
get: found.get,
|
|
387
|
+
|
|
388
|
+
set(value) {
|
|
389
|
+
if (spec.type === "boolean") {
|
|
390
|
+
if (value === true || value === "" || value === "true") this.setAttribute(name, "");else if (value === false && spec.default === true) this.setAttribute(name, "false");else this.removeAttribute(name);
|
|
391
|
+
} else if (value === null || value === undefined) {
|
|
392
|
+
this.removeAttribute(name);
|
|
393
|
+
} else {
|
|
394
|
+
this.setAttribute(name, String(value));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return Class;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/***/ },
|
|
405
|
+
|
|
406
|
+
/***/ "36b027867bab"
|
|
407
|
+
(module, __webpack_exports__, __webpack_require__) {
|
|
408
|
+
|
|
409
|
+
__webpack_require__.a(module, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
|
|
410
|
+
__webpack_require__.r(__webpack_exports__);
|
|
411
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
412
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
413
|
+
/* harmony export */ });
|
|
414
|
+
/* harmony import */ var _js_first_paint_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("4d9330f13af8");
|
|
415
|
+
/* harmony import */ var _core_js_page_language__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("43090d1c8307");
|
|
416
|
+
/* harmony import */ var _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("5b7d7695b41b");
|
|
417
|
+
/* harmony import */ var _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("deb1a4d61565");
|
|
418
|
+
/* harmony import */ var _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("892a0da25239");
|
|
419
|
+
/* harmony import */ var _view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("17828be41e75");
|
|
420
|
+
/* harmony import */ var _component_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("7086146c00cc");
|
|
421
|
+
/* harmony import */ var _aliases_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__("80ad38cb6908");
|
|
422
|
+
/* harmony import */ var _lang_en_json__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__("ce0305e57fa5");
|
|
423
|
+
/* harmony import */ var _lang_ru_json__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__("3e9dffe84919");
|
|
424
|
+
/* harmony import */ var _core_js_preferences_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__("51250bf01ab0");
|
|
425
|
+
/* harmony import */ var _js_tree_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__("29f88c884ba1");
|
|
426
|
+
var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__, _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__, _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__, _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__]);
|
|
427
|
+
([_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__, _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__, _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__, _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__] = __webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__);
|
|
428
|
+
/* harmony import */ var _core_js_page_language__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__);
|
|
429
|
+
/* harmony import */ var _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__);
|
|
430
|
+
/* harmony import */ var _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__);
|
|
431
|
+
/* harmony import */ var _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__);
|
|
432
|
+
// A composition shell. The host owns access, storage and query semantics;
|
|
433
|
+
// this element owns the browser lifecycle and projects answers into its slots.
|
|
434
|
+
// sf-view-table, sf-pagination and sf-toast are not imported: a package carries no
|
|
435
|
+
// element layer of its own, and an element that needs another asks for it by
|
|
436
|
+
// tag (ui-source docs/developer/shared-runtime.md). The rule's relations bring
|
|
437
|
+
// them (cl-view-table, cl-pagination, cl-toast lazy), and the view asks the Loader
|
|
438
|
+
// for any that is not defined yet (_askForParts). Imported, each was a second
|
|
439
|
+
// copy of the element in data-view.min.js (431 KB min; stand 25).
|
|
440
|
+
// The view's own first-paint styles, its skeleton, are its sheet.
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
// The renamed tags (sf-view-table for sf-table, sf-view for sf-data-view until
|
|
444
|
+
// 2027-01-01): a lookup by name takes both through these, and the view defines
|
|
445
|
+
// its former tag beside its own (it is not a Smart base element).
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
const LISTENED = Object.freeze(["sf-view-table-search-change", "sf-view-table-filter-change", "sf-view-table-sort-change", "sf-view-table-save", "sf-view-table-apply", "sf-view-table-refresh", "sf-view-table-select", "sf-view-table-column-settings-reset", "sf-view-table-expand", "sf-view-table-load-more", "sf-view-table-reference-open", "sf-view-table-cell-set", "sf-view-table-move", "sf-pagination-change", "sf-pagination-size-change", "sf-pagination-apply", "sf-pagination-load-more", "sf-pagination-selection-clear", "sf-pagination-count", "sf-pagination-action-change", "sf-pagination-action-for-all-change", "sf-view-saved-view-intent"]); // The table's and the page row's events by their names since standard 1.3.8
|
|
457
|
+
// (vocabulary 1.1.5): the old names they still send until 2026-12-01 are not
|
|
458
|
+
// listened to, so each act is taken once.
|
|
459
|
+
|
|
460
|
+
const EVENTS = Object.freeze({
|
|
461
|
+
"sf-view-table-query": "query.change",
|
|
462
|
+
"sf-view-table-template-select": "view.open",
|
|
463
|
+
"sf-view-table-template-save": "view.save",
|
|
464
|
+
"sf-view-table-template-delete": "view.delete",
|
|
465
|
+
"sf-view-table-create": "view.create_record"
|
|
466
|
+
});
|
|
467
|
+
let instanceNumber = 0; // Every port version this composite accepts, in one place. The list used to be
|
|
468
|
+
// written inline in the guard, and two versions were published — 1.3.0 for a
|
|
469
|
+
// host that does not count, 1.4.0 for personal pinning — while the guard still
|
|
470
|
+
// named 1.2.0 as the newest it would take. A host moving to either would have
|
|
471
|
+
// been refused outright by the component that asked it to move. The contract
|
|
472
|
+
// test beside this file now compares the list with the manifest.
|
|
473
|
+
// The port speaks snake_case and the table reads camelCase, and the translation
|
|
474
|
+
// is this view's job: a column that named its property_type reached the table
|
|
475
|
+
// under a name nothing read, so the cell drew the stored value. Both names are
|
|
476
|
+
// kept -- the port's, because that is what the host sent and what a consumer
|
|
477
|
+
// reading the DOM expects, and the component's, because that is what the shared
|
|
478
|
+
// renderer set asks for.
|
|
479
|
+
|
|
480
|
+
const COLUMN_PRESENTATION = {
|
|
481
|
+
property_type: "propertyType",
|
|
482
|
+
min_width: "minWidth"
|
|
483
|
+
}; // The sentence a host answer carries for a person (port answer `message`):
|
|
484
|
+
// a non-empty string, trimmed; anything else is no message.
|
|
485
|
+
// A chrome part the host turned off on its own table (selectable="false",
|
|
486
|
+
// settings="false") stays off. The view writes these attributes on every
|
|
487
|
+
// projection, so what the table carried before the view first touched it is
|
|
488
|
+
// kept aside, as data-sf-host-row-actions is, and read from there.
|
|
489
|
+
|
|
490
|
+
function hostWroteOff(table, name) {
|
|
491
|
+
if (!table) return false;
|
|
492
|
+
const marker = `data-sf-host-${name}`;
|
|
493
|
+
if (!table.hasAttribute?.(marker)) table.setAttribute?.(marker, table.getAttribute?.(name) ?? "");
|
|
494
|
+
return table.getAttribute?.(marker) === "false";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function hostMessage(answer) {
|
|
498
|
+
return typeof answer?.message === "string" ? answer.message.trim() : "";
|
|
499
|
+
} // Whether a refused list was refused for access rather than for the query
|
|
500
|
+
// (Larena acceptance, 2026-10-22: a search longer than the host's limit,
|
|
501
|
+
// refused as query_invalid, drew «Нет доступа» and took the toolbar away, so
|
|
502
|
+
// the search could not be shortened without reloading). The port's reason is
|
|
503
|
+
// free text a host may extend (simai.dataview-port 1.12.0 answer.reason) and the
|
|
504
|
+
// contract names one access reason, `forbidden`; it has no class for the rest.
|
|
505
|
+
// Access is read from the code: `forbidden`, `access_denied`, `scope_denied`,
|
|
506
|
+
// and a host's own prefixed form of the last two (`storage_page_scope_denied`,
|
|
507
|
+
// `registered_list_access_denied`). A refusal without a reason stays a refusal
|
|
508
|
+
// of access, as every refused list was before. Any other reason is about the
|
|
509
|
+
// query: the person keeps the toolbar and the query to correct it.
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
const ACCESS_REFUSALS = new Set(["forbidden", "access_denied", "scope_denied"]);
|
|
513
|
+
|
|
514
|
+
function isAccessRefusal(reason) {
|
|
515
|
+
if (reason == null || reason === "") return true; // Larena writes the codes with underscores; ui-admin's reference host with
|
|
516
|
+
// hyphens (access-denied, stand 24 U2): both are the same refusal.
|
|
517
|
+
|
|
518
|
+
const code = String(reason).replace(/-/gu, "_");
|
|
519
|
+
return ACCESS_REFUSALS.has(code) || code.endsWith("_access_denied") || code.endsWith("_scope_denied");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function projectColumn(column) {
|
|
523
|
+
const projected = copy(column);
|
|
524
|
+
|
|
525
|
+
for (const [sent, read] of Object.entries(COLUMN_PRESENTATION)) {
|
|
526
|
+
if (projected[sent] !== undefined && projected[read] === undefined) {
|
|
527
|
+
projected[read] = projected[sent];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return projected;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const SUPPORTED_PORT_VERSIONS = ["1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "1.6.0", "1.7.0", "1.8.0", "1.9.0", "1.10.0", "1.11.0", "1.12.0", "1.13.0"]; // Creating a record and saving personal column settings became capabilities in
|
|
535
|
+
// port 1.10.0. A host that declares an earlier version was offered both without
|
|
536
|
+
// announcing them and keeps that; a host on 1.10.0 is offered them only when it
|
|
537
|
+
// announces them, so a page is never given a control nobody would answer.
|
|
538
|
+
|
|
539
|
+
const BASELINE_UNTIL_1_10 = new Set(["create-record", "personal-column-settings"]);
|
|
540
|
+
|
|
541
|
+
const portAtLeast = (version, minimum) => {
|
|
542
|
+
const parts = value => String(value ?? "").split(".").map(part => Number.parseInt(part, 10) || 0);
|
|
543
|
+
|
|
544
|
+
const [a, b] = [parts(version), parts(minimum)];
|
|
545
|
+
|
|
546
|
+
for (let index = 0; index < 3; index += 1) {
|
|
547
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return true;
|
|
551
|
+
}; // The words are the language files' (lang/<locale>.json, standard 1.3.8 §11),
|
|
552
|
+
// read under the camelCase keys a product registers more languages by.
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
const camelWords = (words = {}) => Object.fromEntries(Object.entries(words).map(([key, word]) => [key.replace(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase()), word]));
|
|
556
|
+
|
|
557
|
+
const DATA_VIEW_TEXT = {
|
|
558
|
+
en: camelWords(_lang_en_json__WEBPACK_IMPORTED_MODULE_8__.runtime),
|
|
559
|
+
ru: camelWords(_lang_ru_json__WEBPACK_IMPORTED_MODULE_9__.runtime)
|
|
560
|
+
}; // The parts this element draws itself and a consumer can leave out. `status` is
|
|
561
|
+
// absent on purpose and cannot be switched off: a list that could not say a load
|
|
562
|
+
// failed would be a list that looks empty when it is broken, and an embedded one
|
|
563
|
+
// with a row cap and no chrome is exactly where someone would be tempted to
|
|
564
|
+
// remove it. `bulk` and `notice` are host slots that render nothing when empty,
|
|
565
|
+
// so switching them would be theatre.
|
|
566
|
+
// `select` and `settings` are the table's two chrome columns rather than parts of
|
|
567
|
+
// this element, and they are named here because they are furniture of the same
|
|
568
|
+
// kind: a checkbox column exists to feed a bulk band and a gear column exists to
|
|
569
|
+
// reach column settings, and an embedded list has neither. Named explicitly
|
|
570
|
+
// instead of inferred from the toolbar, so a consumer who does want a selection
|
|
571
|
+
// in an embedded list can ask for it -- `chrome="select"` -- rather than having to
|
|
572
|
+
// turn the whole toolbar back on to get a checkbox.
|
|
573
|
+
// Words the view passes on to the table and the pagination it draws.
|
|
574
|
+
|
|
575
|
+
const TABLE_LABELS = ["column-search-placeholder", "column-search-empty-text", "column-move-label", "filter-apply-text"];
|
|
576
|
+
const PAGINATION_LABELS = ["show-more-text", "selected-label", "total-label", "pages-label", "first-label", "previous-label", "page-label", "next-label", "last-label", "page-field-label", "action-choose-label", "actions-label", "actions-region-label", "clear-selection-label", "page-size-label"];
|
|
577
|
+
const LABEL_ATTRIBUTES = [...TABLE_LABELS, ...PAGINATION_LABELS, "pagination-aria-label"];
|
|
578
|
+
const CHROME_PARTS = ["toolbar", "pagination", "select", "settings"]; // Writes that cannot have changed a row, so the view does not re-query after
|
|
579
|
+
// them. Opening a record changes nothing either; the rest of the settings layer
|
|
580
|
+
// is the person's own view of the same rows.
|
|
581
|
+
|
|
582
|
+
const ANSWER_ONLY = new Set(["node.targets"]);
|
|
583
|
+
const TREE_INTENTS = new Set(["tree.expand", "node.targets", "node.move"]);
|
|
584
|
+
const NO_REFRESH_AFTER = new Set(["record.open", "tree.expand", "node.targets", "settings.save_personal", "settings.save_shared"]);
|
|
585
|
+
|
|
586
|
+
const copy = value => value == null ? value : JSON.parse(JSON.stringify(value)); // Whether a filter value holds anything: an empty string, list or object is
|
|
587
|
+
// no filter.
|
|
588
|
+
// SF.Preferences: the view's sort and page size are the person's (owner,
|
|
589
|
+
// 2026-10-14), kept under the same key as the table's arrangement.
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
const preferences = () => globalThis.SF?.Preferences || _core_js_preferences_js__WEBPACK_IMPORTED_MODULE_10__["default"]; // The stored key keeps the former tag's spelling: it names what a person has
|
|
593
|
+
// already saved (SF.Preferences, a host's store), not the element.
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
const viewPreferenceKey = key => `sf-table/${String(key).replace(/[^\w.:-]+/gu, "-")}`;
|
|
597
|
+
|
|
598
|
+
function hasValue(value) {
|
|
599
|
+
if (value === null || value === undefined || value === "" || value === false) return false;
|
|
600
|
+
if (Array.isArray(value)) return value.some(hasValue);
|
|
601
|
+
if (typeof value === "object") return Object.values(value).some(hasValue);
|
|
602
|
+
return true;
|
|
603
|
+
} // sf-view from its component.json (standard simai.smart-component 1.3.8
|
|
604
|
+
// §3, §4, §8, §11): a leaf over plain HTMLElement (it draws into its own
|
|
605
|
+
// shadow root), its settings and values made by core/js/element-manifest.js,
|
|
606
|
+
// its actions answering outcomes (../table/js/manifest-actions.js), its events by the
|
|
607
|
+
// vocabulary's verbs with the old names as dated aliases (aliases.json).
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class PlainElement extends HTMLElement {
|
|
611
|
+
connectedCallback() {}
|
|
612
|
+
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const DataViewElement = (0,_core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__.elementFromManifest)(_component_json__WEBPACK_IMPORTED_MODULE_6__, {
|
|
616
|
+
base: PlainElement,
|
|
617
|
+
lang: {
|
|
618
|
+
en: _lang_en_json__WEBPACK_IMPORTED_MODULE_8__,
|
|
619
|
+
ru: _lang_ru_json__WEBPACK_IMPORTED_MODULE_9__
|
|
620
|
+
},
|
|
621
|
+
source: "ru",
|
|
622
|
+
aliases: _aliases_json__WEBPACK_IMPORTED_MODULE_7__
|
|
623
|
+
}); // The first paint's rows when nothing says how many the page asks for (sf-view-table's
|
|
624
|
+
// page, its pagination-page-size default), how many fields a record of the
|
|
625
|
+
// compact list shows under its title (sf-view-table list-fields).
|
|
626
|
+
|
|
627
|
+
const FIRST_PAINT_ROWS = 10;
|
|
628
|
+
const FIRST_PAINT_LIST_FIELDS = 3; // The vocabulary's data state (§3) for the view's own words for it.
|
|
629
|
+
|
|
630
|
+
const DATA_STATES = {
|
|
631
|
+
loading: "loading",
|
|
632
|
+
populated: "ready",
|
|
633
|
+
empty: "empty",
|
|
634
|
+
error: "error",
|
|
635
|
+
"no-access": "refused",
|
|
636
|
+
unavailable: "unavailable"
|
|
637
|
+
}; // A value as text with its keys in order, so two answers that say the same
|
|
638
|
+
// thing compare equal whatever order a server wrote the keys in.
|
|
639
|
+
|
|
640
|
+
const canonical = value => JSON.stringify(value, (key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(name => [name, item[name]])) : item); // What a port offers, as the projection drew it.
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
const capabilitiesKey = port => canonical([...(port?.capabilities ?? [])].map(String).sort()); // Whether the port's answer is the one the page printed: the same revision
|
|
644
|
+
// when both name one, else the same rows, count and columns.
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
function sameAnswer(printed, data) {
|
|
648
|
+
if (printed.revision !== undefined && printed.revision !== null && data.revision !== undefined && data.revision !== null) {
|
|
649
|
+
return String(printed.revision) === String(data.revision);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const shape = answer => canonical({
|
|
653
|
+
records: answer.records ?? null,
|
|
654
|
+
total: answer.total ?? null,
|
|
655
|
+
has_next: answer.has_next ?? null,
|
|
656
|
+
columns: answer.columns ?? null,
|
|
657
|
+
tree: answer.tree ?? null
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
return shape(printed) === shape(data);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
class SfDataView extends DataViewElement {
|
|
664
|
+
constructor() {
|
|
665
|
+
super();
|
|
666
|
+
this.instanceId = `sf-view-${++instanceNumber}`;
|
|
667
|
+
this.query = {
|
|
668
|
+
filters: {},
|
|
669
|
+
search: "",
|
|
670
|
+
sort: [],
|
|
671
|
+
page: 1,
|
|
672
|
+
per_page: 25
|
|
673
|
+
};
|
|
674
|
+
this.records = []; // The tree, when the host answers in levels (port 1.8.0): mode is
|
|
675
|
+
// tree, flat or flattened as the last answer said, null before one.
|
|
676
|
+
|
|
677
|
+
this.treeMode = null;
|
|
678
|
+
this.tree = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.createTree)();
|
|
679
|
+
this._treeAppend = new Set();
|
|
680
|
+
this.treeSequence = 0; // The host's labels and groups for its own action ids (row_actions).
|
|
681
|
+
|
|
682
|
+
this.rowActionCatalog = [];
|
|
683
|
+
this.continuing = false;
|
|
684
|
+
this.appended = null;
|
|
685
|
+
this.columns = [];
|
|
686
|
+
this.total = 0; // Whether the host counts. It is answered by the answers themselves: a
|
|
687
|
+
// total means there is a number, no total means there is a link. There
|
|
688
|
+
// is no mode attribute, because a view whose host stops counting should
|
|
689
|
+
// not need a host to be reconfigured as well.
|
|
690
|
+
|
|
691
|
+
this.countKnown = true; // A host may answer an estimate (`total_exact: false`): the number is
|
|
692
|
+
// shown, but as «~N», and nothing is applied under the filter on it.
|
|
693
|
+
|
|
694
|
+
this.totalExact = true;
|
|
695
|
+
this.hasNext = false;
|
|
696
|
+
this.countedQuery = null;
|
|
697
|
+
this.savedViews = [];
|
|
698
|
+
this.columnSettings = null;
|
|
699
|
+
this.sequence = 0;
|
|
700
|
+
this.epoch = 0;
|
|
701
|
+
this.pending = null;
|
|
702
|
+
this.hostPort = null; // Whether a projection has arrived. Until one has, the counts the host
|
|
703
|
+
// wrote in the markup are left alone.
|
|
704
|
+
|
|
705
|
+
this._answered = false;
|
|
706
|
+
|
|
707
|
+
this._bound = event => this._onEvent(event);
|
|
708
|
+
|
|
709
|
+
this._seenEvents = new WeakSet();
|
|
710
|
+
this.attachShadow({
|
|
711
|
+
mode: "open"
|
|
712
|
+
});
|
|
713
|
+
this.shadowRoot.innerHTML = `
|
|
714
|
+
<style>
|
|
715
|
+
:host { display: block; }
|
|
716
|
+
[part="layout"] { display: grid; gap: var(--sf-data-view-gap, var(--sf-space-3)); }
|
|
717
|
+
[part="toolbar"] { display: flex; flex-wrap: wrap; gap: var(--sf-data-view-toolbar-gap, var(--sf-space-2)); }
|
|
718
|
+
/* Hidden means hidden: the toolbar's own display beat the hidden
|
|
719
|
+
attribute, and a related list in a record panel (chrome="")
|
|
720
|
+
showed the bare fallback search above its rows (Larena,
|
|
721
|
+
2026-10-14). */
|
|
722
|
+
[hidden] { display: none !important; }
|
|
723
|
+
/* Files from the computer over a view that takes them (accept-files):
|
|
724
|
+
a dashed zone over the list says where they go. */
|
|
725
|
+
[part="drop"] {
|
|
726
|
+
position: absolute; inset: 0; z-index: 20;
|
|
727
|
+
display: grid; place-items: center;
|
|
728
|
+
border: var(--sf-focus--width, 2px) dashed var(--sf-data-view-drop-border-color, var(--sf-primary));
|
|
729
|
+
border-radius: var(--sf-radius--surface);
|
|
730
|
+
background: var(--sf-data-view-drop-background-color, color-mix(in srgb, var(--sf-primary-container) 80%, transparent));
|
|
731
|
+
color: var(--sf-data-view-drop-color, var(--sf-on-primary-container));
|
|
732
|
+
font: var(--sf-font-weight-medium) var(--sf-text-size-1) / var(--sf-text-height-1) var(--sf-text--family);
|
|
733
|
+
pointer-events: none;
|
|
734
|
+
}
|
|
735
|
+
/* What the view is doing is announced, not laid out. It used to be
|
|
736
|
+
a line above the table, which said "Нет данных" over a table
|
|
737
|
+
whose own body says the same, and pushed everything down by its
|
|
738
|
+
height every time a search found nothing. */
|
|
739
|
+
[part="status"] {
|
|
740
|
+
position: absolute;
|
|
741
|
+
inline-size: var(--sf-px);
|
|
742
|
+
block-size: var(--sf-px);
|
|
743
|
+
margin: calc(var(--sf-px) * -1);
|
|
744
|
+
padding: 0;
|
|
745
|
+
overflow: hidden;
|
|
746
|
+
clip-path: inset(50%);
|
|
747
|
+
white-space: nowrap;
|
|
748
|
+
border: 0;
|
|
749
|
+
}
|
|
750
|
+
/* A notice is a message about an action. It used to float over the
|
|
751
|
+
top of the card, which moved nothing but covered the search, the
|
|
752
|
+
create button and the filters -- the consumer was right that this
|
|
753
|
+
is worse than a shifted table. It stands in the corner of the
|
|
754
|
+
window now, over the page and over none of the controls, and it
|
|
755
|
+
goes away by itself. Slotted from the light DOM because the
|
|
756
|
+
Framework's styles -- sf-toast's among them -- do not cross a
|
|
757
|
+
shadow boundary. */
|
|
758
|
+
slot[name="notice"]::slotted(*) {
|
|
759
|
+
position: fixed;
|
|
760
|
+
inset-block-end: var(--sf-space-3);
|
|
761
|
+
inset-inline-end: var(--sf-space-3);
|
|
762
|
+
max-inline-size: var(--sf-g0);
|
|
763
|
+
/* One above the menus: they share the top step, and a refusal
|
|
764
|
+
answering a menu choice opened under the menu that asked. */
|
|
765
|
+
z-index: calc(var(--sf-z-index-9, 90) + 1);
|
|
766
|
+
}
|
|
767
|
+
/* The band of bulk controls is the last strip of the card and holds
|
|
768
|
+
the bottom of the window while the card runs past it. It is
|
|
769
|
+
slotted rather than built in here: the Framework's own styles do
|
|
770
|
+
not cross a shadow boundary, so the band lives in the light DOM
|
|
771
|
+
where they reach it, and this layout is what contains it. */
|
|
772
|
+
slot[name="bulk"]::slotted(*) {
|
|
773
|
+
position: sticky;
|
|
774
|
+
inset-block-end: 0;
|
|
775
|
+
z-index: var(--sf-z-index-3, 30);
|
|
776
|
+
}
|
|
777
|
+
/* The first paint (standard 1.3.8 §5.4): until the host's first
|
|
778
|
+
answer the view is its skeleton, slotted from the light DOM
|
|
779
|
+
where a server printed it (static/<template>/<example>.html)
|
|
780
|
+
or where the view drew it, and the layout stands back. */
|
|
781
|
+
:host([data-sf-awaiting]) [part="layout"] { display: none; }
|
|
782
|
+
:host(:not([data-sf-awaiting])) slot[name="first-paint"] { display: none; }
|
|
783
|
+
</style>
|
|
784
|
+
<slot name="first-paint"></slot>
|
|
785
|
+
<section part="layout" style="position: relative">
|
|
786
|
+
<div part="toolbar"><slot name="toolbar">
|
|
787
|
+
<label>Поиск <input type="search" data-search></label>
|
|
788
|
+
<!-- Saved views are offered only when the host says it keeps them.
|
|
789
|
+
They were offered always, and a host that claims neither
|
|
790
|
+
saved-views nor shared-saved-views answers every one of these
|
|
791
|
+
with capability-not-provided -- so «Сохранить вид» could do
|
|
792
|
+
nothing but refuse, which is not a control, it is a trap. The
|
|
793
|
+
owner met them on a page whose host claims only row-actions
|
|
794
|
+
(owner, 2026-10-09). -->
|
|
795
|
+
<span data-saved-views hidden>
|
|
796
|
+
<label>Вид <select data-view><option value="">Текущий</option></select></label>
|
|
797
|
+
<label>Название вида <input type="text" data-view-name></label>
|
|
798
|
+
<button type="button" data-save>Сохранить вид</button>
|
|
799
|
+
<button type="button" data-delete>Удалить вид</button>
|
|
800
|
+
</span>
|
|
801
|
+
</slot></div>
|
|
802
|
+
<div part="status" role="status" aria-live="polite" hidden></div>
|
|
803
|
+
<div part="drop" hidden></div>
|
|
804
|
+
<slot name="content"><sf-view-table toolbar="false"></sf-view-table></slot>
|
|
805
|
+
<slot name="show-all"></slot>
|
|
806
|
+
<slot name="pagination"><sf-pagination top="false" bottom="false"></sf-pagination></slot>
|
|
807
|
+
<slot name="bulk"></slot>
|
|
808
|
+
<slot name="notice"></slot>
|
|
809
|
+
<slot name="dialog"></slot>
|
|
810
|
+
</section>`;
|
|
811
|
+
} // ---------- the manifest's values and settings (§4) ----------
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
get total() {
|
|
815
|
+
return this._total ?? 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
set total(value) {
|
|
819
|
+
this._total = value;
|
|
820
|
+
} // The vocabulary's data state of the list (§3): ready for populated,
|
|
821
|
+
// refused for no access.
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
get state() {
|
|
825
|
+
if (this._queryRefused) return "refused";
|
|
826
|
+
return DATA_STATES[this._dataState] ?? "idle";
|
|
827
|
+
} // The row the page has open, marked in the table (aria-current).
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
get current() {
|
|
831
|
+
return this._current ?? "";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
set current(id) {
|
|
835
|
+
this._current = id === null || id === undefined ? "" : String(id);
|
|
836
|
+
if (this.table) this.table.current = this._current;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
get selected() {
|
|
840
|
+
return this._current || undefined;
|
|
841
|
+
} // The actions offered for the marked rows, [{id, label}] (component.json
|
|
842
|
+
// settings.bulk-actions). A page writes them on its page row's markup
|
|
843
|
+
// (actions="publish:Опубликовать|…"); an assembly that places the bare view
|
|
844
|
+
// gives them here, their words from its language file (§11.3), and the view
|
|
845
|
+
// hands them to its page row.
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
get bulkActions() {
|
|
849
|
+
return this._bulkActions ?? [];
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
set bulkActions(list) {
|
|
853
|
+
this._bulkActions = Array.isArray(list) ? list.filter(item => item && (item.id ?? item.value)) : [];
|
|
854
|
+
|
|
855
|
+
this._applyBulkActions();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
_applyBulkActions() {
|
|
859
|
+
const pagination = this.pagination;
|
|
860
|
+
if (!pagination || !this._bulkActions?.length) return;
|
|
861
|
+
|
|
862
|
+
const clean = text => String(text ?? "").replace(/[|:]/gu, " ").trim();
|
|
863
|
+
|
|
864
|
+
const markup = this._bulkActions.map(item => `${clean(item.id ?? item.value)}:${clean(item.label ?? item.text ?? item.id ?? item.value)}`).join("|");
|
|
865
|
+
|
|
866
|
+
if (pagination.getAttribute("actions") !== markup) pagination.setAttribute("actions", markup);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
get selection() {
|
|
870
|
+
return this.table?.selection ?? [];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
get table() {
|
|
874
|
+
const slot = this.shadowRoot.querySelector('slot[name="content"]');
|
|
875
|
+
return slot.assignedElements().find(element => (0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.isTag)(element, "sf-view-table")) ?? slot.querySelector((0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table"));
|
|
876
|
+
} // The band of bulk controls, kept in the light DOM so the Framework styles
|
|
877
|
+
// reach it, and assigned to the layout's own slot so the card contains it.
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
get bulkBand() {
|
|
881
|
+
if (this._bulkBand?.isConnected) return this._bulkBand;
|
|
882
|
+
const existing = this.querySelector(':scope > [slot="bulk"]');
|
|
883
|
+
this._bulkBand = existing ?? document.createElement("div");
|
|
884
|
+
|
|
885
|
+
this._bulkBand.setAttribute("slot", "bulk");
|
|
886
|
+
|
|
887
|
+
this._bulkBand.classList.add("sf-pagination-actions", "flex", "items-cross-center");
|
|
888
|
+
|
|
889
|
+
this._bulkBand.setAttribute("role", "region");
|
|
890
|
+
|
|
891
|
+
if (!this._bulkBand.isConnected) this.append(this._bulkBand);
|
|
892
|
+
return this._bulkBand;
|
|
893
|
+
} // The show-all row, in the light DOM for the same reason the bulk band and the
|
|
894
|
+
// notice are: the Framework's stylesheets do not cross a shadow boundary, so a
|
|
895
|
+
// control drawn inside the shadow comes out as a native grey button. A consumer
|
|
896
|
+
// reported exactly that beside «5 of 61», and it is the same trap that had an
|
|
897
|
+
// embedded table showing its icon ligatures as text.
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
get showAllBand() {
|
|
901
|
+
if (this._showAll?.isConnected) return this._showAll;
|
|
902
|
+
const existing = this.querySelector(':scope > [slot="show-all"]');
|
|
903
|
+
this._showAll = existing ?? document.createElement("div");
|
|
904
|
+
|
|
905
|
+
this._showAll.setAttribute("slot", "show-all");
|
|
906
|
+
|
|
907
|
+
this._showAll.classList.add("flex", "items-cross-center", "gap-1/3"); // The click is listened for on the band rather than on this element. A
|
|
908
|
+
// listener on the element would see every click inside it, and the handler
|
|
909
|
+
// below branches on [data-save] and [data-delete] as well -- a row that
|
|
910
|
+
// happened to carry one of those would have reached the wrong branch.
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
if (this._showAll.getAttribute("data-sf-bound") !== "1") {
|
|
914
|
+
this._showAll.setAttribute("data-sf-bound", "1");
|
|
915
|
+
|
|
916
|
+
this._showAll.addEventListener("click", this._bound);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (!this._showAll.isConnected) this.append(this._showAll);
|
|
920
|
+
return this._showAll;
|
|
921
|
+
} // The notice: one alert at a time, in the light DOM so the Framework's own
|
|
922
|
+
// styles reach it, assigned to the layout's slot so the card holds it.
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
get noticeBand() {
|
|
926
|
+
if (this._notice?.isConnected) return this._notice;
|
|
927
|
+
const existing = this.querySelector(':scope > [slot="notice"]');
|
|
928
|
+
this._notice = existing ?? document.createElement("div");
|
|
929
|
+
|
|
930
|
+
this._notice.setAttribute("slot", "notice");
|
|
931
|
+
|
|
932
|
+
if (!this._notice.isConnected) this.append(this._notice);
|
|
933
|
+
return this._notice;
|
|
934
|
+
} // What a reason code says to a person. A host that sends its own `message`
|
|
935
|
+
// is believed; a reason this component knows is translated; anything else
|
|
936
|
+
// is a plain sentence, with the code kept in the title for the people who
|
|
937
|
+
// read codes. A code is never the message: the consumer saw
|
|
938
|
+
// `minimal_cms_action_rejected` where a sentence belonged.
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
_noticeText(answer = {}) {
|
|
942
|
+
const message = hostMessage(answer);
|
|
943
|
+
if (message) return message; // The host's own sentence wins; these are what to say when it sent
|
|
944
|
+
// none. They follow the page's language like every other word.
|
|
945
|
+
|
|
946
|
+
const keys = {
|
|
947
|
+
"action-not-allowed": "noticeActionNotAllowed",
|
|
948
|
+
"capability-not-provided": "noticeCapabilityNotProvided",
|
|
949
|
+
"port-not-connected": "noticePortNotConnected",
|
|
950
|
+
"saved-view-levels-require-port-1.1.0": "noticeSavedViewLevels",
|
|
951
|
+
// The rest of the reasons simai.dataview-port 1.12.0 lists: each
|
|
952
|
+
// has its own words rather than the answer's general one.
|
|
953
|
+
"confirm_required": "noticeConfirmRequired",
|
|
954
|
+
"not-confirmed": "noticeNotConfirmed",
|
|
955
|
+
"no-base-revision": "noticeNoBaseRevision",
|
|
956
|
+
"record_invalid": "noticeRecordInvalid",
|
|
957
|
+
"forbidden": "noticeForbidden",
|
|
958
|
+
"stale-revision": "noticeStaleRevision"
|
|
959
|
+
};
|
|
960
|
+
if (keys[answer.reason]) return (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, keys[answer.reason], this);
|
|
961
|
+
const byAnswer = {
|
|
962
|
+
conflict: "noticeConflict",
|
|
963
|
+
refused: "noticeRefused",
|
|
964
|
+
unavailable: "noticeUnavailable"
|
|
965
|
+
}[answer.answer];
|
|
966
|
+
return (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, byAnswer ?? "noticeFailed", this);
|
|
967
|
+
} // Shown over the card, not in it. `role` follows what happened: an error is
|
|
968
|
+
// an alert, a refusal is a message.
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
notify(text, {
|
|
972
|
+
kind = "error",
|
|
973
|
+
code = ""
|
|
974
|
+
} = {}) {
|
|
975
|
+
// The page's one notification area (SF.notify, Framework
|
|
976
|
+
// ui-82fc0ba882ba): the view's messages go where every other message
|
|
977
|
+
// goes, and a notification centre records them from sf-notification
|
|
978
|
+
// (owner, 2026-10-13). A clear closes the view's last message there.
|
|
979
|
+
// A Framework without SF.notify keeps the band over the card.
|
|
980
|
+
if (typeof globalThis.SF?.notify === "function") {
|
|
981
|
+
if (!text) {
|
|
982
|
+
this._notifyHandle?.close?.();
|
|
983
|
+
this._notifyHandle = null;
|
|
984
|
+
return this;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
this._notifyHandle = globalThis.SF.notify({
|
|
988
|
+
status: kind === "error" ? "error" : "warning",
|
|
989
|
+
title: text,
|
|
990
|
+
// Long enough to read a sentence, as the band's was.
|
|
991
|
+
duration: 7000,
|
|
992
|
+
source: code ? `sf-view:${code}` : "sf-view",
|
|
993
|
+
key: code ? `sf-view:${code}` : undefined
|
|
994
|
+
});
|
|
995
|
+
return this;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const band = this.noticeBand;
|
|
999
|
+
band.replaceChildren();
|
|
1000
|
+
|
|
1001
|
+
if (!text) {
|
|
1002
|
+
band.remove();
|
|
1003
|
+
this._notice = null;
|
|
1004
|
+
return this;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
this._askForParts(["sf-toast"]);
|
|
1008
|
+
|
|
1009
|
+
const toast = document.createElement("sf-toast");
|
|
1010
|
+
toast.setAttribute("status", kind === "error" ? "error" : "warning");
|
|
1011
|
+
toast.setAttribute("closable", "true");
|
|
1012
|
+
toast.setAttribute("close-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "closeNoticeLabel", this));
|
|
1013
|
+
toast.setAttribute("role", kind === "error" ? "alert" : "status");
|
|
1014
|
+
toast.setAttribute("heading", text); // Long enough to read a sentence, and it can be closed at once. A
|
|
1015
|
+
// message that stays until it is dismissed ends up covering something,
|
|
1016
|
+
// which is how the overlay before it went wrong.
|
|
1017
|
+
|
|
1018
|
+
toast.setAttribute("duration", "7000"); // The code is for the people who read codes, and they read it in the
|
|
1019
|
+
// inspector: shown as a second line it was a raw identifier under a
|
|
1020
|
+
// sentence, which the owner read as noise (2026-10-12).
|
|
1021
|
+
|
|
1022
|
+
if (code) toast.setAttribute("data-sf-code", code); // Closed, not asked to close: sf-toast-close is the cancelable request now.
|
|
1023
|
+
|
|
1024
|
+
toast.addEventListener("sf-toast-closed", () => this.notify(""));
|
|
1025
|
+
band.append(toast);
|
|
1026
|
+
return this;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
get pagination() {
|
|
1030
|
+
const slot = this.shadowRoot.querySelector('slot[name="pagination"]');
|
|
1031
|
+
return slot.assignedElements().find(element => element.matches("sf-pagination")) ?? slot.querySelector("sf-pagination");
|
|
1032
|
+
} // Files from the computer dropped on the view (accept-files, owner
|
|
1033
|
+
// 2026-10-15, the file manager) are the host's to upload: the view says
|
|
1034
|
+
// so with sf-data-view-files-drop {files}. They do not go through the
|
|
1035
|
+
// port, whose payloads are JSON.
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
_bindFileDrop() {
|
|
1039
|
+
if (this._fileDropBound) return;
|
|
1040
|
+
this._fileDropBound = true;
|
|
1041
|
+
|
|
1042
|
+
const zone = () => this.shadowRoot.querySelector('[part="drop"]');
|
|
1043
|
+
|
|
1044
|
+
const carriesFiles = event => this.hasAttribute("accept-files") && [...(event.dataTransfer?.types ?? [])].includes("Files");
|
|
1045
|
+
|
|
1046
|
+
const show = on => {
|
|
1047
|
+
const node = zone();
|
|
1048
|
+
if (!node) return;
|
|
1049
|
+
node.textContent = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "dropFiles", this);
|
|
1050
|
+
node.hidden = !on;
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
this.addEventListener("dragover", event => {
|
|
1054
|
+
if (!carriesFiles(event)) return;
|
|
1055
|
+
event.preventDefault();
|
|
1056
|
+
event.dataTransfer.dropEffect = "copy";
|
|
1057
|
+
show(true);
|
|
1058
|
+
});
|
|
1059
|
+
this.addEventListener("dragleave", event => {
|
|
1060
|
+
if (!this.contains(event.relatedTarget) && !this.shadowRoot.contains(event.relatedTarget)) show(false);
|
|
1061
|
+
});
|
|
1062
|
+
this.addEventListener("drop", event => {
|
|
1063
|
+
if (!carriesFiles(event)) return;
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
show(false);
|
|
1066
|
+
const files = [...event.dataTransfer.files];
|
|
1067
|
+
if (files.length) this.dispatchEvent(new CustomEvent("sf-view-files-drop", {
|
|
1068
|
+
bubbles: true,
|
|
1069
|
+
composed: true,
|
|
1070
|
+
detail: {
|
|
1071
|
+
files
|
|
1072
|
+
}
|
|
1073
|
+
}));
|
|
1074
|
+
});
|
|
1075
|
+
} // A view placed with no table and no page row of the page's own (an
|
|
1076
|
+
// assembly places the bare tag) makes them as its light children, slotted,
|
|
1077
|
+
// where the page's stylesheets reach them: the fallbacks inside the shadow
|
|
1078
|
+
// root are drawn without the table's sheet. The documented markup gives
|
|
1079
|
+
// them, and then nothing is made.
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
_ownParts() {
|
|
1083
|
+
if (!this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"], :scope > [slot="content"]`)) {
|
|
1084
|
+
const table = document.createElement("sf-view-table");
|
|
1085
|
+
table.slot = "content";
|
|
1086
|
+
table.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "recordsLabel", this));
|
|
1087
|
+
table.toggleAttribute("data-sf-view-made", true);
|
|
1088
|
+
this.append(table);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (!this.querySelector(':scope > [slot="pagination"]')) {
|
|
1092
|
+
const pagination = document.createElement("sf-pagination");
|
|
1093
|
+
pagination.slot = "pagination";
|
|
1094
|
+
pagination.setAttribute("top", "false");
|
|
1095
|
+
pagination.setAttribute("bottom", "false");
|
|
1096
|
+
pagination.toggleAttribute("data-sf-view-made", true);
|
|
1097
|
+
this.append(pagination);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
this._applyBulkActions();
|
|
1101
|
+
|
|
1102
|
+
this._applyForAll();
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
connectedCallback() {
|
|
1106
|
+
this._readPrintedPageSize();
|
|
1107
|
+
|
|
1108
|
+
super.connectedCallback();
|
|
1109
|
+
if (this.getAttribute("data-sf-instance")) this._ownParts();
|
|
1110
|
+
|
|
1111
|
+
this._askForParts(["sf-view-table", "sf-pagination"]); // Dated aliases (§16.20), installed once per element as the Smart base does.
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
if (!this.__sfAliases) this.__sfAliases = (0,_core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__.installAliases)(this, _aliases_json__WEBPACK_IMPORTED_MODULE_7__, {
|
|
1115
|
+
tag: this.localName,
|
|
1116
|
+
adapters: {}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
this._bindFileDrop();
|
|
1120
|
+
|
|
1121
|
+
this.epoch += 1; // The key reaches the table before its first paint.
|
|
1122
|
+
|
|
1123
|
+
this._applyPreferences();
|
|
1124
|
+
|
|
1125
|
+
this._controller = new AbortController();
|
|
1126
|
+
const events = [...Object.keys(EVENTS), ...LISTENED];
|
|
1127
|
+
|
|
1128
|
+
for (const name of events) this.shadowRoot.addEventListener(name, this._bound);
|
|
1129
|
+
|
|
1130
|
+
for (const name of events) this.addEventListener(name, this._bound);
|
|
1131
|
+
|
|
1132
|
+
this.shadowRoot.querySelector("[data-search]").addEventListener("change", this._bound);
|
|
1133
|
+
this.shadowRoot.querySelector("[data-view]").addEventListener("change", this._bound);
|
|
1134
|
+
this.shadowRoot.querySelector("[data-save]").addEventListener("click", this._bound);
|
|
1135
|
+
this.shadowRoot.querySelector("[data-delete]").addEventListener("click", this._bound);
|
|
1136
|
+
|
|
1137
|
+
this._readDeclaredPort();
|
|
1138
|
+
|
|
1139
|
+
this._seedPageSize();
|
|
1140
|
+
|
|
1141
|
+
this._seedPrintedQuery();
|
|
1142
|
+
|
|
1143
|
+
this._renderProjection();
|
|
1144
|
+
|
|
1145
|
+
this._firstPaintReady = true;
|
|
1146
|
+
|
|
1147
|
+
this._drawFirstAnswer();
|
|
1148
|
+
|
|
1149
|
+
this._startFirstPaint();
|
|
1150
|
+
|
|
1151
|
+
if (this.hostPort) this.refresh("connected");else this._requestHostPort();
|
|
1152
|
+
} // ---------- the first paint (standard 1.3.8 §5.4) ----------
|
|
1153
|
+
// The rows the skeleton holds: the page the query asks for (a kept page
|
|
1154
|
+
// size, else the page size the page printed on its page row), no more than
|
|
1155
|
+
// the cap of an embedded list; else the cap; else a page of the table's
|
|
1156
|
+
// (sf-view-table pagination-page-size, 10). A skeleton of 10 rows over a page
|
|
1157
|
+
// of 20 grows the list when the rows come (stand 25 D1).
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
get firstPaintRows() {
|
|
1161
|
+
const asked = this._declaredPageSize();
|
|
1162
|
+
|
|
1163
|
+
const cap = this.rowCap;
|
|
1164
|
+
if (asked && cap) return Math.min(asked, cap);
|
|
1165
|
+
return asked || cap || FIRST_PAINT_ROWS;
|
|
1166
|
+
} // The page size somebody declared: the person's kept one, a first answer's
|
|
1167
|
+
// echo, or the page row's page-size as the page printed it. Zero when
|
|
1168
|
+
// nobody did -- the query's own 25 is the view's default, not a request.
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
_declaredPageSize() {
|
|
1172
|
+
if (this._pageSizeDeclared) return Number(this.query.per_page) || 0;
|
|
1173
|
+
return this._printedPageSize || 0;
|
|
1174
|
+
} // The page size the page printed on its page row, read once, before
|
|
1175
|
+
// anything of the view's writes its own page size there (the page row's
|
|
1176
|
+
// state, from the first projection on).
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
_readPrintedPageSize() {
|
|
1180
|
+
if (this._printedPageSize !== undefined) return;
|
|
1181
|
+
const row = this.querySelector?.(':scope > sf-pagination[slot="pagination"]');
|
|
1182
|
+
if (!row) return;
|
|
1183
|
+
const printed = Number.parseInt(row.getAttribute("page-size") ?? "", 10);
|
|
1184
|
+
this._printedPageSize = Number.isInteger(printed) && printed > 0 ? printed : 0;
|
|
1185
|
+
} // The first query asks for the page the page printed (its page row's
|
|
1186
|
+
// page-size), unless the person kept another: the skeleton, the request
|
|
1187
|
+
// and the page row then agree on one number before the host answers.
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
_seedPageSize() {
|
|
1191
|
+
if (this._pageSizeDeclared || this._answered) return;
|
|
1192
|
+
|
|
1193
|
+
const size = this._declaredPageSize();
|
|
1194
|
+
|
|
1195
|
+
if (!size) return;
|
|
1196
|
+
this.query.per_page = size;
|
|
1197
|
+
this._pageSizeDeclared = true;
|
|
1198
|
+
} // The host's first answer printed with the page (settings.first-answer,
|
|
1199
|
+
// a data-sf-settings island): the shape of query.change's applied data --
|
|
1200
|
+
// records (each with its revision), total or has_next, echo, columns. The
|
|
1201
|
+
// view draws it at once, as if the host had answered, so rows the server
|
|
1202
|
+
// already has are usable before the port; the port then answers the same
|
|
1203
|
+
// query in the background (refresh "connected", no loading state) and its
|
|
1204
|
+
// answer replaces this one. Taken once: a later answer is the port's.
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
get firstAnswer() {
|
|
1208
|
+
return this._firstAnswer ?? null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
set firstAnswer(data) {
|
|
1212
|
+
this._firstAnswer = data && typeof data === "object" && !Array.isArray(data) && Array.isArray(data.records) ? copy(data) : null; // Set after the view connected (not from its island, which is read
|
|
1213
|
+
// before the view is ready to draw): drawn now.
|
|
1214
|
+
|
|
1215
|
+
if (this._firstPaintReady && this.isConnected && this._firstAnswer && !this._answered) {
|
|
1216
|
+
this._drawFirstAnswer();
|
|
1217
|
+
|
|
1218
|
+
if (this.hostPort) this.refresh("connected");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
_drawFirstAnswer() {
|
|
1223
|
+
const data = this._firstAnswer;
|
|
1224
|
+
if (!data || this._answered) return false;
|
|
1225
|
+
this._firstAnswer = null; // A printed answer to another query than the person's saved state is
|
|
1226
|
+
// not drawn: the skeleton stays and the view asks its own query.
|
|
1227
|
+
|
|
1228
|
+
if (!this._printedAnswerFits(data)) return false;
|
|
1229
|
+
if (data.echo && Number.isInteger(data.echo.per_page) && data.echo.per_page > 0) this._pageSizeDeclared = true;
|
|
1230
|
+
this._revalidate = true; // Kept to compare the port's answer to the same query with.
|
|
1231
|
+
|
|
1232
|
+
this._printedAnswer = copy(data); // The capabilities the page printed with its answer, when it did not
|
|
1233
|
+
// declare its port: known before the first draw (§18.4).
|
|
1234
|
+
|
|
1235
|
+
if (!this._declaredPort && Array.isArray(data.capabilities)) {
|
|
1236
|
+
this._declaredPort = {
|
|
1237
|
+
version: data.version ?? null,
|
|
1238
|
+
capabilities: data.capabilities.map(String)
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const asked = copy(this.query);
|
|
1243
|
+
this.applyProjection(data); // The revalidation asks the query the page printed, not the answer's
|
|
1244
|
+
// echo: a host whose default (a sort) is echoed differently would
|
|
1245
|
+
// otherwise be asked another question (stand 27 D2). Without a printed
|
|
1246
|
+
// query it asks what the view would have asked.
|
|
1247
|
+
|
|
1248
|
+
if (!this._seedPrintedQuery({
|
|
1249
|
+
answer: data
|
|
1250
|
+
})) this.query = asked; // A printed skeleton the server sent as well goes with the first
|
|
1251
|
+
// answer, in the same frame.
|
|
1252
|
+
|
|
1253
|
+
this.removeAttribute("data-sf-awaiting");
|
|
1254
|
+
|
|
1255
|
+
for (const node of this._firstPaintNodes()) node.remove?.();
|
|
1256
|
+
|
|
1257
|
+
return true;
|
|
1258
|
+
} // The port the page declared in the element (standard §4.9, stand 27 c):
|
|
1259
|
+
// <script type="application/json" data-sf-port> with port, version, url,
|
|
1260
|
+
// capabilities and first {intent, payload}. Core may take it out and
|
|
1261
|
+
// connect the port itself; read here, it is what the view knows of its
|
|
1262
|
+
// port before the port: its capabilities, so the first draw is final
|
|
1263
|
+
// (the toolbar and the row actions drawn once), and its first query.
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
_readDeclaredPort() {
|
|
1267
|
+
if (this._declaredPort !== undefined) return;
|
|
1268
|
+
const island = Array.from(this.children ?? []).find(child => child.localName === "script" && child.hasAttribute?.("data-sf-port") && /^application\/json$/iu.test(child.type));
|
|
1269
|
+
this._declaredPort = null;
|
|
1270
|
+
if (!island) return;
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const declared = JSON.parse(island.textContent || "{}");
|
|
1274
|
+
if (!declared || typeof declared !== "object") return;
|
|
1275
|
+
this._declaredPort = {
|
|
1276
|
+
version: declared.version ?? null,
|
|
1277
|
+
capabilities: Array.isArray(declared.capabilities) ? declared.capabilities.map(String) : [],
|
|
1278
|
+
first: declared.first && typeof declared.first === "object" ? copy(declared.first) : null
|
|
1279
|
+
};
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
console.warn("[sf-view] the data-sf-port island is not JSON", error);
|
|
1282
|
+
}
|
|
1283
|
+
} // The port as the view knows it: the connected one, else the one the page
|
|
1284
|
+
// declared (or printed beside its first answer).
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
get knownPort() {
|
|
1288
|
+
return this.hostPort ?? this._declaredPort ?? null;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
knownCapability(capability) {
|
|
1292
|
+
return Boolean(this.knownPort?.capabilities?.includes?.(capability));
|
|
1293
|
+
} // The query the page printed: the first answer's own (first-answer.query),
|
|
1294
|
+
// else the declared port's first query.change. The view asks exactly that,
|
|
1295
|
+
// so an early call the page started is the view's own (one request) and
|
|
1296
|
+
// the revalidation of a printed answer asks what was printed.
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
_seedPrintedQuery({
|
|
1300
|
+
answer = null
|
|
1301
|
+
} = {}) {
|
|
1302
|
+
const declared = this._declaredPort?.first;
|
|
1303
|
+
const printed = answer?.query && typeof answer.query === "object" ? answer.query : declared?.intent === "query.change" && declared.payload?.query && typeof declared.payload.query === "object" ? declared.payload.query : null;
|
|
1304
|
+
if (!printed) return false; // The printed query is the defaults; what the person saved in this
|
|
1305
|
+
// browser goes over it. The view asks its real state: an early call
|
|
1306
|
+
// started with the printed query is then taken only when it matches
|
|
1307
|
+
// (the adapter compares), otherwise the view's own query is asked.
|
|
1308
|
+
|
|
1309
|
+
this.query = { ...this.query,
|
|
1310
|
+
...copy(printed),
|
|
1311
|
+
...copy(this._keptState ?? {})
|
|
1312
|
+
};
|
|
1313
|
+
if (Number.isInteger(this.query.per_page) && this.query.per_page > 0) this._pageSizeDeclared = true;
|
|
1314
|
+
return true;
|
|
1315
|
+
} // Whether a printed answer answers the view's real query: not when the
|
|
1316
|
+
// person saved a page size or a sort in this browser that the printed
|
|
1317
|
+
// query (or the answer's echo) does not have. Drawn anyway, its rows
|
|
1318
|
+
// would show and then be replaced by the real query's (25 rows, then 50).
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
_printedAnswerFits(data) {
|
|
1322
|
+
const kept = this._keptState ?? {};
|
|
1323
|
+
if (!Object.keys(kept).length) return true;
|
|
1324
|
+
const printed = data?.query && typeof data.query === "object" ? data.query : data?.echo ?? {};
|
|
1325
|
+
if (kept.per_page !== undefined && Number(printed.per_page) !== kept.per_page) return false;
|
|
1326
|
+
if (kept.sort !== undefined && canonical(printed.sort ?? []) !== canonical(kept.sort)) return false;
|
|
1327
|
+
return true;
|
|
1328
|
+
} // How many columns the skeleton stands for (expected-columns): the shape
|
|
1329
|
+
// of the first paint, never a limit -- the host's answer brings its own
|
|
1330
|
+
// columns. The page or server that renders a collection knows them.
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
get expectedColumns() {
|
|
1334
|
+
const declared = Number.parseInt(this.getAttribute("expected-columns") ?? "", 10);
|
|
1335
|
+
return Number.isFinite(declared) && declared > 0 ? declared : 4;
|
|
1336
|
+
} // Until the host's first answer the view shows its skeleton: the shape the
|
|
1337
|
+
// list will have once the answer comes, so nothing moves when it does
|
|
1338
|
+
// (standard 1.3.8 §5.4, gate 24). The toolbar and the page row as the data
|
|
1339
|
+
// draws them; the table's head and a row per row it will draw (row-cap,
|
|
1340
|
+
// else a page of 10), a cell per expected column, with the chrome columns
|
|
1341
|
+
// (select, settings) the table will have; and the compact list the table
|
|
1342
|
+
// turns into below its list-below width. The table chooses that in code,
|
|
1343
|
+
// by measuring its container; a printed skeleton cannot measure, so the
|
|
1344
|
+
// choice is the sheet's: a container query at the same width (sf-view-table's
|
|
1345
|
+
// sheet, .sf-data-view-skeleton). It is light DOM, so a server prints it
|
|
1346
|
+
// as the view's static markup and the page draws it before any code; the
|
|
1347
|
+
// view adopts what was printed (its data-sf-static mark goes) rather than
|
|
1348
|
+
// drawing a second one beside it.
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
_startFirstPaint() {
|
|
1352
|
+
if (this._answered) return;
|
|
1353
|
+
this.setAttribute("data-sf-awaiting", "");
|
|
1354
|
+
if (this.getAttribute("aria-busy") === null) this.setAttribute("aria-busy", "true");
|
|
1355
|
+
|
|
1356
|
+
const printed = this._firstPaintNodes();
|
|
1357
|
+
|
|
1358
|
+
if (printed.length) {
|
|
1359
|
+
for (const node of printed) node.removeAttribute?.("data-sf-static");
|
|
1360
|
+
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (typeof document === "undefined") return;
|
|
1365
|
+
const skeleton = document.createElement("div");
|
|
1366
|
+
skeleton.setAttribute("slot", "first-paint");
|
|
1367
|
+
skeleton.setAttribute("aria-hidden", "true");
|
|
1368
|
+
skeleton.className = "sf-data-view-skeleton sf-skeleton sf-skeleton--animation-pulse";
|
|
1369
|
+
skeleton.setAttribute("data-size", this.size);
|
|
1370
|
+
skeleton.innerHTML = this._firstPaintMarkup(skeleton);
|
|
1371
|
+
this.append(skeleton);
|
|
1372
|
+
} // The skeleton's markup, kept small (it is printed in every page that
|
|
1373
|
+
// shows the view): a bar is an <i class="sf-skeleton-text">, a control
|
|
1374
|
+
// block one marked data-control; the lengths and the bars' widths are the
|
|
1375
|
+
// sheet's (sf-view-table's sheet, .sf-data-view-skeleton).
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
_firstPaintMarkup(skeleton) {
|
|
1379
|
+
const table = this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"]`);
|
|
1380
|
+
const bar = '<i class="sf-skeleton-text"></i>';
|
|
1381
|
+
|
|
1382
|
+
const control = (kind = "") => `<i class="sf-skeleton-text" data-control${kind ? `="${kind}"` : ""}></i>`; // What the data will draw: the table's toolbar unless the page turned
|
|
1383
|
+
// it off (without a table of the page's, the view's own as its chrome
|
|
1384
|
+
// asks), the chrome columns, and a table or the compact list.
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
const toolbar = table ? table.getAttribute("toolbar") !== "false" : this.renders("toolbar");
|
|
1388
|
+
const ownToolbar = Boolean(table) && this.renders("toolbar") && Boolean(this.querySelector(':scope > [slot="toolbar"]'));
|
|
1389
|
+
const select = this.renders("select") && !hostWroteOff(table, "selectable");
|
|
1390
|
+
const settings = this.renders("settings") && !hostWroteOff(table, "settings");
|
|
1391
|
+
const layout = table?.getAttribute("layout") || (this.renders("toolbar") ? "auto" : "table");
|
|
1392
|
+
skeleton.setAttribute("data-layout", ["auto", "table", "list"].includes(layout) ? layout : "table");
|
|
1393
|
+
const columns = this.expectedColumns;
|
|
1394
|
+
const rows = this.firstPaintRows;
|
|
1395
|
+
const listFields = Number.parseInt(table?.getAttribute("list-fields") ?? "", 10);
|
|
1396
|
+
const fields = Math.min(columns - 1, Number.isFinite(listFields) && listFields >= 0 ? listFields : FIRST_PAINT_LIST_FIELDS);
|
|
1397
|
+
|
|
1398
|
+
const row = head => `<div>${select ? `<span data-chrome>${bar}</span>` : ""}${settings ? `<span data-chrome>${head ? bar : control()}</span>` : ""}${`<span>${bar}</span>`.repeat(columns)}</div>`;
|
|
1399
|
+
|
|
1400
|
+
const record = `<div>${select ? bar : "<span></span>"}<div>${bar.repeat(fields + 1)}</div>${settings ? control() : "<span></span>"}</div>`;
|
|
1401
|
+
const pageRow = `<nav class="sf-pagination sf-pagination--size-${this.size} flex flex-col"><div class="sf-pagination-main flex items-cross-center content-main-between">` + `<div class="sf-pagination-left flex items-cross-center">${bar}</div>` + `<div class="sf-pagination-container flex items-cross-center"><span class="sf-pagination-pages-label">${bar}</span>${control()}</div>` + `<div class="sf-pagination-count flex items-cross-center">${bar}${control("field")}</div>` + "</div></nav>";
|
|
1402
|
+
return [// An own toolbar slot is a row of the view's layout, even empty.
|
|
1403
|
+
ownToolbar ? '<div class="sf-data-view-skeleton-own-toolbar"></div>' : "", '<div class="sf-data-view-skeleton-records">', toolbar ? `<div class="sf-data-view-skeleton-toolbar">${control("button")}${control("field")}</div>` : "", `<div class="sf-data-view-skeleton-table">${row(true)}${Array.from({
|
|
1404
|
+
length: rows
|
|
1405
|
+
}, () => row(false)).join("")}</div>`, `<div class="sf-data-view-skeleton-list">${record.repeat(rows)}</div>`, "</div>", this.renders("pagination") ? pageRow : ""].join("");
|
|
1406
|
+
} // The host answered (a list, an empty one, a refusal or a failure): the
|
|
1407
|
+
// skeleton goes and the layout comes, in one frame.
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
_endFirstPaint() {
|
|
1411
|
+
if (this.getAttribute("data-sf-awaiting") === null) return;
|
|
1412
|
+
this.removeAttribute?.("data-sf-awaiting");
|
|
1413
|
+
|
|
1414
|
+
for (const node of this._firstPaintNodes()) node.remove?.();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
_firstPaintNodes() {
|
|
1418
|
+
return Array.from(this.children ?? []).filter(child => child.getAttribute?.("slot") === "first-paint");
|
|
1419
|
+
} // A component that takes a host port can also ask for one, so a consumer who
|
|
1420
|
+
// composes it inside something else does not have to find the element and call
|
|
1421
|
+
// a method on it. Raised once per connection and only while there is no port;
|
|
1422
|
+
// `setHostPort` is what answers it, synchronously or later. The name is the
|
|
1423
|
+
// shared convention rather than one of this element's own: the next composite
|
|
1424
|
+
// that takes a host port raises the same event, and a host answers them all
|
|
1425
|
+
// the same way.
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
_requestHostPort() {
|
|
1429
|
+
if (this.hostPort || this._portRequested) return false;
|
|
1430
|
+
this._portRequested = true;
|
|
1431
|
+
this.dispatchEvent(new CustomEvent("sf-host-port-request", {
|
|
1432
|
+
bubbles: true,
|
|
1433
|
+
composed: true,
|
|
1434
|
+
detail: {
|
|
1435
|
+
component: this,
|
|
1436
|
+
context: {
|
|
1437
|
+
owner: this.portOwner,
|
|
1438
|
+
record: this.portRecord,
|
|
1439
|
+
group: this.portGroup
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}));
|
|
1443
|
+
return true;
|
|
1444
|
+
} // What a host needs to know to answer: which composite is asking and about
|
|
1445
|
+
// what. Set by whatever embedded this element; absent on a page list, which is
|
|
1446
|
+
// asking about nothing but itself.
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
get portOwner() {
|
|
1450
|
+
return this.getAttribute("port-owner") || null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
get portRecord() {
|
|
1454
|
+
return this.getAttribute("port-record") || null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
get portGroup() {
|
|
1458
|
+
return this.getAttribute("port-group") || null;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
disconnectedCallback() {
|
|
1462
|
+
this.epoch += 1; // Once per connection, so an element moved in the DOM asks again. Without
|
|
1463
|
+
// this reset a list re-attached into a different host would sit portless
|
|
1464
|
+
// and silent, which looks exactly like a host that chose not to answer.
|
|
1465
|
+
|
|
1466
|
+
this._portRequested = false;
|
|
1467
|
+
this._controller?.abort();
|
|
1468
|
+
this._controller = null;
|
|
1469
|
+
const events = [...Object.keys(EVENTS), ...LISTENED];
|
|
1470
|
+
|
|
1471
|
+
for (const name of events) this.shadowRoot.removeEventListener(name, this._bound);
|
|
1472
|
+
|
|
1473
|
+
for (const name of events) this.removeEventListener(name, this._bound);
|
|
1474
|
+
|
|
1475
|
+
this.shadowRoot.querySelector("[data-search]").removeEventListener("change", this._bound);
|
|
1476
|
+
this.shadowRoot.querySelector("[data-view]").removeEventListener("change", this._bound);
|
|
1477
|
+
this.shadowRoot.querySelector("[data-save]").removeEventListener("click", this._bound);
|
|
1478
|
+
this.shadowRoot.querySelector("[data-delete]").removeEventListener("click", this._bound);
|
|
1479
|
+
this.pending = null;
|
|
1480
|
+
} // A host adapter exposes raise(intent, payload, {signal}). No transport is implied.
|
|
1481
|
+
// Which of this element's own parts render. A list and not a boolean, because
|
|
1482
|
+
// a boolean would have to mean «all of it or none», and an embedded list wants
|
|
1483
|
+
// its rows without a search box while a page list wants both. The names are
|
|
1484
|
+
// the ones this element already publishes as parts and slots, so nothing here
|
|
1485
|
+
// gives an existing thing a second name -- `sf-view-table` keeps its own `toolbar`
|
|
1486
|
+
// and `sf-pagination` keeps `top`/`bottom`.
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
get chrome() {
|
|
1490
|
+
const declared = this.getAttribute("chrome");
|
|
1491
|
+
if (declared === null) return [...CHROME_PARTS];
|
|
1492
|
+
return declared.split(/\s+/u).filter(part => CHROME_PARTS.includes(part));
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
renders(part) {
|
|
1496
|
+
return this.chrome.includes(part);
|
|
1497
|
+
} // How many rows an embedded list draws at most. Zero is no cap, which is what
|
|
1498
|
+
// a page list is. Not `limit`, which reads like a query limit the host owns,
|
|
1499
|
+
// and not `page-size`, which the pagination and the table already use for the
|
|
1500
|
+
// host's paging -- a cap is what this element shows of a page it already has.
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
get rowCap() {
|
|
1504
|
+
const declared = Number.parseInt(this.getAttribute("row-cap") ?? "", 10);
|
|
1505
|
+
return Number.isFinite(declared) && declared > 0 ? declared : 0;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
get cappedRecords() {
|
|
1509
|
+
// A tree is drawn whole, as far as it is open: the cap is a list's.
|
|
1510
|
+
if (this.treeMode === "tree") return (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree);
|
|
1511
|
+
const cap = this.rowCap;
|
|
1512
|
+
return cap ? this.records.slice(0, cap) : this.records;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
get capHidesRows() {
|
|
1516
|
+
const cap = this.rowCap;
|
|
1517
|
+
return Boolean(cap) && (this.records.length > cap || this.hasNext === true);
|
|
1518
|
+
} // The whole list is the host's to open. The component has no address to build
|
|
1519
|
+
// one from, and a route it guessed would send a person to a page the product
|
|
1520
|
+
// may not have. The query travels so the full view opens on the records the
|
|
1521
|
+
// person was looking at rather than on an unfiltered collection.
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
openList(reason = "row-cap") {
|
|
1525
|
+
if (!this.hostPort) return Promise.resolve({
|
|
1526
|
+
answer: "unavailable",
|
|
1527
|
+
reason: "port-not-connected"
|
|
1528
|
+
});
|
|
1529
|
+
return this._raise("list.open", {
|
|
1530
|
+
reason,
|
|
1531
|
+
query: {
|
|
1532
|
+
search: this.query.search ?? "",
|
|
1533
|
+
filters: { ...(this.query.filters ?? {})
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
} // The presentation attributes are observed, because they are read when the
|
|
1538
|
+
// projection runs and nothing re-ran it. A consumer rendering this element
|
|
1539
|
+
// through a template sets its attributes after it is in the document, so the
|
|
1540
|
+
// first projection saw no `chrome` at all and drew the full page chrome -- a
|
|
1541
|
+
// consumer reported the fallback «Поиск / Сохранить вид» form standing inside a
|
|
1542
|
+
// group that had asked for no chrome, until data arrived and projected again.
|
|
1543
|
+
// The step of the size ladder the view's controls take, handed to the table,
|
|
1544
|
+
// the page row and the selection band: 1, or the admin's dense 1/2, where the
|
|
1545
|
+
// view stands beside the admin menu's 32px rows (owner, 2026-10-13).
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
get size() {
|
|
1549
|
+
return this.getAttribute("size") === "1/2" ? "1/2" : "1";
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
static get observedAttributes() {
|
|
1553
|
+
return ["chrome", "row-cap", "aria-label", "size", "preferences-key", ...LABEL_ATTRIBUTES];
|
|
1554
|
+
} // The words of the table and the pagination the view draws itself, written
|
|
1555
|
+
// on the view and passed on: a host that let the view draw its own table
|
|
1556
|
+
// had no way to word it, so a list inside the CMS settings spoke the
|
|
1557
|
+
// fallback words (Larena, 2026-10-13). A word the view is not given is left
|
|
1558
|
+
// to the part's own default; one that is removed is removed there too.
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
_forwardLabels() {
|
|
1562
|
+
this._forwarded ??= new Set();
|
|
1563
|
+
|
|
1564
|
+
const pass = (target, from, to = from) => {
|
|
1565
|
+
if (!target?.setAttribute) return;
|
|
1566
|
+
const value = this.getAttribute(from);
|
|
1567
|
+
const key = `${target.localName}:${to}`;
|
|
1568
|
+
|
|
1569
|
+
if (value === null) {
|
|
1570
|
+
if (this._forwarded.delete(key)) target.removeAttribute(to);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
target.setAttribute(to, value);
|
|
1575
|
+
|
|
1576
|
+
this._forwarded.add(key);
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
for (const name of TABLE_LABELS) pass(this.table, name);
|
|
1580
|
+
|
|
1581
|
+
for (const name of PAGINATION_LABELS) pass(this.pagination, name);
|
|
1582
|
+
|
|
1583
|
+
pass(this.pagination, "pagination-aria-label", "aria-label");
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
attributeChangedCallback(name, previous, next) {
|
|
1587
|
+
if (previous === next || !this.isConnected) return;
|
|
1588
|
+
if (name === "preferences-key") this._applyPreferences();
|
|
1589
|
+
|
|
1590
|
+
this._renderProjection();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
setHostPort(host) {
|
|
1594
|
+
if (!host || typeof host.raise !== "function" || !SUPPORTED_PORT_VERSIONS.includes(host.version)) {
|
|
1595
|
+
throw new TypeError(`sf-view requires a declared dataview host port: ${SUPPORTED_PORT_VERSIONS.join(", ")}`);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
this.hostPort = host;
|
|
1599
|
+
|
|
1600
|
+
this._applyForAll();
|
|
1601
|
+
|
|
1602
|
+
if (this.isConnected) this.refresh("host-connected");
|
|
1603
|
+
return this;
|
|
1604
|
+
} // «Для всех (N)»: the page row offers the filter-wide action when the host
|
|
1605
|
+
// can act on all (bulk-under-filter), on the view's own page row and on a
|
|
1606
|
+
// page's that does not say otherwise (stand 24 P2: the box never showed,
|
|
1607
|
+
// since nothing set show-action-for-all). A page that writes the
|
|
1608
|
+
// attribute decides; what the view wrote it takes back when the host
|
|
1609
|
+
// stops offering it.
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
_applyForAll() {
|
|
1613
|
+
const pagination = this.pagination;
|
|
1614
|
+
if (!pagination?.setAttribute) return;
|
|
1615
|
+
const mine = pagination.getAttribute("data-sf-view-for-all") !== null;
|
|
1616
|
+
if (!mine && pagination.getAttribute("show-action-for-all") !== null) return;
|
|
1617
|
+
|
|
1618
|
+
if (this.hostOffers("bulk-under-filter")) {
|
|
1619
|
+
if (!mine) {
|
|
1620
|
+
pagination.setAttribute("show-action-for-all", "true");
|
|
1621
|
+
pagination.setAttribute("data-sf-view-for-all", "");
|
|
1622
|
+
}
|
|
1623
|
+
} else if (mine) {
|
|
1624
|
+
pagination.removeAttribute?.("show-action-for-all");
|
|
1625
|
+
pagination.removeAttribute?.("data-sf-view-for-all");
|
|
1626
|
+
}
|
|
1627
|
+
} // `message` is for the live region only: it is announced, never laid out.
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
_setState(state, message = "") {
|
|
1631
|
+
this._dataState = state;
|
|
1632
|
+
if (state !== "loading") this._endFirstPaint();
|
|
1633
|
+
this.setAttribute("data-state", state);
|
|
1634
|
+
this.setAttribute("aria-busy", state === "loading" ? "true" : "false");
|
|
1635
|
+
if (this.table && state === "empty") this.table.emptyContext = this._emptyContext(); // A load the person caused -- a search or a filter they set -- that
|
|
1636
|
+
// fails is an alert; any other failed load is said politely.
|
|
1637
|
+
|
|
1638
|
+
if (this.table && state === "error") {
|
|
1639
|
+
this.table.emptyContext = {
|
|
1640
|
+
caused: ["search", "filter"].includes(this._queryReason),
|
|
1641
|
+
...(this._queryMessage ? {
|
|
1642
|
+
message: this._queryMessage
|
|
1643
|
+
} : {})
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
this.table?.setDataState?.(state); // A failed or refused list has nothing to count: the page row goes with
|
|
1648
|
+
// it until a list comes back (docs trial 4: «Всего: 10» over an error).
|
|
1649
|
+
|
|
1650
|
+
const pageRow = this.shadowRoot.querySelector('slot[name="pagination"]');
|
|
1651
|
+
if (pageRow && ["error", "no-access"].includes(state)) pageRow.hidden = true;
|
|
1652
|
+
const status = this.shadowRoot.querySelector('[part="status"]');
|
|
1653
|
+
status.hidden = !message;
|
|
1654
|
+
status.textContent = message;
|
|
1655
|
+
this.dispatchEvent(new CustomEvent("sf-view-state", {
|
|
1656
|
+
bubbles: true,
|
|
1657
|
+
composed: true,
|
|
1658
|
+
detail: {
|
|
1659
|
+
state,
|
|
1660
|
+
message,
|
|
1661
|
+
instance_id: this.instanceId
|
|
1662
|
+
}
|
|
1663
|
+
}));
|
|
1664
|
+
} // Why the list is empty, from what the view already knows (owner,
|
|
1665
|
+
// 2026-10-14): the host refused the query -- no access; a search or a
|
|
1666
|
+
// filter is on -- nothing found, and what a reset would clear; otherwise
|
|
1667
|
+
// no data. The table draws it.
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
_emptyContext() {
|
|
1671
|
+
if (this._queryRefused) return this._queryMessage ? {
|
|
1672
|
+
kind: "no-access",
|
|
1673
|
+
message: this._queryMessage
|
|
1674
|
+
} : {
|
|
1675
|
+
kind: "no-access"
|
|
1676
|
+
};
|
|
1677
|
+
const search = String(this.query.search ?? "").trim() !== "";
|
|
1678
|
+
const filters = hasValue(this.query.filters);
|
|
1679
|
+
if (search || filters) return {
|
|
1680
|
+
kind: "nothing-found",
|
|
1681
|
+
reset: search && filters ? "both" : search ? "search" : "filters"
|
|
1682
|
+
};
|
|
1683
|
+
return {
|
|
1684
|
+
kind: "no-data"
|
|
1685
|
+
};
|
|
1686
|
+
} // Whether the host offers a capability: announced, or -- for the two that
|
|
1687
|
+
// were baseline before port 1.10.0 -- implied by an earlier version.
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
hostOffers(capability) {
|
|
1691
|
+
// The connected port, else the one the page declared (knownPort).
|
|
1692
|
+
const port = this.hostPort ?? this._declaredPort ?? null;
|
|
1693
|
+
if (!port) return false;
|
|
1694
|
+
if (port.capabilities?.includes?.(capability)) return true;
|
|
1695
|
+
return BASELINE_UNTIL_1_10.has(capability) && !portAtLeast(port.version, "1.10.0");
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async _raise(intent, payload, {
|
|
1699
|
+
query = false
|
|
1700
|
+
} = {}) {
|
|
1701
|
+
if (!this.hostPort) {
|
|
1702
|
+
if (query) this._queryMessage = "";
|
|
1703
|
+
|
|
1704
|
+
this._setState("error", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "statePortNotConnected", this));
|
|
1705
|
+
|
|
1706
|
+
this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticePortNotConnected", this));
|
|
1707
|
+
return {
|
|
1708
|
+
answer: "unavailable",
|
|
1709
|
+
reason: "port-not-connected"
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const capability = {
|
|
1714
|
+
"view.open": "saved-views",
|
|
1715
|
+
"view.save": "saved-views",
|
|
1716
|
+
"view.delete": "saved-views",
|
|
1717
|
+
"view.save_shared": "shared-saved-views",
|
|
1718
|
+
"settings.save_shared": "shared-column-settings",
|
|
1719
|
+
"bulk.apply_under_filter": "bulk-under-filter",
|
|
1720
|
+
"record.open": "row-actions",
|
|
1721
|
+
"record.mutate": "row-actions",
|
|
1722
|
+
"tree.expand": "tree",
|
|
1723
|
+
"node.targets": "tree-move",
|
|
1724
|
+
"node.move": "tree-move",
|
|
1725
|
+
"view.create_record": "create-record",
|
|
1726
|
+
"settings.save_personal": "personal-column-settings",
|
|
1727
|
+
"record.reorder": "row-reorder",
|
|
1728
|
+
"record.set": "inline-edit"
|
|
1729
|
+
}[intent];
|
|
1730
|
+
|
|
1731
|
+
if (capability && !this.hostOffers(capability)) {
|
|
1732
|
+
const answer = {
|
|
1733
|
+
answer: "unavailable",
|
|
1734
|
+
reason: "capability-not-provided"
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
this._showAnswer(intent, answer);
|
|
1738
|
+
|
|
1739
|
+
return answer;
|
|
1740
|
+
} // The tree's intents are announced as the one event the port names
|
|
1741
|
+
// for them, so a page can watch them without standing in the way.
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
if (TREE_INTENTS.has(intent)) {
|
|
1745
|
+
// sf-data-view-tree; the old sf-table-tree-intent follows once (aliases.json).
|
|
1746
|
+
this.sfEmit("tree", {
|
|
1747
|
+
intent,
|
|
1748
|
+
payload: copy(payload)
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const epoch = this.epoch;
|
|
1753
|
+
const sequence = query ? payload.sequence : null;
|
|
1754
|
+
|
|
1755
|
+
try {
|
|
1756
|
+
const answer = await this.hostPort.raise(intent, copy(payload), {
|
|
1757
|
+
signal: this._controller?.signal
|
|
1758
|
+
});
|
|
1759
|
+
if (!this.isConnected || epoch !== this.epoch || query && sequence !== this.sequence) return null;
|
|
1760
|
+
|
|
1761
|
+
if (!answer || !["applied", "conflict", "refused", "unavailable"].includes(answer.answer)) {
|
|
1762
|
+
throw new TypeError("Host answer is not a declared port answer");
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (query && answer.sequence != null && answer.sequence !== sequence) return null;
|
|
1766
|
+
if (query) this.pending = null; // Refused for access: «no access», without the toolbar. Refused for
|
|
1767
|
+
// the query: the query stays on screen to be corrected (isAccessRefusal).
|
|
1768
|
+
|
|
1769
|
+
if (query) this._queryRefused = answer.answer === "refused" && isAccessRefusal(answer.reason); // The host's own sentence for a list it cannot give or will not
|
|
1770
|
+
// give (Larena, 2026-10-21: «Список готовится…» while a large
|
|
1771
|
+
// storage's read model is built). It is shown as the state's own
|
|
1772
|
+
// words, as text: it is the host's, and never markup.
|
|
1773
|
+
|
|
1774
|
+
if (query) this._queryMessage = ["refused", "unavailable"].includes(answer.answer) ? hostMessage(answer) : ""; // A write the host confirms first (port 1.8.0): it answered with what
|
|
1775
|
+
// it is about to do and a digest of it. The person reads it; if they
|
|
1776
|
+
// confirm, the same intent goes again with the digest, which the
|
|
1777
|
+
// host checks against the write it described.
|
|
1778
|
+
|
|
1779
|
+
if (!query && answer.answer === "refused" && answer.reason === "confirm_required" && answer.data?.confirm?.digest && !payload?.confirm) {
|
|
1780
|
+
await this._ensureElements(["sf-button"]);
|
|
1781
|
+
const confirmed = await this._dialog({
|
|
1782
|
+
title: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "confirmTitle", this),
|
|
1783
|
+
text: String(answer.data.confirm.summary ?? ""),
|
|
1784
|
+
apply: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "confirmApply", this)
|
|
1785
|
+
});
|
|
1786
|
+
if (!confirmed) return {
|
|
1787
|
+
answer: "refused",
|
|
1788
|
+
reason: "not-confirmed"
|
|
1789
|
+
};
|
|
1790
|
+
return this._raise(intent, { ...payload,
|
|
1791
|
+
confirm: String(answer.data.confirm.digest)
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (answer.answer === "applied") {
|
|
1796
|
+
// Where a node may go is an answer to a question, not a view of
|
|
1797
|
+
// the list: it is read by the move dialog and changes no rows.
|
|
1798
|
+
if (ANSWER_ONLY.has(intent)) {// nothing to project
|
|
1799
|
+
} else if (!query && this._isRecordPatch(answer.data)) this._applyRecordPatch(answer.data.records);else if (answer.data) this.applyProjection(answer.data);
|
|
1800
|
+
|
|
1801
|
+
if (query && !answer.data) throw new TypeError("Applied query has no data");
|
|
1802
|
+
if (intent === "view.open") this.refresh("view-switch"); // A write that changed no record cannot have changed the rows.
|
|
1803
|
+
// Column settings — order, width, visibility, pinning — used to
|
|
1804
|
+
// fall into this branch and re-query the whole page, so moving a
|
|
1805
|
+
// column reloaded the view and flashed a loader at the person.
|
|
1806
|
+
|
|
1807
|
+
if (!query && !answer.data && !NO_REFRESH_AFTER.has(intent)) this.refresh("after-write"); // What the write changed for the rest of the page goes out on
|
|
1808
|
+
// the page host's outputs (simai.interface-fragment, effects);
|
|
1809
|
+
// the page's routes take it to whoever listens.
|
|
1810
|
+
|
|
1811
|
+
if (Array.isArray(answer.effects) && answer.effects.length) globalThis.SF?.Composition?.publishEffects?.(answer.effects, {
|
|
1812
|
+
origin: this
|
|
1813
|
+
});
|
|
1814
|
+
} else if (query && answer.answer === "refused" && !this._queryRefused) {
|
|
1815
|
+
// A query the host would not answer as asked -- a search
|
|
1816
|
+
// longer than it takes, a filter it does not allow. The rows of
|
|
1817
|
+
// the last answer do not answer this query, so they go; the
|
|
1818
|
+
// toolbar, the search the person typed and the filters stay,
|
|
1819
|
+
// and the state says what went wrong: the host's sentence as
|
|
1820
|
+
// text, or the view's own words. No notice over it: the state
|
|
1821
|
+
// and the live region already say it.
|
|
1822
|
+
this.records = [];
|
|
1823
|
+
this.appended = null;
|
|
1824
|
+
this.total = 0;
|
|
1825
|
+
this.countKnown = false;
|
|
1826
|
+
|
|
1827
|
+
this._renderProjection(); // The brick's title as well as the live region: the table's own
|
|
1828
|
+
// «could not load» would say the wrong thing about a query.
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
if (!this._queryMessage) this._queryMessage = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateQueryFailed", this);
|
|
1832
|
+
|
|
1833
|
+
this._setState("error", this._queryMessage);
|
|
1834
|
+
|
|
1835
|
+
this._showAnswer(intent, answer, {
|
|
1836
|
+
stated: true
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
return answer;
|
|
1840
|
+
} else if (query && answer.answer === "refused") {
|
|
1841
|
+
// A refused list shows nothing it had: the rows of the last
|
|
1842
|
+
// allowed answer stayed under «no access» (docs trial 4).
|
|
1843
|
+
this.records = [];
|
|
1844
|
+
this.appended = null;
|
|
1845
|
+
this.total = 0;
|
|
1846
|
+
this.countKnown = false;
|
|
1847
|
+
|
|
1848
|
+
this._renderProjection();
|
|
1849
|
+
} else if (query && answer.answer === "unavailable") {
|
|
1850
|
+
// A host that says why the list is not there is believed, and
|
|
1851
|
+
// its sentence is the state: the error brick and the live
|
|
1852
|
+
// region say it, and no error notice says something else over
|
|
1853
|
+
// it. Without one, the general words and the notice as before.
|
|
1854
|
+
this._setState("error", this._queryMessage || (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateUnavailable", this));
|
|
1855
|
+
|
|
1856
|
+
if (!this._queryMessage) this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeFetchFailed", this), {
|
|
1857
|
+
code: answer.reason || ""
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
this._showAnswer(intent, answer);
|
|
1862
|
+
|
|
1863
|
+
return answer;
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
if (!this.isConnected || epoch !== this.epoch) return null;
|
|
1866
|
+
const answer = {
|
|
1867
|
+
answer: "unavailable",
|
|
1868
|
+
reason: error?.message || "port-error"
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
if (query) {
|
|
1872
|
+
this._queryMessage = "";
|
|
1873
|
+
|
|
1874
|
+
this._setState("error", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateLoadFailed", this));
|
|
1875
|
+
|
|
1876
|
+
this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeLoadFailed", this), {
|
|
1877
|
+
code: answer.reason || ""
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
this._showAnswer(intent, answer);
|
|
1882
|
+
|
|
1883
|
+
return answer;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
_showAnswer(intent, answer, {
|
|
1888
|
+
stated = false
|
|
1889
|
+
} = {}) {
|
|
1890
|
+
if (!stated && ["conflict", "refused"].includes(answer.answer) || answer.answer === "unavailable" && intent !== "query.change") {
|
|
1891
|
+
const text = this._noticeText(answer);
|
|
1892
|
+
|
|
1893
|
+
this._setState(this.records.length ? "populated" : "empty", text);
|
|
1894
|
+
|
|
1895
|
+
this.notify(text, {
|
|
1896
|
+
kind: answer.answer === "conflict" ? "warning" : "error",
|
|
1897
|
+
code: answer.reason || ""
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
this.dispatchEvent(new CustomEvent("sf-view-answer", {
|
|
1902
|
+
bubbles: true,
|
|
1903
|
+
composed: true,
|
|
1904
|
+
detail: {
|
|
1905
|
+
intent,
|
|
1906
|
+
...copy(answer),
|
|
1907
|
+
instance_id: this.instanceId
|
|
1908
|
+
}
|
|
1909
|
+
}));
|
|
1910
|
+
} // The key the person's view of this list is kept under: the host names
|
|
1911
|
+
// it (preferences-key, e.g. the collection). The table takes the same.
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
get preferencesKey() {
|
|
1915
|
+
const key = this.getAttribute("preferences-key") || "";
|
|
1916
|
+
return key ? viewPreferenceKey(key) : "";
|
|
1917
|
+
} // Before the first query: the person's sort and page size, so the rows
|
|
1918
|
+
// come in their order the first time rather than after a click.
|
|
1919
|
+
// A list page scrolls as a page (owner, 2026-10-15): the view's table is in
|
|
1920
|
+
// page mode unless the host chose its mode itself.
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
_tableScrollMode(table) {
|
|
1924
|
+
if (typeof table?.hasAttribute !== "function" || typeof table.setAttribute !== "function") return;
|
|
1925
|
+
|
|
1926
|
+
if (!table.hasAttribute("data-sf-host-vertical-scroll")) {
|
|
1927
|
+
table.setAttribute("data-sf-host-vertical-scroll", table.getAttribute("vertical-scroll") ?? "");
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (!table.getAttribute("data-sf-host-vertical-scroll")) table.setAttribute("vertical-scroll", "page");
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
_applyPreferences() {
|
|
1934
|
+
this._tableScrollMode(this.table);
|
|
1935
|
+
|
|
1936
|
+
const key = this.preferencesKey;
|
|
1937
|
+
if (key === this._preferencesApplied) return;
|
|
1938
|
+
this._preferencesApplied = key;
|
|
1939
|
+
const table = this.table;
|
|
1940
|
+
if (table && key && !table.getAttribute("preferences-key")) table.setAttribute("preferences-key", this.getAttribute("preferences-key"));
|
|
1941
|
+
const kept = key ? preferences()?.read(key) : null; // What the person saved in this browser: it wins over anything the
|
|
1942
|
+
// page printed (a declared port's first query, a printed answer's
|
|
1943
|
+
// query), which the page wrote without knowing it (standard §4.9:
|
|
1944
|
+
// the page must not guess).
|
|
1945
|
+
|
|
1946
|
+
this._keptState = { ...(Array.isArray(kept?.sort) && kept.sort.length ? {
|
|
1947
|
+
sort: copy(kept.sort)
|
|
1948
|
+
} : {}),
|
|
1949
|
+
...(Number.isInteger(kept?.pageSize) && kept.pageSize > 0 ? {
|
|
1950
|
+
per_page: kept.pageSize
|
|
1951
|
+
} : {})
|
|
1952
|
+
};
|
|
1953
|
+
if (!kept) return;
|
|
1954
|
+
if (Array.isArray(kept.sort) && !this.query.sort?.length) this.query.sort = copy(kept.sort);
|
|
1955
|
+
|
|
1956
|
+
if (Number.isInteger(kept.pageSize) && kept.pageSize > 0) {
|
|
1957
|
+
this.query.per_page = kept.pageSize;
|
|
1958
|
+
this._pageSizeDeclared = true;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
_keepPreference(patch) {
|
|
1963
|
+
const key = this.preferencesKey;
|
|
1964
|
+
if (!key) return;
|
|
1965
|
+
preferences()?.write(key, { ...(preferences()?.read(key) ?? {}),
|
|
1966
|
+
v: 1,
|
|
1967
|
+
...patch
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
refresh(reason = "refresh") {
|
|
1972
|
+
if (!this.isConnected) return Promise.resolve(null);
|
|
1973
|
+
|
|
1974
|
+
this._applyPreferences();
|
|
1975
|
+
|
|
1976
|
+
const sequence = ++this.sequence;
|
|
1977
|
+
this.pending = sequence;
|
|
1978
|
+
this._queryReason = reason; // Show more continues the list; every other reason replaces it. The
|
|
1979
|
+
// answer carries no reason of its own, so it is remembered here.
|
|
1980
|
+
|
|
1981
|
+
this.continuing = reason === "show-more"; // The port's first answer over a first answer the page printed is a
|
|
1982
|
+
// revalidation: the rows stay as they are, usable, until it comes.
|
|
1983
|
+
|
|
1984
|
+
const revalidating = this._revalidate === true && ["connected", "host-connected"].includes(reason);
|
|
1985
|
+
this._revalidate = false;
|
|
1986
|
+
this._revalidating = revalidating; // Continuing the list is answered inside the button that asked for it.
|
|
1987
|
+
// The table is not loading — the rows on screen stay exactly as they
|
|
1988
|
+
// are — so it neither dims them nor replaces them with a loader.
|
|
1989
|
+
|
|
1990
|
+
if (this.continuing) {
|
|
1991
|
+
this.pagination?.setState?.({
|
|
1992
|
+
showMoreBusy: true
|
|
1993
|
+
});
|
|
1994
|
+
} else if (!revalidating) {
|
|
1995
|
+
this._setState("loading", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateLoading", this));
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
return this._raise("query.change", {
|
|
1999
|
+
sequence,
|
|
2000
|
+
query: copy(this.query),
|
|
2001
|
+
reason
|
|
2002
|
+
}, {
|
|
2003
|
+
query: true
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
_syncSearchBoxes(value) {
|
|
2008
|
+
const text = String(value ?? "");
|
|
2009
|
+
const own = this.shadowRoot?.querySelector("input[data-search]");
|
|
2010
|
+
if (own && own.value !== text && this.shadowRoot.activeElement !== own) own.value = text;
|
|
2011
|
+
const table = this.table;
|
|
2012
|
+
|
|
2013
|
+
if (table?.updateSearchState && (table.state?.search?.value ?? "") !== text && !table.contains(document.activeElement)) {
|
|
2014
|
+
table.updateSearchState({
|
|
2015
|
+
value: text
|
|
2016
|
+
}, false);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
changeQuery(patch, reason = "query-change") {
|
|
2021
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Query patch must be an object");
|
|
2022
|
+
this.query = { ...this.query,
|
|
2023
|
+
...copy(patch)
|
|
2024
|
+
};
|
|
2025
|
+
if (!["page-change", "page-size-change", "show-more"].includes(reason)) this.query.page = 1;
|
|
2026
|
+
return this.refresh(reason);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
applyProjection(data) {
|
|
2030
|
+
if (!data || typeof data !== "object") throw new TypeError("Port data must be an object"); // The port's answer to the query a printed first answer already
|
|
2031
|
+
// answered (stand 25, Larena's storage page: the printed list was drawn
|
|
2032
|
+
// and then drawn again, CLS 0.13 -> 0.32). The same answer -- the same
|
|
2033
|
+
// revision, or the same rows, count and columns -- changes nothing on
|
|
2034
|
+
// the page; a different one is taken in place: the rows keyed by id,
|
|
2035
|
+
// only the changed ones redrawn, in one render, no skeleton between.
|
|
2036
|
+
|
|
2037
|
+
let inPlace = false;
|
|
2038
|
+
|
|
2039
|
+
if (this._revalidating) {
|
|
2040
|
+
this._revalidating = false;
|
|
2041
|
+
const printed = this._printedAnswer;
|
|
2042
|
+
this._printedAnswer = null;
|
|
2043
|
+
|
|
2044
|
+
if (printed && sameAnswer(printed, data)) {
|
|
2045
|
+
// Only what the port itself brings is drawn: its capabilities
|
|
2046
|
+
// (row actions, settings), when they differ from the ones the
|
|
2047
|
+
// printed answer was drawn without.
|
|
2048
|
+
if (this._capabilitiesKey !== capabilitiesKey(this.knownPort)) this._renderProjection();
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
inPlace = Boolean(printed);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
if (Array.isArray(data.row_actions)) this.rowActionCatalog = copy(data.row_actions); // One node's children: they go under it, and nothing else about the
|
|
2056
|
+
// view changes -- not the query, not the count, not the columns.
|
|
2057
|
+
|
|
2058
|
+
const tree = data.tree && typeof data.tree === "object" ? data.tree : null;
|
|
2059
|
+
|
|
2060
|
+
if (tree?.mode === "tree" && tree.parent !== null && tree.parent !== undefined && Array.isArray(data.records)) {
|
|
2061
|
+
const parent = String(tree.parent);
|
|
2062
|
+
(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.acceptLevel)(this.tree, {
|
|
2063
|
+
parent,
|
|
2064
|
+
records: copy(data.records),
|
|
2065
|
+
hasNext: tree.has_next === true,
|
|
2066
|
+
cursor: tree.cursor ?? null,
|
|
2067
|
+
append: this._treeAppend.delete(parent)
|
|
2068
|
+
});
|
|
2069
|
+
this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
|
|
2070
|
+
|
|
2071
|
+
this._renderProjection();
|
|
2072
|
+
|
|
2073
|
+
return;
|
|
2074
|
+
} // An answer without records is no new shape of the list (a settings
|
|
2075
|
+
// write, a saved view): the mode stays what the last list said.
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
if (tree) this.treeMode = tree.mode ?? null;else if (Array.isArray(data.records)) this.treeMode = null;
|
|
2079
|
+
|
|
2080
|
+
if (this.treeMode === "tree" && Array.isArray(data.records)) {
|
|
2081
|
+
const reopen = [...this.tree.expanded];
|
|
2082
|
+
(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.acceptLevel)(this.tree, {
|
|
2083
|
+
records: copy(data.records),
|
|
2084
|
+
hasNext: tree.has_next === true,
|
|
2085
|
+
cursor: tree.cursor ?? null,
|
|
2086
|
+
append: this.continuing && this.records.length > 0
|
|
2087
|
+
});
|
|
2088
|
+
this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
|
|
2089
|
+
this.appended = null; // Branches a person had open stay open: their children are asked
|
|
2090
|
+
// for again, since a re-read may have changed them.
|
|
2091
|
+
|
|
2092
|
+
for (const id of reopen) {
|
|
2093
|
+
if (this.tree.nodes.get(id)?.has_children === true && !(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.hasLevel)(this.tree, id)) this.expandNode(id);
|
|
2094
|
+
}
|
|
2095
|
+
} else if (Array.isArray(data.records)) {
|
|
2096
|
+
const next = copy(data.records); // A continuation keeps what is already shown and adds to it, and
|
|
2097
|
+
// keeps it by identity: a record the port repeats is not duplicated.
|
|
2098
|
+
|
|
2099
|
+
if (this.continuing && this.records.length) {
|
|
2100
|
+
const known = new Set(this.records.map(record => record.id));
|
|
2101
|
+
this.appended = next.filter(record => !known.has(record.id));
|
|
2102
|
+
this.records = [...this.records, ...this.appended];
|
|
2103
|
+
} else {
|
|
2104
|
+
this.appended = null;
|
|
2105
|
+
this.records = next; // A new result set: the table draws it as new rows where a
|
|
2106
|
+
// kept one would move (sf-view-table setRows "result"). A
|
|
2107
|
+
// revalidation of the printed answer is the same result.
|
|
2108
|
+
|
|
2109
|
+
this._newResult = !inPlace;
|
|
2110
|
+
}
|
|
2111
|
+
} // The same columns are not given again: the table would take them as
|
|
2112
|
+
// new and measure its widths again.
|
|
2113
|
+
|
|
2114
|
+
|
|
2115
|
+
if (Array.isArray(data.columns) && !(inPlace && canonical(data.columns) === this._columnsKey)) {
|
|
2116
|
+
this.columns = data.columns.map(projectColumn);
|
|
2117
|
+
this._columnsKey = canonical(data.columns);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (Number.isSafeInteger(data.total) && data.total >= 0) {
|
|
2121
|
+
this.total = data.total;
|
|
2122
|
+
this.countKnown = true;
|
|
2123
|
+
} else if (typeof data.has_next === "boolean") {
|
|
2124
|
+
this.hasNext = data.has_next; // A number already answered for this very query stays on screen.
|
|
2125
|
+
// Paging and Show more do not take it away — only a query that
|
|
2126
|
+
// changes what is being counted does.
|
|
2127
|
+
|
|
2128
|
+
this.countKnown = this.countedQuery !== null && this.countedQuery === this._countKey();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (Number.isSafeInteger(data.total)) this.totalExact = data.total_exact !== false;
|
|
2132
|
+
this._answered = true;
|
|
2133
|
+
if (Array.isArray(data.saved_views)) this.savedViews = copy(data.saved_views);
|
|
2134
|
+
if (data.column_settings) this.columnSettings = copy(data.column_settings);
|
|
2135
|
+
|
|
2136
|
+
if (data.echo) {
|
|
2137
|
+
const echo = data.echo;
|
|
2138
|
+
this.query = { ...this.query,
|
|
2139
|
+
filters: copy(echo.filters_chosen ?? echo.filters ?? {}),
|
|
2140
|
+
search: echo.search ?? "",
|
|
2141
|
+
sort: copy(echo.sort ?? []),
|
|
2142
|
+
page: echo.page ?? 1,
|
|
2143
|
+
per_page: echo.per_page ?? 25
|
|
2144
|
+
};
|
|
2145
|
+
} // The search box says what the list is searched for, whoever set it: a
|
|
2146
|
+
// changeQuery({search}) from page code filtered the rows and left the
|
|
2147
|
+
// box empty (docs trial 5). Written silently -- it is not a new search.
|
|
2148
|
+
|
|
2149
|
+
|
|
2150
|
+
this._syncSearchBoxes(this.query.search ?? "");
|
|
2151
|
+
|
|
2152
|
+
this._renderProjection();
|
|
2153
|
+
|
|
2154
|
+
this._restoreOpenedView(); // The empty result is shown by the table, in its own body. The view only
|
|
2155
|
+
// announces it, so a search that finds nothing moves nothing.
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
this._setState(this.records.length ? "populated" : "empty", this.records.length ? "" : (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "emptyText", this));
|
|
2159
|
+
|
|
2160
|
+
this.notify("");
|
|
2161
|
+
} // What a count belongs to. Search, filters and page size change what the
|
|
2162
|
+
// number would be; a page does not.
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
_countKey() {
|
|
2166
|
+
return JSON.stringify([this.query.search ?? "", this.query.filters ?? {}, this.query.per_page ?? 25]);
|
|
2167
|
+
} // What the band shows and what it does. The words and the action list stay
|
|
2168
|
+
// the host's, set on the pagination as they always were; the band only reads
|
|
2169
|
+
// them, so a host configures one element and sees the band in the card.
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
_renderBulkBand() {
|
|
2173
|
+
const pagination = this.pagination;
|
|
2174
|
+
const table = this.table;
|
|
2175
|
+
const selected = table?.getSelectedRecordIds?.() ?? [];
|
|
2176
|
+
const actions = pagination?.actions ?? [];
|
|
2177
|
+
|
|
2178
|
+
if (!pagination || !selected.length || !actions.length) {
|
|
2179
|
+
if (this._bulkBand?.isConnected) this._bulkBand.remove();
|
|
2180
|
+
this._bulkBand = null;
|
|
2181
|
+
return this;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
const band = this.bulkBand;
|
|
2185
|
+
band.setAttribute("aria-label", pagination.actionsRegionLabel ?? "");
|
|
2186
|
+
const action = pagination.action ?? "";
|
|
2187
|
+
const total = pagination.countKnown === false ? null : pagination.total;
|
|
2188
|
+
const forAllLabel = total == null ? pagination.actionForAllLabel : `${pagination.actionForAllLabel} (${this._shownTotal(total)})`;
|
|
2189
|
+
|
|
2190
|
+
const chosen = (node, name, value) => {
|
|
2191
|
+
if (value == null || value === false) node.removeAttribute(name);else node.setAttribute(name, value === true ? "" : String(value));
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
const parts = {};
|
|
2195
|
+
|
|
2196
|
+
if (!band.childElementCount) {
|
|
2197
|
+
band.innerHTML = `
|
|
2198
|
+
<span class="sf-pagination-actions-count" data-sf-bulk="count"></span>
|
|
2199
|
+
<span class="sf-pagination-actions-separator" aria-hidden="true"></span>
|
|
2200
|
+
<sf-dropdown data-sf-bulk="actions" size="1" appearance="outlined" search="false" mode="select"></sf-dropdown>
|
|
2201
|
+
<sf-button data-sf-bulk="apply" size="1" appearance="default" scheme="primary"></sf-button>
|
|
2202
|
+
<span class="sf-pagination-actions-separator" aria-hidden="true" data-sf-bulk="for-all-separator"></span>
|
|
2203
|
+
<sf-checkbox data-sf-bulk="for-all"></sf-checkbox>
|
|
2204
|
+
<sf-button-icon data-sf-bulk="clear" class="sf-pagination-actions-clear" size="1" appearance="link" scheme="on-surface" icon="close"></sf-button-icon>`;
|
|
2205
|
+
band.querySelector('[data-sf-bulk="apply"]').addEventListener("click", () => this.applyBulkAction());
|
|
2206
|
+
band.querySelector('[data-sf-bulk="actions"]').addEventListener("change", event => pagination.setAction?.(event.detail?.value ?? ""));
|
|
2207
|
+
band.querySelector('[data-sf-bulk="for-all"]').addEventListener("change", event => pagination.setActionForAll?.(Boolean(event.target?.checked)));
|
|
2208
|
+
band.querySelector('[data-sf-bulk="clear"]').addEventListener("click", () => this.clearSelection());
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
for (const node of band.querySelectorAll("[data-sf-bulk]")) {
|
|
2212
|
+
parts[node.dataset.sfBulk] = node;
|
|
2213
|
+
if (node.localName.startsWith("sf-")) chosen(node, "size", this.size);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
parts.count.textContent = `${pagination.selectedLabel} ${selected.length}`;
|
|
2217
|
+
chosen(parts.actions, "value", action);
|
|
2218
|
+
chosen(parts.actions, "placeholder", pagination.actionChooseLabel);
|
|
2219
|
+
chosen(parts.actions, "aria-label", pagination.actionsLabel);
|
|
2220
|
+
parts.actions.options = [{
|
|
2221
|
+
type: "text",
|
|
2222
|
+
size: this.size,
|
|
2223
|
+
text: pagination.actionChooseLabel,
|
|
2224
|
+
value: "",
|
|
2225
|
+
selected: !action
|
|
2226
|
+
}, ...actions.map(item => ({
|
|
2227
|
+
type: "text",
|
|
2228
|
+
size: this.size,
|
|
2229
|
+
text: item.text,
|
|
2230
|
+
value: item.value,
|
|
2231
|
+
selected: item.value === action,
|
|
2232
|
+
disabled: Boolean(item.disabled)
|
|
2233
|
+
}))];
|
|
2234
|
+
chosen(parts.apply, "text", pagination.actionApplyText);
|
|
2235
|
+
chosen(parts.apply, "disabled", !action);
|
|
2236
|
+
chosen(parts["for-all"], "label", forAllLabel);
|
|
2237
|
+
chosen(parts["for-all"], "checked", Boolean(pagination.actionForAll));
|
|
2238
|
+
parts["for-all"].hidden = !pagination.showActionForAll; // Its separator goes with it, or the band ends on a rule before the
|
|
2239
|
+
// close button.
|
|
2240
|
+
|
|
2241
|
+
parts["for-all-separator"].hidden = !pagination.showActionForAll;
|
|
2242
|
+
chosen(parts.clear, "title", pagination.clearSelectionLabel);
|
|
2243
|
+
chosen(parts.clear, "aria-label", pagination.clearSelectionLabel);
|
|
2244
|
+
return this;
|
|
2245
|
+
} // Apply is the band's only write, and it goes the same way it went from the
|
|
2246
|
+
// page row: under the filter when For-all is ticked, over the marked rows
|
|
2247
|
+
// otherwise.
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
applyBulkAction() {
|
|
2251
|
+
const pagination = this.pagination;
|
|
2252
|
+
const action = pagination?.action;
|
|
2253
|
+
if (!action) return undefined;
|
|
2254
|
+
if (pagination.actionForAll) return this.applyUnderFilter(action, this.total);
|
|
2255
|
+
return this._action({
|
|
2256
|
+
action_id: action,
|
|
2257
|
+
record_ids: this.table?.getSelectedRecordIds?.() ?? []
|
|
2258
|
+
}, {
|
|
2259
|
+
selectedBulk: true
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
clearSelection() {
|
|
2264
|
+
this.table?.clearSelection?.();
|
|
2265
|
+
this.pagination?.setState?.({
|
|
2266
|
+
selectedCount: 0
|
|
2267
|
+
});
|
|
2268
|
+
return this._renderBulkBand();
|
|
2269
|
+
} // One record, changed in place. A write that touches a row does not need
|
|
2270
|
+
// the page re-queried and the body rebuilt: the host answers with the
|
|
2271
|
+
// record it changed, or the page calls this itself, and only that row is
|
|
2272
|
+
// written. A full projection is still a full projection.
|
|
2273
|
+
|
|
2274
|
+
|
|
2275
|
+
updateRecord(id, patch = {}) {
|
|
2276
|
+
const key = String(id);
|
|
2277
|
+
|
|
2278
|
+
if (this.treeMode === "tree") {
|
|
2279
|
+
const known = this.tree.nodes.get(key);
|
|
2280
|
+
if (!known || !patch || typeof patch !== "object") return false;
|
|
2281
|
+
this.tree.nodes.set(key, { ...known,
|
|
2282
|
+
...copy(patch),
|
|
2283
|
+
id: known.id
|
|
2284
|
+
});
|
|
2285
|
+
this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
|
|
2286
|
+
this.table?.setRows?.((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree));
|
|
2287
|
+
|
|
2288
|
+
this._renderBulkBand();
|
|
2289
|
+
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const index = this.records.findIndex(record => String(record?.id) === key);
|
|
2294
|
+
if (index < 0 || !patch || typeof patch !== "object") return false;
|
|
2295
|
+
const next = { ...this.records[index],
|
|
2296
|
+
...copy(patch),
|
|
2297
|
+
id: this.records[index].id
|
|
2298
|
+
};
|
|
2299
|
+
this.records = [...this.records.slice(0, index), next, ...this.records.slice(index + 1)];
|
|
2300
|
+
this.table?.updateRowByKey?.("id", this.records[index].id, { ...next
|
|
2301
|
+
}, "record-update");
|
|
2302
|
+
|
|
2303
|
+
this._renderBulkBand();
|
|
2304
|
+
|
|
2305
|
+
return true;
|
|
2306
|
+
} // ─── the tree (port 1.8.0) ───────────────────────────────────────────────
|
|
2307
|
+
// Opening a node shows the children the view already has, or asks the host
|
|
2308
|
+
// for them; closing it is the view's own business. A node is opened only
|
|
2309
|
+
// when the host said it has children.
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
expandNode(id) {
|
|
2313
|
+
const key = String(id);
|
|
2314
|
+
const node = this.tree.nodes.get(key);
|
|
2315
|
+
if (!node || node.has_children !== true) return null;
|
|
2316
|
+
|
|
2317
|
+
if ((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.hasLevel)(this.tree, key)) {
|
|
2318
|
+
(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.expandKnown)(this.tree, key);
|
|
2319
|
+
|
|
2320
|
+
this._renderTreeRows();
|
|
2321
|
+
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.markLoading)(this.tree, key);
|
|
2326
|
+
|
|
2327
|
+
this._renderTreeRows();
|
|
2328
|
+
|
|
2329
|
+
return this._raise("tree.expand", {
|
|
2330
|
+
id: node.id,
|
|
2331
|
+
cursor: null,
|
|
2332
|
+
sequence: ++this.treeSequence
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
collapseNode(id) {
|
|
2337
|
+
(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.collapse)(this.tree, id);
|
|
2338
|
+
|
|
2339
|
+
this._renderTreeRows();
|
|
2340
|
+
} // The next page of one level: of the roots through the query, as a list
|
|
2341
|
+
// pages, and of a node's children through tree.expand with its cursor.
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
loadMoreNodes(parent, cursor) {
|
|
2345
|
+
if (parent === null || parent === undefined || parent === "") {
|
|
2346
|
+
return this.changeQuery({
|
|
2347
|
+
page: this.query.page + 1
|
|
2348
|
+
}, "show-more");
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
const key = String(parent);
|
|
2352
|
+
|
|
2353
|
+
this._treeAppend.add(key);
|
|
2354
|
+
|
|
2355
|
+
const node = this.tree.nodes.get(key);
|
|
2356
|
+
return this._raise("tree.expand", {
|
|
2357
|
+
id: node?.id ?? parent,
|
|
2358
|
+
cursor: cursor ?? null,
|
|
2359
|
+
sequence: ++this.treeSequence
|
|
2360
|
+
});
|
|
2361
|
+
} // Where a node may go is the host's: it is asked first, and only the places
|
|
2362
|
+
// it names are offered -- the top level when it allows that, and the first
|
|
2363
|
+
// or last place among the new siblings when it allows reordering.
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
async openMoveDialog(record) {
|
|
2367
|
+
const answer = await this._raise("node.targets", {
|
|
2368
|
+
id: record.id
|
|
2369
|
+
});
|
|
2370
|
+
if (answer?.answer !== "applied") return answer;
|
|
2371
|
+
const targets = answer.data?.move_targets ?? {};
|
|
2372
|
+
const parents = Array.isArray(targets.parents) ? targets.parents : [];
|
|
2373
|
+
const options = [...(targets.root ? [{
|
|
2374
|
+
id: null,
|
|
2375
|
+
label: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveRoot", this),
|
|
2376
|
+
path: []
|
|
2377
|
+
}] : []), ...parents.filter(parent => String(parent.id) !== String(record.id))];
|
|
2378
|
+
|
|
2379
|
+
if (!options.length) {
|
|
2380
|
+
this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveNoTargets", this), {
|
|
2381
|
+
kind: "warning"
|
|
2382
|
+
});
|
|
2383
|
+
return {
|
|
2384
|
+
answer: "refused",
|
|
2385
|
+
reason: "no-targets"
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const label = String(record.title ?? record.label ?? record.name ?? record.id); // The choices are sf-radio, which nothing else on the page may have
|
|
2390
|
+
// asked for: a Loader that scans the light DOM never meets the
|
|
2391
|
+
// dialog's (Larena: an empty choice list, 2026-10-12). _dialog asks
|
|
2392
|
+
// for it with the window, and waits briefly.
|
|
2393
|
+
|
|
2394
|
+
const chosen = await this._dialog({
|
|
2395
|
+
title: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveTitle", this).replace("{label}", label),
|
|
2396
|
+
apply: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveApply", this),
|
|
2397
|
+
options,
|
|
2398
|
+
reorder: targets.reorder === true
|
|
2399
|
+
});
|
|
2400
|
+
if (!chosen) return null;
|
|
2401
|
+
return this._raise("node.move", {
|
|
2402
|
+
id: record.id,
|
|
2403
|
+
parent: chosen.parent,
|
|
2404
|
+
index: chosen.index,
|
|
2405
|
+
...(record.revision !== undefined ? {
|
|
2406
|
+
revision: record.revision
|
|
2407
|
+
} : {})
|
|
2408
|
+
});
|
|
2409
|
+
} // The elements the view composes, asked for by tag: the Loader defines each
|
|
2410
|
+
// from its own bundle (the rule's relations usually have already), and the
|
|
2411
|
+
// view projects again once they are, so an answer that came before them is
|
|
2412
|
+
// not lost on an element that could not take it yet.
|
|
2413
|
+
|
|
2414
|
+
|
|
2415
|
+
_askForParts(tags) {
|
|
2416
|
+
const registry = globalThis.customElements;
|
|
2417
|
+
if (typeof registry?.get !== "function" || typeof registry.whenDefined !== "function") return;
|
|
2418
|
+
const missing = tags.filter(tag => !registry.get(tag));
|
|
2419
|
+
if (!missing.length) return;
|
|
2420
|
+
const key = missing.join(",");
|
|
2421
|
+
this._partsAsked ??= new Set();
|
|
2422
|
+
if (this._partsAsked.has(key)) return;
|
|
2423
|
+
|
|
2424
|
+
this._partsAsked.add(key);
|
|
2425
|
+
|
|
2426
|
+
globalThis.SF?.Loader?.ensure?.(missing, {
|
|
2427
|
+
timeout: Infinity
|
|
2428
|
+
}).then(() => {
|
|
2429
|
+
this._partsAsked.delete(key);
|
|
2430
|
+
|
|
2431
|
+
if (this.isConnected && missing.some(tag => tag !== "sf-toast")) this._renderProjection();
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
async _ensureElements(tags) {
|
|
2436
|
+
return globalThis.SF?.Loader?.ensure?.(tags);
|
|
2437
|
+
} // One small window for both the move and the confirmation: the
|
|
2438
|
+
// Framework's sf-modal, a modal layer of the one document stack (core
|
|
2439
|
+
// layer-stack.js) with the shared scrim, Escape, a press on the scrim and
|
|
2440
|
+
// the focus kept inside and given back. It was a native <dialog> shown with
|
|
2441
|
+
// showModal(), outside the stack and over the browser's own backdrop, so a
|
|
2442
|
+
// drawer or a modal opened from it, or under it, did not know it was there
|
|
2443
|
+
// (reuse audit 2026-11-02, findings 1 and 2). The window stands in this
|
|
2444
|
+
// element (slot dialog) and draws in its portal at the end of the body, in
|
|
2445
|
+
// this element's language and theme. Resolves with what was chosen, true
|
|
2446
|
+
// for a plain confirmation, or null.
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
async _dialog({
|
|
2450
|
+
title,
|
|
2451
|
+
text = "",
|
|
2452
|
+
apply,
|
|
2453
|
+
options = null,
|
|
2454
|
+
reorder = false
|
|
2455
|
+
}) {
|
|
2456
|
+
await this._ensureElements(["sf-modal", "sf-button", ...(options ? ["sf-radio"] : [])]);
|
|
2457
|
+
|
|
2458
|
+
if (!globalThis.customElements?.get("sf-modal")) {
|
|
2459
|
+
this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeUnavailable", this));
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
const modal = document.createElement("sf-modal");
|
|
2464
|
+
modal.slot = "dialog";
|
|
2465
|
+
modal.className = "sf-data-view-dialog";
|
|
2466
|
+
modal.setAttribute("heading", title);
|
|
2467
|
+
modal.setAttribute("aria-label", title);
|
|
2468
|
+
modal.setAttribute("width", "min(var(--sf-g4), calc(100vw - 2 * var(--sf-space-3)))"); // The places can be many; the list scrolls and the title and the
|
|
2469
|
+
// buttons stay where they are.
|
|
2470
|
+
|
|
2471
|
+
const content = document.createElement("div");
|
|
2472
|
+
content.slot = "content";
|
|
2473
|
+
content.className = "flex flex-col gap-3";
|
|
2474
|
+
|
|
2475
|
+
if (text) {
|
|
2476
|
+
const body = document.createElement("p");
|
|
2477
|
+
body.className = "sf-text-1 m-0";
|
|
2478
|
+
body.textContent = text;
|
|
2479
|
+
content.append(body);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const name = `${this.instanceId}-move`;
|
|
2483
|
+
|
|
2484
|
+
if (options) {
|
|
2485
|
+
const list = document.createElement("div");
|
|
2486
|
+
list.setAttribute("role", "radiogroup");
|
|
2487
|
+
list.setAttribute("aria-label", title);
|
|
2488
|
+
list.className = "flex flex-col gap-1";
|
|
2489
|
+
list.style.cssText = "overflow-y: auto; min-block-size: 0; flex: 1 1 auto;";
|
|
2490
|
+
options.forEach((option, index) => {
|
|
2491
|
+
const radio = document.createElement("sf-radio");
|
|
2492
|
+
radio.setAttribute("name", name);
|
|
2493
|
+
radio.setAttribute("value", option.id === null ? "" : String(option.id));
|
|
2494
|
+
radio.setAttribute("label", String(option.label));
|
|
2495
|
+
if (Array.isArray(option.path) && option.path.length) radio.setAttribute("description", option.path.join(" / "));
|
|
2496
|
+
if (index === 0) radio.setAttribute("checked", "");
|
|
2497
|
+
list.append(radio);
|
|
2498
|
+
});
|
|
2499
|
+
content.append(list);
|
|
2500
|
+
|
|
2501
|
+
if (reorder) {
|
|
2502
|
+
const placeList = document.createElement("div");
|
|
2503
|
+
placeList.setAttribute("role", "radiogroup");
|
|
2504
|
+
placeList.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "movePlace", this));
|
|
2505
|
+
placeList.className = "flex gap-2";
|
|
2506
|
+
|
|
2507
|
+
for (const [value, key] of [["last", "moveLast"], ["first", "moveFirst"]]) {
|
|
2508
|
+
const radio = document.createElement("sf-radio");
|
|
2509
|
+
radio.setAttribute("name", `${name}-place`);
|
|
2510
|
+
radio.setAttribute("value", value);
|
|
2511
|
+
radio.setAttribute("label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, key, this));
|
|
2512
|
+
if (value === "last") radio.setAttribute("checked", "");
|
|
2513
|
+
placeList.append(radio);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
content.append(placeList);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
const footer = document.createElement("div");
|
|
2521
|
+
footer.slot = "footer";
|
|
2522
|
+
footer.className = "flex justify-end gap-2";
|
|
2523
|
+
const cancel = document.createElement("sf-button");
|
|
2524
|
+
cancel.setAttribute("appearance", "outline");
|
|
2525
|
+
cancel.setAttribute("text", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "cancel", this));
|
|
2526
|
+
const confirm = document.createElement("sf-button");
|
|
2527
|
+
confirm.setAttribute("text", apply);
|
|
2528
|
+
confirm.dataset.apply = "";
|
|
2529
|
+
footer.append(cancel, confirm);
|
|
2530
|
+
modal.append(content, footer);
|
|
2531
|
+
this.append(modal); // The nodes are the page's own, drawn in the window's portal: they are
|
|
2532
|
+
// read where they are.
|
|
2533
|
+
|
|
2534
|
+
const checked = radioName => [...content.querySelectorAll(`sf-radio[name="${radioName}"]`)].find(radio => radio.checked === true || radio.hasAttribute("checked") && radio.checked !== false);
|
|
2535
|
+
|
|
2536
|
+
let chosen = null;
|
|
2537
|
+
cancel.addEventListener("click", () => modal.close({
|
|
2538
|
+
reason: "cancelled"
|
|
2539
|
+
}));
|
|
2540
|
+
confirm.addEventListener("click", () => {
|
|
2541
|
+
if (!options) {
|
|
2542
|
+
chosen = true;
|
|
2543
|
+
} else {
|
|
2544
|
+
const parentRadio = checked(name);
|
|
2545
|
+
const value = parentRadio?.getAttribute("value") ?? "";
|
|
2546
|
+
const option = options.find(item => (item.id === null ? "" : String(item.id)) === value) ?? options[0];
|
|
2547
|
+
const place = reorder ? checked(`${name}-place`)?.getAttribute("value") : "last";
|
|
2548
|
+
chosen = {
|
|
2549
|
+
parent: option.id,
|
|
2550
|
+
index: place === "first" ? 0 : null
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
modal.confirm();
|
|
2555
|
+
}); // Asked with a signal, open() answers when the window closes (§19.4):
|
|
2556
|
+
// accepted through confirm(), else closed by Escape, the scrim, the
|
|
2557
|
+
// close button or «Отмена».
|
|
2558
|
+
|
|
2559
|
+
const scope = new AbortController();
|
|
2560
|
+
const answer = await modal.open({}, {
|
|
2561
|
+
signal: scope.signal
|
|
2562
|
+
});
|
|
2563
|
+
modal.remove();
|
|
2564
|
+
return answer?.outcome === "applied" && answer.value?.accepted === true ? chosen : null;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
_renderTreeRows() {
|
|
2568
|
+
this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
|
|
2569
|
+
this.table?.setRows?.((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree));
|
|
2570
|
+
} // An answer that carries records and nothing else is a patch of those
|
|
2571
|
+
// records, not a new page: it has no echo of the query, no total and no
|
|
2572
|
+
// columns, so there is nothing to lay out again.
|
|
2573
|
+
|
|
2574
|
+
|
|
2575
|
+
_isRecordPatch(data) {
|
|
2576
|
+
return Boolean(data) && Array.isArray(data.records) && !data.echo && data.total == null && !data.columns && !data.saved_views && !data.column_settings && !data.tree;
|
|
2577
|
+
} // A checkable cell (record.set, capability inline-edit): the host answers
|
|
2578
|
+
// with the record as it now is, asks to confirm first (a change that
|
|
2579
|
+
// rewrites records), or refuses; the row is then drawn from what it said.
|
|
2580
|
+
|
|
2581
|
+
|
|
2582
|
+
async setCell({
|
|
2583
|
+
id,
|
|
2584
|
+
key,
|
|
2585
|
+
value,
|
|
2586
|
+
revision
|
|
2587
|
+
} = {}) {
|
|
2588
|
+
if (id == null || !key) return null;
|
|
2589
|
+
const record = this.records.find(entry => String(entry.id) === String(id));
|
|
2590
|
+
const answer = await this._raise("record.set", {
|
|
2591
|
+
record_id: id,
|
|
2592
|
+
key,
|
|
2593
|
+
value,
|
|
2594
|
+
revision: revision ?? record?._revision ?? record?.revision ?? null
|
|
2595
|
+
});
|
|
2596
|
+
this.table?.clearCellPending?.(String(id), key); // Anything but applied leaves the record as it was, and the box shows
|
|
2597
|
+
// the value the host still holds.
|
|
2598
|
+
|
|
2599
|
+
if (answer?.answer !== "applied") {
|
|
2600
|
+
const held = this.records.find(entry => String(entry.id) === String(id));
|
|
2601
|
+
this.table?.resetCellControl?.(String(id), key, held?.[key]);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
return answer;
|
|
2605
|
+
} // Rows dragged into another order (record.reorder, capability
|
|
2606
|
+
// row-reorder). Shown in the new order at once; a refusal or a conflict
|
|
2607
|
+
// puts the old order back.
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
async reorderRows({
|
|
2611
|
+
ids = [],
|
|
2612
|
+
before = null,
|
|
2613
|
+
order = null
|
|
2614
|
+
} = {}) {
|
|
2615
|
+
if (!ids.length) return null;
|
|
2616
|
+
const previous = this.records;
|
|
2617
|
+
|
|
2618
|
+
if (Array.isArray(order)) {
|
|
2619
|
+
const byId = new Map(this.records.map(record => [String(record.id), record]));
|
|
2620
|
+
const placed = order.map(key => byId.get(String(key))).filter(Boolean);
|
|
2621
|
+
|
|
2622
|
+
if (placed.length === this.records.length) {
|
|
2623
|
+
this.records = placed;
|
|
2624
|
+
|
|
2625
|
+
this._renderProjection();
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
const first = previous.find(record => ids.map(String).includes(String(record.id)));
|
|
2630
|
+
const answer = await this._raise("record.reorder", {
|
|
2631
|
+
record_ids: [...ids],
|
|
2632
|
+
before,
|
|
2633
|
+
revision: first?._revision ?? first?.revision ?? null
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
if (answer?.answer !== "applied") {
|
|
2637
|
+
this.records = previous;
|
|
2638
|
+
|
|
2639
|
+
this._renderProjection();
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
return answer;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
_applyRecordPatch(records = []) {
|
|
2646
|
+
let applied = 0;
|
|
2647
|
+
|
|
2648
|
+
for (const record of records) {
|
|
2649
|
+
if (record && record.id != null && this.updateRecord(record.id, record)) applied += 1;
|
|
2650
|
+
} // A record the page does not hold cannot be patched into it; that is a
|
|
2651
|
+
// new row, and only a query knows where it belongs.
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
if (applied !== records.length) this.refresh("record-unknown");else this.notify("");
|
|
2655
|
+
return applied;
|
|
2656
|
+
} // The catalogue of row actions the table names in its own constructor --
|
|
2657
|
+
// the eye for «view», the pencil for «edit». It is captured once and kept,
|
|
2658
|
+
// because the projection writes its own narrowed list onto the very same
|
|
2659
|
+
// property: reading that list back would make the narrowed list the
|
|
2660
|
+
// catalogue, which is what the single read guarded against.
|
|
2661
|
+
//
|
|
2662
|
+
// The capture used to be one `??=` read of the table's own property with an
|
|
2663
|
+
// empty list behind it, which is where the defect was.
|
|
2664
|
+
// A list the view draws itself stands in the shadow template, so it is in
|
|
2665
|
+
// the tree before sf-view-table is defined, and an element that has not upgraded
|
|
2666
|
+
// carries no ACTIONS_SETTINGS_ROW at all: `[]` was cached, and `??=` never
|
|
2667
|
+
// looked again, because `[]` is not nullish. From then on every id was
|
|
2668
|
+
// unknown -- the warning below fired and «view» came out as a generic word
|
|
2669
|
+
// instead of the eye, on every list (Larena storage QA, 2026-10-22). So the
|
|
2670
|
+
// capture looks again while it holds nothing, and the list this view wrote
|
|
2671
|
+
// is remembered so it is never taken for the table's own.
|
|
2672
|
+
|
|
2673
|
+
|
|
2674
|
+
_captureRowActions(table) {
|
|
2675
|
+
if (this._knownRowActions?.length) return this._knownRowActions;
|
|
2676
|
+
const own = table?.ACTIONS_SETTINGS_ROW;
|
|
2677
|
+
|
|
2678
|
+
const named = Array.isArray(own) && own.length > 0 && own !== this._projectedRowActions;
|
|
2679
|
+
|
|
2680
|
+
this._knownRowActions = named ? own : [];
|
|
2681
|
+
if (!named) this._awaitRowActions();
|
|
2682
|
+
return this._knownRowActions;
|
|
2683
|
+
} // Waited for once, and the projection drawn again when the definition
|
|
2684
|
+
// lands: on a page whose list has settled nothing else would make the view
|
|
2685
|
+
// draw, and the catalogue can only be read from an upgraded element. The
|
|
2686
|
+
// Loader is told which tag is wanted, as it is for the move dialog's own
|
|
2687
|
+
// elements.
|
|
2688
|
+
|
|
2689
|
+
|
|
2690
|
+
_awaitRowActions() {
|
|
2691
|
+
if (this._awaitingRowActions) return;
|
|
2692
|
+
this._awaitingRowActions = true;
|
|
2693
|
+
globalThis.SF?.Loader?.ensure?.(["sf-view-table"]); // Read off globalThis and asked for softly: a stand that renders this
|
|
2694
|
+
// view over a document of its own need not keep a registry, and a view
|
|
2695
|
+
// that cannot wait simply keeps the fallback words.
|
|
2696
|
+
|
|
2697
|
+
const defined = globalThis.customElements?.whenDefined?.("sf-view-table");
|
|
2698
|
+
defined?.then?.(() => {
|
|
2699
|
+
if (this.isConnected) this._renderProjection();
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
_renderProjection() {
|
|
2704
|
+
this._capabilitiesKey = capabilitiesKey(this.knownPort);
|
|
2705
|
+
|
|
2706
|
+
this._readPrintedPageSize();
|
|
2707
|
+
|
|
2708
|
+
this._applyForAll();
|
|
2709
|
+
|
|
2710
|
+
if (this._current !== undefined && this.table && this.table.current !== this._current) this.table.current = this._current; // The saved-view controls appear when the host keeps saved views and
|
|
2711
|
+
// not before: this is the only place that runs both when a host is set
|
|
2712
|
+
// and when data arrives, which is when the answer can have changed.
|
|
2713
|
+
|
|
2714
|
+
const savedViews = this.shadowRoot?.querySelector?.("[data-saved-views]");
|
|
2715
|
+
|
|
2716
|
+
if (savedViews) {
|
|
2717
|
+
savedViews.hidden = !this.knownCapability("saved-views");
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
this.pagination?.setState?.({
|
|
2721
|
+
showMoreBusy: false
|
|
2722
|
+
}); // The page row no longer carries the band: the card does, so the strip
|
|
2723
|
+
// can hold the bottom of the window without the row coming along. The
|
|
2724
|
+
// attribute is written rather than put through setState, which clears a
|
|
2725
|
+
// false value by removing the attribute — and a removed `bottom` means
|
|
2726
|
+
// the default, which is to draw the band.
|
|
2727
|
+
|
|
2728
|
+
this.pagination?.setAttribute?.("bottom", "false");
|
|
2729
|
+
this.pagination?.setAttribute?.("size", this.size);
|
|
2730
|
+
|
|
2731
|
+
this._forwardLabels();
|
|
2732
|
+
|
|
2733
|
+
const table = this.table;
|
|
2734
|
+
|
|
2735
|
+
if (table) {
|
|
2736
|
+
// The two chrome columns. Written as attributes rather than left to the
|
|
2737
|
+
// table's own defaults, which are both on: an embedded list rendered a
|
|
2738
|
+
// checkbox column with «select all rows» and a gear column, and both
|
|
2739
|
+
// read as leftovers from a page table -- there is no bulk band and no
|
|
2740
|
+
// toolbar in an embedded list for either of them to lead to.
|
|
2741
|
+
// A refused list offers nothing to do with it: no selection, no
|
|
2742
|
+
// «Создать», no search or filter over rows the person may not see
|
|
2743
|
+
// (docs trial 5: they stayed active under «no access»). The
|
|
2744
|
+
// author's own toolbar setting comes back with the access.
|
|
2745
|
+
const refused = this._queryRefused === true;
|
|
2746
|
+
if (!table.hasAttribute?.("data-sf-host-toolbar")) table.setAttribute("data-sf-host-toolbar", table.getAttribute?.("toolbar") ?? "");
|
|
2747
|
+
const authorToolbar = table.getAttribute?.("data-sf-host-toolbar");
|
|
2748
|
+
if (refused) table.setAttribute("toolbar", "false");else if (authorToolbar) table.setAttribute("toolbar", authorToolbar);else table.removeAttribute?.("toolbar"); // The row boxes and the gear column are the view's chrome, unless the
|
|
2749
|
+
// host wrote them off on its table: what the table carried before the
|
|
2750
|
+
// view first touched it is the host's word, as for row-actions below.
|
|
2751
|
+
// A table written selectable="false" came back selectable="true" and
|
|
2752
|
+
// drew a box in every row of a list that has no bulk actions (Larena,
|
|
2753
|
+
// the sites list, 2026-11-05).
|
|
2754
|
+
|
|
2755
|
+
table.setAttribute("selectable", this.renders("select") && !refused && !hostWroteOff(table, "selectable") ? "true" : "false"); // A page's list becomes the compact list on a narrow container; an
|
|
2756
|
+
// embedded one (no toolbar) stays a table, and a layout the host
|
|
2757
|
+
// wrote on its table is the host's.
|
|
2758
|
+
|
|
2759
|
+
if (!table.hasAttribute?.("data-sf-host-layout")) {
|
|
2760
|
+
table.setAttribute("data-sf-host-layout", table.getAttribute?.("layout") ?? "");
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
if (!table.getAttribute?.("data-sf-host-layout")) {
|
|
2764
|
+
table.setAttribute("layout", this.renders("toolbar") ? "auto" : "table");
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
table.setAttribute("size", this.size);
|
|
2768
|
+
|
|
2769
|
+
this._tableScrollMode(table);
|
|
2770
|
+
|
|
2771
|
+
table.setAttribute("settings", this.renders("settings") && !hostWroteOff(table, "settings") ? "true" : "false"); // The column settings, without the column: it also carries each
|
|
2772
|
+
// row's menu, which is the host's row-actions, not this.
|
|
2773
|
+
|
|
2774
|
+
table.setAttribute("settings-menu", this.hostOffers("personal-column-settings") ? "true" : "false");
|
|
2775
|
+
table.setAttribute("creatable", this.hostOffers("create-record") && !refused ? "true" : "false");
|
|
2776
|
+
table.setAttribute("row-reorder", this.hostOffers("row-reorder") && this.treeMode !== "tree" ? "true" : "false");
|
|
2777
|
+
table.setAttribute("inline-edit", this.hostOffers("inline-edit") ? "true" : "false"); // A filter template is a saved view the table keeps by itself; in a
|
|
2778
|
+
// view it went nowhere, so it is offered where the host keeps views.
|
|
2779
|
+
|
|
2780
|
+
table.setAttribute("filter-templates", this.hostOffers("saved-views") ? "true" : "false"); // Row actions are the host's to claim. The intent was already
|
|
2781
|
+
// refused without the capability -- answered «unavailable» without
|
|
2782
|
+
// the host being called -- but the column and its buttons were drawn
|
|
2783
|
+
// all the same, so a person could press an action nothing would ever
|
|
2784
|
+
// apply. A view does not show a control the host has not claimed,
|
|
2785
|
+
// which is the same rule the saved-view controls already follow
|
|
2786
|
+
// (admin polygon, 2026-10-10).
|
|
2787
|
+
// A table the host wrote row-actions="false" on keeps it: the host has
|
|
2788
|
+
// put every action in its own row menu and still claims the
|
|
2789
|
+
// capability, because its menu raises record.open and record.mutate
|
|
2790
|
+
// through the port (Larena, 2026-10-12). What the table carried
|
|
2791
|
+
// before the view first touched it is the host's word.
|
|
2792
|
+
|
|
2793
|
+
if (!table.hasAttribute?.("data-sf-host-row-actions")) {
|
|
2794
|
+
table.setAttribute("data-sf-host-row-actions", table.getAttribute?.("row-actions") ?? "");
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const hostSaysNo = table.getAttribute?.("data-sf-host-row-actions") === "false";
|
|
2798
|
+
table.setAttribute("row-actions", !hostSaysNo && this.knownCapability("row-actions") ? "true" : "false"); // The accessible name of a list the host did not slot in. The fallback
|
|
2799
|
+
// table carries a name of its own, which is right for a page list and
|
|
2800
|
+
// wrong inside a group called «Счета»: it would announce «Записи».
|
|
2801
|
+
// Only the fallback is renamed -- a table a host slotted in keeps the
|
|
2802
|
+
// name the host gave it.
|
|
2803
|
+
|
|
2804
|
+
const slot = this.shadowRoot.querySelector('slot[name="content"]');
|
|
2805
|
+
const own = this.getAttribute("aria-label");
|
|
2806
|
+
if (own && slot && !slot.assignedElements().length) table.setAttribute("aria-label", own);
|
|
2807
|
+
const allowed = new Set(this.records.flatMap(record => Array.isArray(record.actions) ? record.actions : [])); // The catalogue is read once and kept, from an element that has
|
|
2808
|
+
// upgraded: narrowing the table's own list in place is destructive,
|
|
2809
|
+
// and so is capturing from an element that has nothing to read yet.
|
|
2810
|
+
// Both left the catalogue empty, and from then on every id the host
|
|
2811
|
+
// named was unknown -- «view» came out as a link labelled "view"
|
|
2812
|
+
// instead of the eye, and in a narrow column the label broke
|
|
2813
|
+
// mid-word and set the row height. _captureRowActions above.
|
|
2814
|
+
|
|
2815
|
+
const known = this._captureRowActions(table); // The host's own actions are named by its catalogue (row_actions,
|
|
2816
|
+
// port 1.8.0); an action the catalogue groups is offered once, as
|
|
2817
|
+
// the group, with a submenu of the members a record allows. Which
|
|
2818
|
+
// members a row shows is still that record's actions alone.
|
|
2819
|
+
|
|
2820
|
+
|
|
2821
|
+
const catalog = new Map(this.rowActionCatalog.map(entry => [entry.id, entry])); // In a tree, move and create are the view's own actions and named
|
|
2822
|
+
// by it, unless the host's catalogue names them.
|
|
2823
|
+
|
|
2824
|
+
const treeActions = this.treeMode === "tree" ? {
|
|
2825
|
+
move: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveAction", this),
|
|
2826
|
+
...(this.hostOffers("create-record") ? {
|
|
2827
|
+
create: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "createChildAction", this)
|
|
2828
|
+
} : {})
|
|
2829
|
+
} : {};
|
|
2830
|
+
|
|
2831
|
+
const single = id => {
|
|
2832
|
+
const entry = catalog.get(id) ?? (treeActions[id] ? {
|
|
2833
|
+
id,
|
|
2834
|
+
label: treeActions[id]
|
|
2835
|
+
} : null);
|
|
2836
|
+
|
|
2837
|
+
if (entry) {
|
|
2838
|
+
return {
|
|
2839
|
+
id,
|
|
2840
|
+
name: entry.label,
|
|
2841
|
+
danger: entry.danger === true,
|
|
2842
|
+
inline: false,
|
|
2843
|
+
component: {
|
|
2844
|
+
type: "button",
|
|
2845
|
+
props: {
|
|
2846
|
+
text: entry.label,
|
|
2847
|
+
appearance: "link",
|
|
2848
|
+
...(entry.icon ? {
|
|
2849
|
+
icon: entry.icon
|
|
2850
|
+
} : {})
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
} // An id the host's catalogue does not name gets a word, not its
|
|
2855
|
+
// raw id clipped in the column, and the host is told (docs trial 4).
|
|
2856
|
+
|
|
2857
|
+
|
|
2858
|
+
const found = known.find(action => action.id === id);
|
|
2859
|
+
if (found) return found;
|
|
2860
|
+
console.warn(`[sf-view] row action «${id}» has no row_actions entry; name it in the host's catalogue.`);
|
|
2861
|
+
const label = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "unnamedAction", this);
|
|
2862
|
+
return {
|
|
2863
|
+
id,
|
|
2864
|
+
name: label,
|
|
2865
|
+
inline: false,
|
|
2866
|
+
component: {
|
|
2867
|
+
type: "button",
|
|
2868
|
+
props: {
|
|
2869
|
+
text: label,
|
|
2870
|
+
appearance: "link",
|
|
2871
|
+
title: id
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
};
|
|
2876
|
+
|
|
2877
|
+
const groups = new Map();
|
|
2878
|
+
const listed = [];
|
|
2879
|
+
|
|
2880
|
+
for (const id of allowed) {
|
|
2881
|
+
const group = catalog.get(id)?.group;
|
|
2882
|
+
|
|
2883
|
+
if (!group) {
|
|
2884
|
+
listed.push(single(id));
|
|
2885
|
+
continue;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (!groups.has(group)) {
|
|
2889
|
+
const label = catalog.get(id).group_label ?? group;
|
|
2890
|
+
const entry = {
|
|
2891
|
+
id: `group:${group}`,
|
|
2892
|
+
name: label,
|
|
2893
|
+
members: [],
|
|
2894
|
+
inline: false,
|
|
2895
|
+
component: {
|
|
2896
|
+
type: "button",
|
|
2897
|
+
props: {
|
|
2898
|
+
text: label,
|
|
2899
|
+
appearance: "link"
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
groups.set(group, entry);
|
|
2904
|
+
listed.push(entry);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
groups.get(group).members.push(single(id));
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
table.ACTIONS_SETTINGS_ROW = listed; // Remembered by identity, so the capture never reads this narrowed
|
|
2911
|
+
// list back as the table's own catalogue.
|
|
2912
|
+
|
|
2913
|
+
this._projectedRowActions = listed; // A list whose every action lives in the row menu has no actions
|
|
2914
|
+
// column to draw.
|
|
2915
|
+
|
|
2916
|
+
if (listed.length && listed.every(action => action.inline === false)) {
|
|
2917
|
+
table.setAttribute("row-actions", "false");
|
|
2918
|
+
} // The port the list is holding, handed to the table for one thing:
|
|
2919
|
+
// the renderer set asks a host how to format a value, and a cell had
|
|
2920
|
+
// no host to ask. Only the formatting is read from it -- the table
|
|
2921
|
+
// raises its intents as it always has and the view re-raises them.
|
|
2922
|
+
|
|
2923
|
+
|
|
2924
|
+
table.cellHost = this.hostPort ?? null;
|
|
2925
|
+
table.setColumns?.(this.columns);
|
|
2926
|
+
|
|
2927
|
+
if (Array.isArray(this.appended)) {
|
|
2928
|
+
// Appending leaves the rows already on screen untouched, so a
|
|
2929
|
+
// row that was checked stays checked: the checkbox state lives
|
|
2930
|
+
// on the row itself.
|
|
2931
|
+
table.addRows?.(this.appended);
|
|
2932
|
+
} else {
|
|
2933
|
+
table.setRows?.(this.cappedRecords, this._newResult ? "result" : "rows");
|
|
2934
|
+
this._newResult = false;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
const first = this.query.sort[0];
|
|
2938
|
+
table.setSort?.(first?.key ?? null, first?.direction ?? null);
|
|
2939
|
+
table.setTemplates?.(Object.fromEntries(this.savedViews.map(view => [view.key, view])));
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const pagination = this.pagination; // Before the first answer there is no count to write. Writing one anyway
|
|
2943
|
+
// put «Всего: 0» under a heading that already said 450: the host's own
|
|
2944
|
+
// total in the markup was overwritten with this element's initial zero.
|
|
2945
|
+
|
|
2946
|
+
const counted = this._answered ? {
|
|
2947
|
+
total: this.total,
|
|
2948
|
+
countKnown: this.countKnown,
|
|
2949
|
+
totalExact: this.totalExact,
|
|
2950
|
+
hasNext: this.hasNext
|
|
2951
|
+
} : {}; // The page size in force is one of the sizes offered: 25 by default
|
|
2952
|
+
// against the pager's 10/20/30/40 showed a size the select could not
|
|
2953
|
+
// name (docs trial 4). An author's own page-sizes is kept, the size added.
|
|
2954
|
+
|
|
2955
|
+
const sizes = String(pagination?.getAttribute?.("page-sizes") ?? "10,20,30,40").split(/[\s,]+/u).map(Number).filter(size => Number.isInteger(size) && size > 0);
|
|
2956
|
+
const perPage = Number(this.query.per_page);
|
|
2957
|
+
|
|
2958
|
+
if (pagination && Number.isInteger(perPage) && perPage > 0 && !sizes.includes(perPage)) {
|
|
2959
|
+
pagination.setAttribute("page-sizes", [...sizes, perPage].sort((a, b) => a - b).join(","));
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
pagination?.setState?.({
|
|
2963
|
+
current: this.query.page,
|
|
2964
|
+
pageSize: this.query.per_page,
|
|
2965
|
+
...counted,
|
|
2966
|
+
canCount: this.knownCapability("count-on-request"),
|
|
2967
|
+
counting: false
|
|
2968
|
+
}); // The parts a consumer left out. Hidden rather than removed, so turning one
|
|
2969
|
+
// back on does not rebuild the shadow tree and lose what a host slotted
|
|
2970
|
+
// into it.
|
|
2971
|
+
// The shadow template is written once and cannot carry a language: it
|
|
2972
|
+
// is built before the element is anywhere. These two are named here,
|
|
2973
|
+
// where the page is readable.
|
|
2974
|
+
|
|
2975
|
+
const layout = this.shadowRoot.querySelector('[part="layout"]'); // The author's name for the view, when it has one: the generic «Представление
|
|
2976
|
+
// данных» replaced it (docs trial 4).
|
|
2977
|
+
|
|
2978
|
+
if (layout) layout.setAttribute("aria-label", this.getAttribute("aria-label") || (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "layoutLabel", this));
|
|
2979
|
+
const fallbackTable = this.shadowRoot.querySelector(`slot[name="content"] > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}`);
|
|
2980
|
+
|
|
2981
|
+
if (fallbackTable && !fallbackTable.getAttribute("aria-label")) {
|
|
2982
|
+
fallbackTable.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "recordsLabel", this));
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
const toolbar = this.shadowRoot.querySelector('[part="toolbar"]'); // A slotted table that draws its own toolbar (search, filter) needs no
|
|
2986
|
+
// second one: the view's fallback search stood beside the table's, two
|
|
2987
|
+
// searches in the documented minimal markup (docs trial 4).
|
|
2988
|
+
|
|
2989
|
+
const slottedTable = this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"]`);
|
|
2990
|
+
const authorToolbar = slottedTable?.getAttribute("data-sf-host-toolbar") ?? slottedTable?.getAttribute("toolbar");
|
|
2991
|
+
const tableHasToolbar = Boolean(slottedTable) && authorToolbar !== "false";
|
|
2992
|
+
const ownToolbar = this.querySelector(':scope > [slot="toolbar"]');
|
|
2993
|
+
if (toolbar) toolbar.hidden = !this.renders("toolbar") || tableHasToolbar && !ownToolbar || this._queryRefused === true;
|
|
2994
|
+
const pageRow = this.shadowRoot.querySelector('slot[name="pagination"]'); // A tree has no pages: a level that is long ends in Show more, and the
|
|
2995
|
+
// host gives no total for the page row to count. Nor is there a page
|
|
2996
|
+
// row before the first answer or when there is no list to page: it
|
|
2997
|
+
// said «Всего: 10» over a loading, failed or refused list (docs trial 4).
|
|
2998
|
+
|
|
2999
|
+
const nothingToCount = !this._answered || this._queryRefused || ["error", "no-access"].includes(this.getAttribute("data-state"));
|
|
3000
|
+
if (pageRow) pageRow.hidden = !this.renders("pagination") || this.treeMode === "tree" || nothingToCount;
|
|
3001
|
+
|
|
3002
|
+
if (!this.capHidesRows) {
|
|
3003
|
+
if (this._showAll?.isConnected) this._showAll.remove();
|
|
3004
|
+
this._showAll = null;
|
|
3005
|
+
} else {
|
|
3006
|
+
const band = this.showAllBand;
|
|
3007
|
+
let button = band.querySelector("[data-show-all]");
|
|
3008
|
+
|
|
3009
|
+
if (!button) {
|
|
3010
|
+
button = document.createElement("sf-button");
|
|
3011
|
+
button.setAttribute("data-show-all", "");
|
|
3012
|
+
button.setAttribute("appearance", "link");
|
|
3013
|
+
button.setAttribute("size", "1");
|
|
3014
|
+
band.append(button);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
button.setAttribute("text", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "showAll", this));
|
|
3018
|
+
let count = band.querySelector("[data-show-count]");
|
|
3019
|
+
|
|
3020
|
+
if (!count) {
|
|
3021
|
+
count = document.createElement("span");
|
|
3022
|
+
count.setAttribute("data-show-count", "");
|
|
3023
|
+
band.append(count);
|
|
3024
|
+
} // «N of M» only when the host has already said M. A count fetched to
|
|
3025
|
+
// decide what a link says would spend the host's query on this
|
|
3026
|
+
// element's own layout.
|
|
3027
|
+
|
|
3028
|
+
|
|
3029
|
+
count.textContent = this.countKnown ? (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "ofTotal", this).replace("{shown}", String(this.cappedRecords.length)).replace("{total}", this._shownTotal(this.total)) : "";
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const search = this.shadowRoot.querySelector("[data-search]");
|
|
3033
|
+
if (search && search.value !== this.query.search) search.value = this.query.search;
|
|
3034
|
+
const select = this.shadowRoot.querySelector("[data-view]");
|
|
3035
|
+
|
|
3036
|
+
if (select) {
|
|
3037
|
+
const previous = select.value;
|
|
3038
|
+
select.replaceChildren(new Option((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "currentViewText", this), ""), ...this.savedViews.map(view => new Option(view.label, view.key)));
|
|
3039
|
+
select.value = this.savedViews.some(view => view.key === previous) ? previous : "";
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
saveView({
|
|
3044
|
+
key,
|
|
3045
|
+
label,
|
|
3046
|
+
level = "personal",
|
|
3047
|
+
revision,
|
|
3048
|
+
roles,
|
|
3049
|
+
layout,
|
|
3050
|
+
settings
|
|
3051
|
+
} = {}) {
|
|
3052
|
+
if (!key || !label) throw new TypeError("Saved view needs key and label");
|
|
3053
|
+
if (!["personal", "shared"].includes(level)) throw new TypeError("Only personal and shared views can be written"); // Shared views came with port 1.1.0 and stay in every later edition: the
|
|
3054
|
+
// test was for 1.1.0 exactly, so 1.2.0 onwards refused them.
|
|
3055
|
+
|
|
3056
|
+
const [major, minor] = String(this.hostPort?.version ?? "0.0.0").split(".").map(Number);
|
|
3057
|
+
|
|
3058
|
+
if (level === "shared" && !(major > 1 || major === 1 && minor >= 1)) {
|
|
3059
|
+
return Promise.resolve({
|
|
3060
|
+
answer: "unavailable",
|
|
3061
|
+
reason: "saved-view-levels-require-port-1.1.0"
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
return this._raise(level === "shared" ? "view.save_shared" : "view.save", {
|
|
3066
|
+
key,
|
|
3067
|
+
label,
|
|
3068
|
+
revision,
|
|
3069
|
+
query: copy(this.query),
|
|
3070
|
+
roles: copy(roles ?? {}),
|
|
3071
|
+
layout: copy(layout ?? {
|
|
3072
|
+
columns: this.columns
|
|
3073
|
+
}),
|
|
3074
|
+
settings: copy(settings ?? this.columnSettings ?? {}),
|
|
3075
|
+
...(level === "shared" ? {
|
|
3076
|
+
target: "shared"
|
|
3077
|
+
} : {})
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
deleteView(key) {
|
|
3082
|
+
return this._raise("view.delete", {
|
|
3083
|
+
key
|
|
3084
|
+
});
|
|
3085
|
+
} // Which view was opened, so its own settings can be put back when the
|
|
3086
|
+
// answer arrives: the fields the filter was showing are saved with it.
|
|
3087
|
+
|
|
3088
|
+
|
|
3089
|
+
openView(key) {
|
|
3090
|
+
this._openedView = key;
|
|
3091
|
+
return this._raise("view.open", {
|
|
3092
|
+
key
|
|
3093
|
+
});
|
|
3094
|
+
} // The part of a saved view that belongs to the table rather than to the
|
|
3095
|
+
// query: which filter fields it was showing.
|
|
3096
|
+
|
|
3097
|
+
|
|
3098
|
+
_restoreOpenedView() {
|
|
3099
|
+
const key = this._openedView;
|
|
3100
|
+
if (!key) return this;
|
|
3101
|
+
const view = this.savedViews.find(candidate => candidate?.key === key);
|
|
3102
|
+
if (!view) return this;
|
|
3103
|
+
this._openedView = null;
|
|
3104
|
+
const fields = view.settings?.filter_fields;
|
|
3105
|
+
|
|
3106
|
+
if (Array.isArray(fields)) {
|
|
3107
|
+
this.table?.setActiveFilterFields?.(fields.filter(item => typeof item === "string"));
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
return this;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
saveColumnSettings(columnSettings, {
|
|
3114
|
+
shared = false,
|
|
3115
|
+
base_revision
|
|
3116
|
+
} = {}) {
|
|
3117
|
+
const layer = shared ? "shared" : "personal"; // The port's target.revision is the base when the host sent no layer of
|
|
3118
|
+
// that kind yet: the first personal save had none, and a column drag
|
|
3119
|
+
// threw an uncaught TypeError that never reached the host (docs trial 4).
|
|
3120
|
+
|
|
3121
|
+
const target = this.columnSettings?.target;
|
|
3122
|
+
const targetRevision = target && (target.kind ?? target.target ?? target) === layer ? target.revision : target?.revision ?? null;
|
|
3123
|
+
const revision = base_revision ?? this.columnSettings?.layers?.[layer]?.revision ?? targetRevision;
|
|
3124
|
+
|
|
3125
|
+
if (revision == null) {
|
|
3126
|
+
this._showAnswer(shared ? "settings.save_shared" : "settings.save_personal", {
|
|
3127
|
+
answer: "unavailable",
|
|
3128
|
+
reason: "no-base-revision"
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
return Promise.resolve({
|
|
3132
|
+
answer: "unavailable",
|
|
3133
|
+
reason: "no-base-revision"
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
return this._raise(shared ? "settings.save_shared" : "settings.save_personal", {
|
|
3138
|
+
columnSettings: copy(columnSettings),
|
|
3139
|
+
base_revision: revision,
|
|
3140
|
+
...(shared ? {
|
|
3141
|
+
target: "shared"
|
|
3142
|
+
} : {})
|
|
3143
|
+
}).then(answer => {
|
|
3144
|
+
// A conflict means the layer moved on: the revision this view holds
|
|
3145
|
+
// is stale, and every later change conflicted with it until a reload
|
|
3146
|
+
// (docs trial 5). The view takes the layer's current revision when
|
|
3147
|
+
// the host names it, and otherwise reads the list again, whose
|
|
3148
|
+
// answer carries the column settings as they are now.
|
|
3149
|
+
// The conflict carries current_revision (and the layer's columns)
|
|
3150
|
+
// beside answer, as port 1.12.0 states and every host gives it;
|
|
3151
|
+
// data.* is read too, for a host that followed 1.10.0's words.
|
|
3152
|
+
if (answer?.answer === "conflict") {
|
|
3153
|
+
const current = answer.current_revision ?? answer.data?.current_revision;
|
|
3154
|
+
const columns = answer.columns ?? answer.data?.columns;
|
|
3155
|
+
if (answer.data?.column_settings) this.columnSettings = copy(answer.data.column_settings);else if (current != null) {
|
|
3156
|
+
const layers = { ...(this.columnSettings?.layers ?? {})
|
|
3157
|
+
};
|
|
3158
|
+
layers[layer] = { ...(layers[layer] ?? {}),
|
|
3159
|
+
revision: current,
|
|
3160
|
+
...(columns ? {
|
|
3161
|
+
columns: copy(columns)
|
|
3162
|
+
} : {})
|
|
3163
|
+
};
|
|
3164
|
+
this.columnSettings = { ...(this.columnSettings ?? {}),
|
|
3165
|
+
layers
|
|
3166
|
+
};
|
|
3167
|
+
} else this.refresh("settings-conflict");
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
return answer;
|
|
3171
|
+
});
|
|
3172
|
+
} // Resetting the person's layer is a write of nothing against its revision,
|
|
3173
|
+
// which is exactly what the port calls a reset: "a write against its
|
|
3174
|
+
// revision, never a deletion". The empty object matters — the host owns the
|
|
3175
|
+
// defaults, and writing this component's idea of them (everything visible,
|
|
3176
|
+
// no widths, nothing pinned) would overwrite them.
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
resetColumnSettings() {
|
|
3180
|
+
return this.saveColumnSettings({}, {});
|
|
3181
|
+
} // An estimate reads «~N» wherever this element writes the number itself.
|
|
3182
|
+
|
|
3183
|
+
|
|
3184
|
+
_shownTotal(total) {
|
|
3185
|
+
return this.totalExact ? String(total) : `~${total}`;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
applyUnderFilter(actionId, confirmedTotal) {
|
|
3189
|
+
// An estimate is not a number anyone can confirm: count first, show the
|
|
3190
|
+
// exact number and stop. The person confirmed «~N», not this number,
|
|
3191
|
+
// so nothing is written until they confirm it (standard 1.3.8 §19.4;
|
|
3192
|
+
// stand 24 S15: the counted number went to the host at once, more
|
|
3193
|
+
// records than the person agreed to when the estimate was low). The
|
|
3194
|
+
// answer is the conflict a moved count gives: current_total is the
|
|
3195
|
+
// number to confirm.
|
|
3196
|
+
if (!this.totalExact) {
|
|
3197
|
+
return this.count("action-for-all").then(answer => {
|
|
3198
|
+
if (!(this.totalExact && answer?.answer === "applied")) return answer;
|
|
3199
|
+
|
|
3200
|
+
this._renderBulkBand();
|
|
3201
|
+
|
|
3202
|
+
return {
|
|
3203
|
+
answer: "conflict",
|
|
3204
|
+
reason: "total-counted",
|
|
3205
|
+
current_total: this.total
|
|
3206
|
+
};
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (!Number.isSafeInteger(confirmedTotal) || confirmedTotal !== this.total) {
|
|
3211
|
+
throw new TypeError("Confirm the current total before a filter-wide action");
|
|
3212
|
+
} // A count that moved meanwhile is a conflict carrying the current total
|
|
3213
|
+
// (vocabulary §3.1): the page row shows that number at once, so the
|
|
3214
|
+
// person confirms what is there now, not what was.
|
|
3215
|
+
|
|
3216
|
+
|
|
3217
|
+
return this._raise("bulk.apply_under_filter", {
|
|
3218
|
+
action_id: actionId,
|
|
3219
|
+
filters: copy(this.query.filters),
|
|
3220
|
+
confirmed_total: confirmedTotal
|
|
3221
|
+
}).then(answer => {
|
|
3222
|
+
const current = answer?.current_total ?? answer?.data?.total;
|
|
3223
|
+
|
|
3224
|
+
if (answer?.answer === "conflict" && Number.isSafeInteger(current) && current >= 0) {
|
|
3225
|
+
this.total = current;
|
|
3226
|
+
this.totalExact = true;
|
|
3227
|
+
this.pagination?.setState?.({
|
|
3228
|
+
total: current,
|
|
3229
|
+
countKnown: true,
|
|
3230
|
+
totalExact: true
|
|
3231
|
+
});
|
|
3232
|
+
|
|
3233
|
+
this._renderBulkBand();
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
return answer;
|
|
3237
|
+
});
|
|
3238
|
+
} // The number, asked for on its own. The view resumes whatever wanted it:
|
|
3239
|
+
// showing it, jumping to the last page, or an action under the filter that
|
|
3240
|
+
// has to name a confirmed total. A refusal puts the view back where it was
|
|
3241
|
+
// and says so — a for-all box left ticked over a number nobody has would be
|
|
3242
|
+
// a promise the view cannot keep.
|
|
3243
|
+
|
|
3244
|
+
|
|
3245
|
+
async count(reason = "total") {
|
|
3246
|
+
const requested = this._countKey();
|
|
3247
|
+
|
|
3248
|
+
const answer = await this._raise("query.count", {
|
|
3249
|
+
query: {
|
|
3250
|
+
search: this.query.search ?? "",
|
|
3251
|
+
filters: copy(this.query.filters ?? {})
|
|
3252
|
+
},
|
|
3253
|
+
reason
|
|
3254
|
+
});
|
|
3255
|
+
const total = answer?.data?.total;
|
|
3256
|
+
|
|
3257
|
+
if (answer?.answer !== "applied" || !Number.isSafeInteger(total) || total < 0) {
|
|
3258
|
+
this.pagination?.setState?.({
|
|
3259
|
+
counting: false
|
|
3260
|
+
});
|
|
3261
|
+
if (reason === "action-for-all") this.pagination?.setState?.({
|
|
3262
|
+
actionForAll: false
|
|
3263
|
+
});
|
|
3264
|
+
return answer;
|
|
3265
|
+
} // The query may have moved on while the host was counting; a number
|
|
3266
|
+
// belonging to a query nobody is looking at any more is not shown.
|
|
3267
|
+
|
|
3268
|
+
|
|
3269
|
+
if (requested !== this._countKey()) {
|
|
3270
|
+
this.pagination?.setState?.({
|
|
3271
|
+
counting: false
|
|
3272
|
+
});
|
|
3273
|
+
return answer;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
this.total = total;
|
|
3277
|
+
this.countKnown = true;
|
|
3278
|
+
this.totalExact = answer.data.total_exact !== false;
|
|
3279
|
+
this.countedQuery = requested;
|
|
3280
|
+
this.pagination?.setState?.({
|
|
3281
|
+
counting: false,
|
|
3282
|
+
countKnown: true,
|
|
3283
|
+
totalExact: this.totalExact,
|
|
3284
|
+
total
|
|
3285
|
+
});
|
|
3286
|
+
if (reason === "last-page") this.pagination?.lastPage?.();
|
|
3287
|
+
return answer;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
_action(detail, {
|
|
3291
|
+
selectedBulk = false
|
|
3292
|
+
} = {}) {
|
|
3293
|
+
const ids = detail?.record_ids;
|
|
3294
|
+
if (!Array.isArray(ids) || !ids.length || typeof detail.action_id !== "string") return;
|
|
3295
|
+
const chosen = ids.map(id => this.records.find(record => record.id === id)); // In a tree two of a record's actions are the view's own: move opens
|
|
3296
|
+
// the choice of the places the host offers, create makes a node under
|
|
3297
|
+
// it. Each is offered only when the record lists it.
|
|
3298
|
+
|
|
3299
|
+
if (this.treeMode === "tree" && !selectedBulk && ids.length === 1 && chosen[0]?.actions?.includes(detail.action_id)) {
|
|
3300
|
+
if (detail.action_id === "move") return this.openMoveDialog(chosen[0]);
|
|
3301
|
+
if (detail.action_id === "create") return this._raise("view.create_record", {
|
|
3302
|
+
item: null,
|
|
3303
|
+
parent: chosen[0].id
|
|
3304
|
+
});
|
|
3305
|
+
} // A selected-bulk action has its own ID and permission in the host, even
|
|
3306
|
+
// when just one row is selected. The host checks the whole batch.
|
|
3307
|
+
|
|
3308
|
+
|
|
3309
|
+
if (chosen.some(record => !record) || !selectedBulk && ids.length === 1 && !chosen[0].actions?.includes(detail.action_id)) {
|
|
3310
|
+
this._showAnswer("record.mutate", {
|
|
3311
|
+
answer: "refused",
|
|
3312
|
+
reason: "action-not-allowed"
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
return;
|
|
3316
|
+
} // Opening a record is a request a page or an assembly takes
|
|
3317
|
+
// (vocabulary §5 open {id}): sf-data-view-open, then the host's
|
|
3318
|
+
// record.open unless a listener refused it (preventDefault).
|
|
3319
|
+
|
|
3320
|
+
|
|
3321
|
+
if (!selectedBulk && ids.length === 1 && detail.action_id === "view") {
|
|
3322
|
+
const allowed = this.sfEmit("open", {
|
|
3323
|
+
id: ids[0]
|
|
3324
|
+
}, {
|
|
3325
|
+
cancelable: true
|
|
3326
|
+
});
|
|
3327
|
+
return allowed ? this._raise("record.open", detail) : null;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
const revisions = Object.fromEntries(chosen.map(record => [String(record.id), record.revision]));
|
|
3331
|
+
|
|
3332
|
+
const written = this._raise(selectedBulk || ids.length > 1 ? "bulk.apply_selected" : "record.mutate", { ...detail,
|
|
3333
|
+
revisions
|
|
3334
|
+
}); // The fact a screen closes a deleted record on (vocabulary §4: a
|
|
3335
|
+
// fact after applied): sf-data-view-deleted {ids}.
|
|
3336
|
+
|
|
3337
|
+
|
|
3338
|
+
if (detail.action_id === "delete") {
|
|
3339
|
+
return Promise.resolve(written).then(answer => {
|
|
3340
|
+
if (answer?.answer === "applied") this.sfEmit("deleted", {
|
|
3341
|
+
ids: [...ids]
|
|
3342
|
+
});
|
|
3343
|
+
return answer;
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
return written;
|
|
3348
|
+
} // A value pointing at another record opens it in the host's panel without
|
|
3349
|
+
// loading a page (owner, 2026-10-14). Taken only when the host opens
|
|
3350
|
+
// records; when its answer is not an opening, the address is followed, so
|
|
3351
|
+
// the click still goes somewhere.
|
|
3352
|
+
|
|
3353
|
+
|
|
3354
|
+
_openReference(event) {
|
|
3355
|
+
if (!this.hostPort || !this.hostOffers("row-actions")) return;
|
|
3356
|
+
event.preventDefault(); // The table's detail is camelCase on the element (standard 1.3.8 §3.2);
|
|
3357
|
+
// the port's payload is snake_case.
|
|
3358
|
+
|
|
3359
|
+
const {
|
|
3360
|
+
href,
|
|
3361
|
+
recordId,
|
|
3362
|
+
record_id: recordIdBefore,
|
|
3363
|
+
...detail
|
|
3364
|
+
} = event.detail ?? {};
|
|
3365
|
+
return this._raise("record.open", { ...detail,
|
|
3366
|
+
record_id: recordId ?? recordIdBefore ?? null,
|
|
3367
|
+
href
|
|
3368
|
+
}).then(answer => {
|
|
3369
|
+
if (!["applied", "accepted", "opened"].includes(answer?.answer) && href) globalThis.location?.assign?.(href);
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
_onEvent(event) {
|
|
3374
|
+
if (this._seenEvents.has(event)) return;
|
|
3375
|
+
|
|
3376
|
+
this._seenEvents.add(event); // The port's event for a shared saved view (view.save_shared): a page
|
|
3377
|
+
// raises it on the view itself, so it is taken before the rule that
|
|
3378
|
+
// ignores the view's own events. It was declared and never listened to.
|
|
3379
|
+
|
|
3380
|
+
|
|
3381
|
+
if (event.type === "sf-view-saved-view-intent") {
|
|
3382
|
+
if (event.currentTarget !== this) return;
|
|
3383
|
+
const view = event.detail ?? {};
|
|
3384
|
+
return this.saveView({
|
|
3385
|
+
key: view.key,
|
|
3386
|
+
label: view.label ?? view.key,
|
|
3387
|
+
revision: view.revision,
|
|
3388
|
+
level: "shared",
|
|
3389
|
+
roles: view.roles
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
if (event.target === this || event.target?.closest?.((0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view")) !== this && !this.shadowRoot.contains(event.target)) return; // An event crossing the shadow boundary is observed once, at the shadow root.
|
|
3394
|
+
|
|
3395
|
+
if (event.currentTarget === this && this.shadowRoot.contains(event.target)) return;
|
|
3396
|
+
if (event.type === "change" && event.target.matches?.("[data-search]")) return this.changeQuery({
|
|
3397
|
+
search: event.target.value
|
|
3398
|
+
}, "search");
|
|
3399
|
+
if (event.type === "change" && event.target.matches?.("[data-view]")) return event.target.value && this.openView(event.target.value);
|
|
3400
|
+
if (event.type === "click" && event.target.matches?.("[data-show-all]")) return this.openList("row-cap");
|
|
3401
|
+
|
|
3402
|
+
if (event.type === "click" && event.target.matches?.("[data-save]")) {
|
|
3403
|
+
const label = this.shadowRoot.querySelector("[data-view-name]")?.value?.trim();
|
|
3404
|
+
if (label) return this.saveView({
|
|
3405
|
+
key: label,
|
|
3406
|
+
label
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
if (event.type === "click" && event.target.matches?.("[data-delete]")) {
|
|
3411
|
+
const key = this.shadowRoot.querySelector("[data-view]").value;
|
|
3412
|
+
if (key) return this.deleteView(key);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
if (event.type === "sf-view-table-cell-set") return this.setCell(event.detail ?? {});
|
|
3416
|
+
if (event.type === "sf-view-table-move") return this.reorderRows(event.detail ?? {});
|
|
3417
|
+
|
|
3418
|
+
if (event.type === "sf-view-table-search-change") {
|
|
3419
|
+
const search = typeof event.detail === "object" && event.detail !== null ? event.detail.search : event.detail;
|
|
3420
|
+
return this.changeQuery({
|
|
3421
|
+
search: String(search ?? "")
|
|
3422
|
+
}, "search");
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
if (event.type === "sf-view-table-filter-change") return this.changeQuery({
|
|
3426
|
+
filters: copy(event.detail?.filters ?? event.detail?.values ?? event.detail ?? {})
|
|
3427
|
+
}, "filter");
|
|
3428
|
+
|
|
3429
|
+
if (event.type === "sf-view-table-sort-change") {
|
|
3430
|
+
const sort = event.detail?.direction ? [copy(event.detail)] : [];
|
|
3431
|
+
|
|
3432
|
+
this._keepPreference({
|
|
3433
|
+
sort
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
return this.changeQuery({
|
|
3437
|
+
sort
|
|
3438
|
+
}, "sort");
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
if (event.type === "sf-pagination-change") return this.changeQuery({
|
|
3442
|
+
page: event.detail?.current
|
|
3443
|
+
}, "page-change");
|
|
3444
|
+
|
|
3445
|
+
if (event.type === "sf-pagination-size-change") {
|
|
3446
|
+
const size = event.detail?.["page-size"];
|
|
3447
|
+
if (Number.isInteger(size)) this._keepPreference({
|
|
3448
|
+
pageSize: size
|
|
3449
|
+
}); // Another page size reads the list from its first page.
|
|
3450
|
+
|
|
3451
|
+
return this.changeQuery({
|
|
3452
|
+
per_page: size,
|
|
3453
|
+
page: 1
|
|
3454
|
+
}, "page-size-change");
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
if (event.type === "sf-pagination-load-more") return this.changeQuery({
|
|
3458
|
+
page: this.query.page + 1
|
|
3459
|
+
}, "show-more");
|
|
3460
|
+
if (event.type === "sf-pagination-count") return this.count(event.detail?.reason ?? "total");
|
|
3461
|
+
if (event.type === "sf-view-table-column-settings-reset") return this.resetColumnSettings(); // The panel's cross says the selection is over. The rows hold it, so
|
|
3462
|
+
// the view passes the word on rather than tracking a copy of its own.
|
|
3463
|
+
|
|
3464
|
+
if (event.type === "sf-pagination-selection-clear") return this.clearSelection();
|
|
3465
|
+
|
|
3466
|
+
if (event.type === "sf-pagination-action-change" || event.type === "sf-pagination-action-for-all-change") {
|
|
3467
|
+
// The chooser and For-all live in the band now; the pagination still
|
|
3468
|
+
// holds their state, so the band is redrawn when it changes.
|
|
3469
|
+
this._renderBulkBand();
|
|
3470
|
+
|
|
3471
|
+
return undefined;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
if (event.type === "sf-view-table-refresh") return this.refresh("retry");
|
|
3475
|
+
if (event.type === "sf-view-table-reference-open") return this._openReference(event);
|
|
3476
|
+
|
|
3477
|
+
if (event.type === "sf-view-table-expand") {
|
|
3478
|
+
const id = event.detail?.id;
|
|
3479
|
+
return event.detail?.expanded ? this.collapseNode(id) : this.expandNode(id);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
if (event.type === "sf-view-table-load-more") return this.loadMoreNodes(event.detail?.parent, event.detail?.cursor);
|
|
3483
|
+
if (event.type === "sf-view-table-apply") return this._action({
|
|
3484
|
+
action_id: event.detail?.action,
|
|
3485
|
+
record_ids: event.detail?.ids
|
|
3486
|
+
}, {
|
|
3487
|
+
selectedBulk: event.detail?.scope === "selection"
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3490
|
+
if (event.type === "sf-view-table-save") {
|
|
3491
|
+
return this.saveColumnSettings(event.detail?.columnSettings ?? event.detail, {
|
|
3492
|
+
shared: event.detail?.target === "shared",
|
|
3493
|
+
base_revision: event.detail?.base_revision
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
if (event.type === "sf-pagination-apply") {
|
|
3498
|
+
if (event.detail?.scope === "filter") return this.applyUnderFilter(event.detail.action, event.detail.total);
|
|
3499
|
+
return this._action({
|
|
3500
|
+
action_id: event.detail?.action,
|
|
3501
|
+
record_ids: this.table?.getSelectedRecordIds?.() ?? []
|
|
3502
|
+
}, {
|
|
3503
|
+
selectedBulk: true
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
if (event.type === "sf-view-table-select") {
|
|
3508
|
+
this.pagination?.setState?.({
|
|
3509
|
+
selectedCount: event.detail?.ids?.length ?? 0
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
this._renderBulkBand();
|
|
3513
|
+
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
const intent = EVENTS[event.type];
|
|
3518
|
+
if (intent === "query.change") return this.refresh("table-context");
|
|
3519
|
+
|
|
3520
|
+
if (intent === "view.save") {
|
|
3521
|
+
// Reordering or pinning on the templates tab sends every changed
|
|
3522
|
+
// template at once, as an array: each one is its own save (it threw
|
|
3523
|
+
// «Saved view needs key and label» on the array itself).
|
|
3524
|
+
const save = (view = {}) => this.saveView({
|
|
3525
|
+
key: view.key,
|
|
3526
|
+
label: view.label ?? view.name ?? view.key,
|
|
3527
|
+
revision: view.revision,
|
|
3528
|
+
level: view.level ?? "personal",
|
|
3529
|
+
roles: view.roles,
|
|
3530
|
+
layout: view.layout,
|
|
3531
|
+
settings: view.settings
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
const detail = event.detail ?? {};
|
|
3535
|
+
return Array.isArray(detail) ? Promise.all(detail.filter(view => view?.key).map(save)) : save(detail);
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
if (intent) return this._raise(intent, copy(event.detail ?? {}));
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
} // Not a Smart base element, so it carries the same brand by hand: quiet when
|
|
3542
|
+
// the component arrives twice, loud when something else holds the tag.
|
|
3543
|
+
|
|
3544
|
+
|
|
3545
|
+
SfDataView.sfElementBrand = "simai.smart-base.v1";
|
|
3546
|
+
(0,_view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__.answerActions)(SfDataView, _component_json__WEBPACK_IMPORTED_MODULE_6__);
|
|
3547
|
+
(0,_view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__.settingSetters)(SfDataView, _component_json__WEBPACK_IMPORTED_MODULE_6__);
|
|
3548
|
+
const definedDataView = customElements.get("sf-view");
|
|
3549
|
+
|
|
3550
|
+
if (!definedDataView) {
|
|
3551
|
+
customElements.define("sf-view", SfDataView); // sf-data-view, the former tag, is a subclass defined beside it until
|
|
3552
|
+
// 2027-01-01 (aliases.json, ui-source change journal tag-sf-data-view-to-sf-view).
|
|
3553
|
+
|
|
3554
|
+
(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.defineTagAliases)(SfDataView, "sf-view");
|
|
3555
|
+
} else if (definedDataView.sfElementBrand !== SfDataView.sfElementBrand) {
|
|
3556
|
+
console.warn("SfDataView.define(): sf-view is already defined", definedDataView);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (SfDataView);
|
|
3560
|
+
__webpack_async_result__();
|
|
3561
|
+
} catch(e) { __webpack_async_result__(e); } });
|
|
3562
|
+
|
|
3563
|
+
/***/ },
|
|
3564
|
+
|
|
3565
|
+
/***/ "29f88c884ba1"
|
|
3566
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
3567
|
+
|
|
3568
|
+
__webpack_require__.r(__webpack_exports__);
|
|
3569
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3570
|
+
/* harmony export */ ROOT: () => (/* binding */ ROOT),
|
|
3571
|
+
/* harmony export */ acceptLevel: () => (/* binding */ acceptLevel),
|
|
3572
|
+
/* harmony export */ collapse: () => (/* binding */ collapse),
|
|
3573
|
+
/* harmony export */ createTree: () => (/* binding */ createTree),
|
|
3574
|
+
/* harmony export */ dropLevel: () => (/* binding */ dropLevel),
|
|
3575
|
+
/* harmony export */ expandKnown: () => (/* binding */ expandKnown),
|
|
3576
|
+
/* harmony export */ hasLevel: () => (/* binding */ hasLevel),
|
|
3577
|
+
/* harmony export */ knownRecords: () => (/* binding */ knownRecords),
|
|
3578
|
+
/* harmony export */ markLoading: () => (/* binding */ markLoading),
|
|
3579
|
+
/* harmony export */ visibleRows: () => (/* binding */ visibleRows)
|
|
3580
|
+
/* harmony export */ });
|
|
3581
|
+
// The tree a data view shows when its host answers in levels (data view port
|
|
3582
|
+
// 1.8.0). The host answers one level at a time -- the roots, or one node's
|
|
3583
|
+
// children after tree.expand -- and the view keeps what it has been told and
|
|
3584
|
+
// which nodes are open. The rows the table draws are a walk of that, in order,
|
|
3585
|
+
// each row knowing its level and its place among its siblings; nothing here
|
|
3586
|
+
// builds a tree out of a flat dump or decides where a node may go.
|
|
3587
|
+
const ROOT = "\u0000root";
|
|
3588
|
+
|
|
3589
|
+
const keyOf = parent => parent === null || parent === undefined ? ROOT : String(parent);
|
|
3590
|
+
|
|
3591
|
+
function createTree() {
|
|
3592
|
+
return {
|
|
3593
|
+
nodes: new Map(),
|
|
3594
|
+
children: new Map(),
|
|
3595
|
+
expanded: new Set(),
|
|
3596
|
+
loading: new Set()
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* One level the host answered: the roots (parent null) or a node's children.
|
|
3601
|
+
* `append` keeps the children already known and adds the next page of them.
|
|
3602
|
+
*/
|
|
3603
|
+
|
|
3604
|
+
function acceptLevel(tree, {
|
|
3605
|
+
parent = null,
|
|
3606
|
+
records = [],
|
|
3607
|
+
hasNext = false,
|
|
3608
|
+
cursor = null,
|
|
3609
|
+
append = false
|
|
3610
|
+
} = {}) {
|
|
3611
|
+
const key = keyOf(parent);
|
|
3612
|
+
|
|
3613
|
+
if (key === ROOT && !append) {
|
|
3614
|
+
tree.nodes.clear();
|
|
3615
|
+
tree.children.clear();
|
|
3616
|
+
tree.loading.clear(); // Open nodes stay open across a re-read: a person who opened a branch
|
|
3617
|
+
// and saved a row does not want it closed under them. They are dropped
|
|
3618
|
+
// when the node is gone, by the walk, which never reaches them.
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
for (const record of records) tree.nodes.set(String(record.id), record);
|
|
3622
|
+
|
|
3623
|
+
const before = append ? tree.children.get(key)?.ids ?? [] : [];
|
|
3624
|
+
const seen = new Set(before);
|
|
3625
|
+
const ids = [...before, ...records.map(record => String(record.id)).filter(id => !seen.has(id))];
|
|
3626
|
+
tree.children.set(key, {
|
|
3627
|
+
ids,
|
|
3628
|
+
hasNext: Boolean(hasNext),
|
|
3629
|
+
cursor: cursor ?? null
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
if (key !== ROOT) {
|
|
3633
|
+
tree.expanded.add(key);
|
|
3634
|
+
tree.loading.delete(key);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
return tree;
|
|
3638
|
+
}
|
|
3639
|
+
/** Whether a node's children are already known, so opening it needs no host. */
|
|
3640
|
+
|
|
3641
|
+
function hasLevel(tree, id) {
|
|
3642
|
+
return tree.children.has(keyOf(id));
|
|
3643
|
+
}
|
|
3644
|
+
function collapse(tree, id) {
|
|
3645
|
+
tree.expanded.delete(String(id));
|
|
3646
|
+
return tree;
|
|
3647
|
+
}
|
|
3648
|
+
function expandKnown(tree, id) {
|
|
3649
|
+
tree.expanded.add(String(id));
|
|
3650
|
+
return tree;
|
|
3651
|
+
}
|
|
3652
|
+
function markLoading(tree, id) {
|
|
3653
|
+
tree.loading.add(String(id));
|
|
3654
|
+
return tree;
|
|
3655
|
+
}
|
|
3656
|
+
/** Forget a node's children, so the next opening asks the host again. */
|
|
3657
|
+
|
|
3658
|
+
function dropLevel(tree, id) {
|
|
3659
|
+
tree.children.delete(keyOf(id));
|
|
3660
|
+
tree.expanded.delete(String(id));
|
|
3661
|
+
return tree;
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* The rows to draw: the known tree walked from the roots through every open
|
|
3665
|
+
* node. A row is the record with `__tree` beside it -- the level from 0, whether
|
|
3666
|
+
* it has children and is open, and its place among its siblings for
|
|
3667
|
+
* aria-setsize and aria-posinset. A level with more to come ends in a row of
|
|
3668
|
+
* its own, `__tree.more`, which asks for the next page.
|
|
3669
|
+
*/
|
|
3670
|
+
|
|
3671
|
+
function visibleRows(tree) {
|
|
3672
|
+
const rows = [];
|
|
3673
|
+
|
|
3674
|
+
const walk = (key, level) => {
|
|
3675
|
+
const entry = tree.children.get(key);
|
|
3676
|
+
if (!entry) return;
|
|
3677
|
+
entry.ids.forEach((id, index) => {
|
|
3678
|
+
const record = tree.nodes.get(id);
|
|
3679
|
+
if (!record) return;
|
|
3680
|
+
const expanded = tree.expanded.has(id) && tree.children.has(id);
|
|
3681
|
+
rows.push({ ...record,
|
|
3682
|
+
__tree: {
|
|
3683
|
+
level: Number.isInteger(record.level) ? record.level : level,
|
|
3684
|
+
hasChildren: record.has_children === true,
|
|
3685
|
+
expanded,
|
|
3686
|
+
loading: tree.loading.has(id),
|
|
3687
|
+
setsize: entry.ids.length,
|
|
3688
|
+
posinset: index + 1
|
|
3689
|
+
}
|
|
3690
|
+
});
|
|
3691
|
+
if (expanded) walk(id, level + 1);
|
|
3692
|
+
});
|
|
3693
|
+
|
|
3694
|
+
if (entry.hasNext) {
|
|
3695
|
+
rows.push({
|
|
3696
|
+
id: `__more:${key === ROOT ? "" : key}`,
|
|
3697
|
+
__tree: {
|
|
3698
|
+
level,
|
|
3699
|
+
more: true,
|
|
3700
|
+
parent: key === ROOT ? null : key,
|
|
3701
|
+
cursor: entry.cursor
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
|
|
3707
|
+
walk(ROOT, 0);
|
|
3708
|
+
return rows;
|
|
3709
|
+
}
|
|
3710
|
+
/** Every record the tree knows, open or not: what an action is checked against. */
|
|
3711
|
+
|
|
3712
|
+
function knownRecords(tree) {
|
|
3713
|
+
return [...tree.nodes.values()];
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
/***/ },
|
|
3717
|
+
|
|
3718
|
+
/***/ "4d9330f13af8"
|
|
3719
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
3720
|
+
|
|
3721
|
+
__webpack_require__.r(__webpack_exports__);
|
|
3722
|
+
// extracted by mini-css-extract-plugin
|
|
3723
|
+
|
|
3724
|
+
|
|
3725
|
+
/***/ },
|
|
3726
|
+
|
|
3727
|
+
/***/ "892a0da25239"
|
|
3728
|
+
(module) {
|
|
3729
|
+
|
|
3730
|
+
module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("aliases", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("aliases", 1).then(resolve, reject), { once: true })));
|
|
3731
|
+
|
|
3732
|
+
/***/ },
|
|
3733
|
+
|
|
3734
|
+
/***/ "5b7d7695b41b"
|
|
3735
|
+
(module) {
|
|
3736
|
+
|
|
3737
|
+
module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("element-manifest", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("element-manifest", 1).then(resolve, reject), { once: true })));
|
|
3738
|
+
|
|
3739
|
+
/***/ },
|
|
3740
|
+
|
|
3741
|
+
/***/ "43090d1c8307"
|
|
3742
|
+
(module) {
|
|
3743
|
+
|
|
3744
|
+
module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("page-language", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("page-language", 1).then(resolve, reject), { once: true })));
|
|
3745
|
+
|
|
3746
|
+
/***/ },
|
|
3747
|
+
|
|
3748
|
+
/***/ "deb1a4d61565"
|
|
3749
|
+
(module) {
|
|
3750
|
+
|
|
3751
|
+
module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("smart-base", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("smart-base", 1).then(resolve, reject), { once: true })));
|
|
3752
|
+
|
|
3753
|
+
/***/ },
|
|
3754
|
+
|
|
3755
|
+
/***/ "80ad38cb6908"
|
|
3756
|
+
(module) {
|
|
3757
|
+
|
|
3758
|
+
module.exports = /*#__PURE__*/JSON.parse('{"$comment":"Dated aliases of sf-view for the shim (core/js/aliases.js, standard §16 item 20).","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","aliases":[{"from":"aria-label","to":null,"kind":"attribute","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","mode":"host-attribute","section":"§3.1, §4.8","note":"stays a host attribute, read for the accessible name; no shim"},{"from":"sf-table-tree-intent","to":"sf-data-view-tree","kind":"event","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","mode":"same-detail","cancelable":false,"section":"§8, vocabulary §9","note":"the view\'s announcement of the tree\'s port intents carried a table name; the detail {intent, payload} is unchanged"},{"from":"sf-data-view","to":"sf-view","kind":"tag","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","section":"rename plan R2 (ui-control RENAME-PLAN-2026-11-03), naming decision 2026-11-02","note":"the element is sf-view; sf-data-view is a subclass defined beside it until the date"},{"from":"sf-data-view-open","to":"sf-view-open","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-deleted","to":"sf-view-deleted","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-answer","to":"sf-view-answer","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-state","to":"sf-view-state","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-tree","to":"sf-view-tree","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-files-drop","to":"sf-view-files-drop","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-saved-view-intent","to":"sf-view-saved-view-intent","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-list-open","to":"sf-view-list-open","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"}]}');
|
|
3759
|
+
|
|
3760
|
+
/***/ },
|
|
3761
|
+
|
|
3762
|
+
/***/ "7086146c00cc"
|
|
3763
|
+
(module) {
|
|
3764
|
+
|
|
3765
|
+
module.exports = /*#__PURE__*/JSON.parse('{"id":"smart.view","tag":"sf-view","version":"1.8.1","kind":"leaf","settings":{"chrome":{"type":"string","default":"toolbar pagination select settings"},"row-cap":{"type":"number","default":0,"integer":true,"min":0},"expected-columns":{"type":"number","default":4,"integer":true,"min":1},"port-owner":{"type":"string","default":""},"port-record":{"type":"string","default":""},"port-group":{"type":"string","default":""},"size":{"type":"enum","values":["1/2","1"],"default":"1"},"column-search-placeholder":{"type":"string"},"column-search-empty-text":{"type":"string"},"column-move-label":{"type":"string"},"filter-apply-text":{"type":"string"},"show-more-text":{"type":"string"},"selected-label":{"type":"string"},"total-label":{"type":"string"},"pages-label":{"type":"string"},"first-label":{"type":"string"},"previous-label":{"type":"string"},"page-label":{"type":"string"},"next-label":{"type":"string"},"last-label":{"type":"string"},"page-field-label":{"type":"string"},"action-choose-label":{"type":"string"},"actions-label":{"type":"string"},"actions-region-label":{"type":"string"},"clear-selection-label":{"type":"string"},"page-size-label":{"type":"string"},"pagination-aria-label":{"type":"string"},"preferences-key":{"type":"string","default":""},"accept-files":{"type":"boolean","default":false},"collection":{"type":"string","default":""},"current":{"type":"string","default":""},"bulk-actions":{"type":"list","of":{"id":"string","label":"string"}},"first-answer":{"type":"object","optional":true}},"values":{"state":{"type":"state"},"total":{"type":"number","nullable":true},"selected":{"type":"id","optional":true},"selection":{"type":"list"}},"events":{"open":{"carries":{"id":"id"},"cancelable":true},"deleted":{"carries":{"ids":"list"}},"answer":{"carries":{"intent":{"type":"string"},"answer":{"type":"enum","values":["applied","conflict","refused","unavailable"],"schema":{"type":"string"}},"reason":{"type":"string","optional":true},"message":{"type":"string","optional":true},"data":{"type":"object","optional":true},"sequence":{"type":"number","integer":true,"optional":true},"effects":{"type":"list","optional":true},"instance_id":{"type":"string"}},"own-verb":true},"state":{"carries":{"state":{"type":"enum","values":["loading","populated","empty","error","no-access"],"schema":{"type":"string"}},"message":{"type":"string"},"instance_id":{"type":"string"}},"own-verb":true},"tree":{"carries":{"intent":"string","payload":{"type":"object","optional":true}},"own-verb":true},"files-drop":{"carries":{"files":{"type":"list","schema":{"items":{"type":"object"},"minItems":1}}},"own-verb":true},"pagination-count":{"shared":"count-on-request@1","carries":{"component":{"type":"object"},"tagName":{"type":"string"},"template":{"type":"string"},"reason":{"type":"enum","values":["total","last-page","action-for-all"],"schema":{"type":"string"}},"current":{"type":"number","integer":true},"pageSize":{"type":"number","integer":true}}},"saved-view-intent":{"carries":{"key":{"type":"string"},"label":{"type":"string"},"revision":{"type":"string","schema":{"type":["string","integer"]},"optional":true},"roles":{"type":"object","optional":true}}},"table-action-intent":{"shared":"table-intents@1","carries":{"action_id":{"type":"string","schema":{"minLength":1}},"record_ids":{"type":"list","schema":{"items":{"type":["string","integer"]},"minItems":1}}}},"table-column-settings-change":{"shared":"table-intents@1","carries":{"reason":{"type":"string"},"columnSettings":{"type":"object"},"tableSettingsKey":{"type":"string"},"target":{"type":"enum","values":["personal","shared"],"schema":{"type":"string"},"optional":true},"base_revision":{"type":"string","schema":{"type":["string","number"]},"optional":true}}},"table-column-settings-reset":{"shared":"table-intents@1","carries":{"target":{"type":"enum","values":["personal"],"schema":{"type":"string"}},"tableSettingsKey":{"type":"string","schema":{"type":["string","null"]}}}},"table-create-intent":{"shared":"table-intents@1","carries":{"item":{"type":"string","schema":{"type":["string","null"]}}}},"table-query-intent":{"shared":"table-intents@1","carries":{"reason":{"type":"enum","values":["context"]},"context":{"type":"object"},"sequence":{"type":"number","integer":true,"schema":{"minimum":1}}}},"table-template-delete":{"shared":"table-intents@1","carries":{"key":{"type":"string","schema":{"minLength":1}}}},"table-template-save":{"shared":"table-intents@1","carries":{},"x-comment":"The detail is one template object or an array of them: a carries map cannot say that, so the payload schema is x-registry/event-fields until 1.4 has a home for a detail that is not a flat object."},"table-template-select":{"shared":"table-intents@1","carries":{"key":{"type":"string","schema":{"minLength":1}}}},"host-port-request":{"shared":"host-port@1","carries":{"component":{"type":"object","optional":true},"context":{"type":"object","optional":true}}},"list-open":{"carries":{"reason":{"type":"id","schema":{"enum":["row-cap"]}},"query":{"type":"object"}}},"table-cell-set":{"shared":"table-intents@1","carries":{"id":{"type":"string","schema":{"type":["string","integer"]}},"key":{"type":"string"},"value":{"type":"id"},"revision":{"type":"string","schema":{"type":["string","integer","null"]}}}},"table-rows-reorder":{"shared":"table-intents@1","carries":{"ids":{"type":"list","schema":{"items":{"type":"string"},"minItems":1}},"before":{"type":"string","schema":{"type":["string","null"]}},"order":{"type":"list","schema":{"items":{"type":"string"}}}}}},"actions":{"refresh":{"with":{"reason":{"type":"string","optional":true}}},"change-query":{"with":{"patch":"object","reason":{"type":"string","optional":true}},"concurrency":"latest"},"open-view":{"with":{"key":"string"}},"save-view":{"with":{"view":{"type":"object"}},"concurrency":"drop"},"delete-view":{"with":{"key":"string"},"concurrency":"drop"},"apply-under-filter":{"with":{"action-id":"string","confirmed-total":"number"},"concurrency":"drop"},"reset-column-settings":{},"clear-selection":{},"notify":{"with":{"text":"string","options":{"type":"object","optional":true}}}},"port":{"id":"simai.dataview-port","version":"1.13.0","contract":"src/core/contracts/ports/simai.dataview-port/1.13.0/port.json"},"presentation":{"skeleton":{"late":[]}},"css-variables":{"--sf-data-view-gap":{"type":"length","state":"default"},"--sf-data-view-drop-border-color":{"type":"color","state":"dragged-over"},"--sf-data-view-drop-background-color":{"type":"color","state":"dragged-over"},"--sf-data-view-drop-color":{"type":"color","state":"dragged-over"},"--sf-data-view-toolbar-gap":{"type":"length","state":"default"}},"host-states":{"data-state":"string","aria-busy":"string"}}');
|
|
3766
|
+
|
|
3767
|
+
/***/ },
|
|
3768
|
+
|
|
3769
|
+
/***/ "ce0305e57fa5"
|
|
3770
|
+
(module) {
|
|
3771
|
+
|
|
3772
|
+
module.exports = /*#__PURE__*/JSON.parse('{"title":"Data view","events":{"answer":{"label":"The host answered"},"state":{"label":"List state"},"tree":{"label":"Tree request"},"files-drop":{"label":"Files dropped on the list"}},"runtime":{"cancel":"Cancel","close-notice-label":"Close the message","confirm-apply":"Confirm","confirm-title":"Confirm the change","create-child-action":"Add a nested one","current-view-text":"Current","drop-files":"Drop the files here to upload them","empty-text":"No data","layout-label":"Data view","move-action":"Move…","move-apply":"Move","move-first":"First","move-last":"Last","move-no-targets":"There is nowhere this record can be moved.","move-place":"Place","move-root":"To the top level","move-title":"Move «{label}»","notice-action-not-allowed":"That action is not available for the chosen record.","notice-capability-not-provided":"The host does not support that action.","notice-confirm-required":"The action has to be confirmed first.","notice-conflict":"The data changed. Read the view again and repeat the action.","notice-failed":"The action was not carried out.","notice-fetch-failed":"The data could not be fetched. Try again.","notice-forbidden":"You may not do this.","notice-load-failed":"The data could not be loaded.","notice-no-base-revision":"The settings cannot be saved yet: the view has no current version of them. Read the view again.","notice-not-confirmed":"The action was cancelled.","notice-port-not-connected":"The data source is not connected.","notice-record-invalid":"The record was not found.","notice-refused":"The action was refused.","notice-saved-view-levels":"Shared views need a newer port.","notice-stale-revision":"Someone changed this in the meantime. Read the view again and repeat the action.","notice-unavailable":"The action is unavailable.","of-total":"{shown} of {total}","records-label":"Records","show-all":"Show all","state-load-failed":"The data could not be loaded","state-loading":"Loading","state-port-not-connected":"The data port is not connected","state-query-failed":"The request could not be completed","state-unavailable":"The data is temporarily unavailable","unnamed-action":"Action"}}');
|
|
3773
|
+
|
|
3774
|
+
/***/ },
|
|
3775
|
+
|
|
3776
|
+
/***/ "3e9dffe84919"
|
|
3777
|
+
(module) {
|
|
3778
|
+
|
|
3779
|
+
module.exports = /*#__PURE__*/JSON.parse('{"title":"Представление данных","events":{"answer":{"label":"Ответ хоста"},"state":{"label":"Состояние списка"},"tree":{"label":"Запрос ветви дерева"},"files-drop":{"label":"Файлы брошены на список"}},"runtime":{"cancel":"Отмена","close-notice-label":"Закрыть сообщение","confirm-apply":"Подтвердить","confirm-title":"Подтвердите изменение","create-child-action":"Добавить вложенную","current-view-text":"Текущий","drop-files":"Отпустите, чтобы загрузить файлы сюда","empty-text":"Нет данных","layout-label":"Представление данных","move-action":"Переместить…","move-apply":"Переместить","move-first":"Первым","move-last":"Последним","move-no-targets":"Эту запись некуда переместить.","move-place":"Место","move-root":"На верхний уровень","move-title":"Переместить «{label}»","notice-action-not-allowed":"Это действие недоступно для выбранной записи.","notice-capability-not-provided":"Хост не поддерживает это действие.","notice-confirm-required":"Действие нужно сначала подтвердить.","notice-conflict":"Данные изменились. Обновите представление и повторите действие.","notice-failed":"Действие не выполнено.","notice-fetch-failed":"Не удалось получить данные. Повторите попытку.","notice-forbidden":"Недостаточно прав для этого действия.","notice-load-failed":"Не удалось загрузить данные.","notice-no-base-revision":"Настройки пока нельзя сохранить: у представления нет их текущей версии. Обновите представление.","notice-not-confirmed":"Действие отменено.","notice-port-not-connected":"Источник данных не подключён.","notice-record-invalid":"Запись не найдена.","notice-refused":"Действие отклонено.","notice-saved-view-levels":"Общие виды требуют более новой версии порта.","notice-stale-revision":"Данные успели изменить. Обновите представление и повторите действие.","notice-unavailable":"Действие недоступно.","of-total":"{shown} из {total}","records-label":"Записи","show-all":"Показать все","state-load-failed":"Не удалось загрузить данные","state-loading":"Загрузка данных","state-port-not-connected":"Порт данных не подключён","state-query-failed":"Запрос не выполнен","state-unavailable":"Данные временно недоступны","unnamed-action":"Действие"}}');
|
|
3780
|
+
|
|
3781
|
+
/***/ }
|
|
3782
|
+
|
|
3783
|
+
/******/ });
|
|
3784
|
+
/************************************************************************/
|
|
3785
|
+
/******/ // The module cache
|
|
3786
|
+
/******/ const __webpack_module_cache__ = {};
|
|
3787
|
+
/******/
|
|
3788
|
+
/******/ // The require function
|
|
3789
|
+
/******/ function __webpack_require__(moduleId) {
|
|
3790
|
+
/******/ // Check if module is in cache
|
|
3791
|
+
/******/ const cachedModule = __webpack_module_cache__[moduleId];
|
|
3792
|
+
/******/ if (cachedModule !== undefined) {
|
|
3793
|
+
/******/ return cachedModule.exports;
|
|
3794
|
+
/******/ }
|
|
3795
|
+
/******/ // Create a new module (and put it into the cache)
|
|
3796
|
+
/******/ const module = __webpack_module_cache__[moduleId] = {
|
|
3797
|
+
/******/ // no module.id needed
|
|
3798
|
+
/******/ // no module.loaded needed
|
|
3799
|
+
/******/ exports: {}
|
|
3800
|
+
/******/ };
|
|
3801
|
+
/******/
|
|
3802
|
+
/******/ // Execute the module function
|
|
3803
|
+
/******/ if (!(moduleId in __webpack_modules__)) {
|
|
3804
|
+
/******/ delete __webpack_module_cache__[moduleId];
|
|
3805
|
+
/******/ const e = new Error("Cannot find module '" + moduleId + "'");
|
|
3806
|
+
/******/ e.code = 'MODULE_NOT_FOUND';
|
|
3807
|
+
/******/ throw e;
|
|
3808
|
+
/******/ }
|
|
3809
|
+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
3810
|
+
/******/
|
|
3811
|
+
/******/ // Return the exports of the module
|
|
3812
|
+
/******/ return module.exports;
|
|
3813
|
+
/******/ }
|
|
3814
|
+
/******/
|
|
3815
|
+
/************************************************************************/
|
|
3816
|
+
/******/ /* webpack/runtime/async module */
|
|
3817
|
+
/******/ (() => {
|
|
3818
|
+
/******/ const hasSymbol = typeof Symbol === "function";
|
|
3819
|
+
/******/ const webpackQueues = hasSymbol ? Symbol("webpack queues") : "__webpack_queues__";
|
|
3820
|
+
/******/ const webpackExports = hasSymbol ? Symbol("webpack exports") : "__webpack_exports__";
|
|
3821
|
+
/******/ const webpackError = hasSymbol ? Symbol("webpack error") : "__webpack_error__";
|
|
3822
|
+
/******/
|
|
3823
|
+
/******/ const resolveQueue = (queue) => {
|
|
3824
|
+
/******/ if(queue?.d < 1) {
|
|
3825
|
+
/******/ queue.d = 1;
|
|
3826
|
+
/******/ queue.forEach((fn) => (fn.r--));
|
|
3827
|
+
/******/ queue.forEach((fn) => (fn.r-- ? fn.r++ : fn()));
|
|
3828
|
+
/******/ }
|
|
3829
|
+
/******/ }
|
|
3830
|
+
/******/ const wrapDeps = (deps) => (deps.map((dep) => {
|
|
3831
|
+
/******/ if(dep !== null && typeof dep === "object") {
|
|
3832
|
+
/******/
|
|
3833
|
+
/******/ if(dep[webpackQueues]) return dep;
|
|
3834
|
+
/******/ if(dep.then) {
|
|
3835
|
+
/******/ const queue = [];
|
|
3836
|
+
/******/ queue.d = 0;
|
|
3837
|
+
/******/ dep.then((r) => {
|
|
3838
|
+
/******/ obj[webpackExports] = r;
|
|
3839
|
+
/******/ resolveQueue(queue);
|
|
3840
|
+
/******/ }, (e) => {
|
|
3841
|
+
/******/ obj[webpackError] = e;
|
|
3842
|
+
/******/ resolveQueue(queue);
|
|
3843
|
+
/******/ });
|
|
3844
|
+
/******/ const obj = {};
|
|
3845
|
+
/******/
|
|
3846
|
+
/******/ obj[webpackQueues] = (fn) => (fn(queue));
|
|
3847
|
+
/******/ return obj;
|
|
3848
|
+
/******/ }
|
|
3849
|
+
/******/ }
|
|
3850
|
+
/******/ const ret = {};
|
|
3851
|
+
/******/ ret[webpackQueues] = x => {};
|
|
3852
|
+
/******/ ret[webpackExports] = dep;
|
|
3853
|
+
/******/ return ret;
|
|
3854
|
+
/******/ }));
|
|
3855
|
+
/******/ __webpack_require__.a = (module, body, hasAwait) => {
|
|
3856
|
+
/******/ let queue;
|
|
3857
|
+
/******/ hasAwait && ((queue = []).d = -1);
|
|
3858
|
+
/******/ const depQueues = new Set();
|
|
3859
|
+
/******/ const exports = module.exports;
|
|
3860
|
+
/******/ let currentDeps;
|
|
3861
|
+
/******/ let outerResolve;
|
|
3862
|
+
/******/ let reject;
|
|
3863
|
+
/******/ const promise = new Promise((resolve, rej) => {
|
|
3864
|
+
/******/ reject = rej;
|
|
3865
|
+
/******/ outerResolve = resolve;
|
|
3866
|
+
/******/ });
|
|
3867
|
+
/******/ promise[webpackExports] = exports;
|
|
3868
|
+
/******/ promise[webpackQueues] = (fn) => (queue && fn(queue), depQueues.forEach(fn), promise["catch"](x => {}));
|
|
3869
|
+
/******/ module.exports = promise;
|
|
3870
|
+
/******/ const handle = (deps) => {
|
|
3871
|
+
/******/ currentDeps = wrapDeps(deps);
|
|
3872
|
+
/******/ let fn;
|
|
3873
|
+
/******/ const getResult = () => (currentDeps.map((d) => {
|
|
3874
|
+
/******/
|
|
3875
|
+
/******/ if(d[webpackError]) throw d[webpackError];
|
|
3876
|
+
/******/ return d[webpackExports];
|
|
3877
|
+
/******/ }))
|
|
3878
|
+
/******/ const promise = new Promise((resolve) => {
|
|
3879
|
+
/******/ fn = () => (resolve(getResult));
|
|
3880
|
+
/******/ fn.r = 0;
|
|
3881
|
+
/******/ const fnQueue = (q) => (q !== queue && !depQueues.has(q) && (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn))));
|
|
3882
|
+
/******/ currentDeps.map((dep) => (dep[webpackQueues](fnQueue)));
|
|
3883
|
+
/******/ });
|
|
3884
|
+
/******/ return fn.r ? promise : getResult();
|
|
3885
|
+
/******/ }
|
|
3886
|
+
/******/ const done = (err) => ((err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue))
|
|
3887
|
+
/******/ body(handle, done);
|
|
3888
|
+
/******/ queue?.d < 0 && (queue.d = 0);
|
|
3889
|
+
/******/ };
|
|
3890
|
+
/******/ })();
|
|
3891
|
+
/******/
|
|
3892
|
+
/******/ /* webpack/runtime/compat get default export */
|
|
3893
|
+
/******/ (() => {
|
|
3894
|
+
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
3895
|
+
/******/ __webpack_require__.n = (module) => {
|
|
3896
|
+
/******/ const getter = module && module.__esModule ?
|
|
3897
|
+
/******/ () => (module['default']) :
|
|
3898
|
+
/******/ () => (module);
|
|
3899
|
+
/******/ __webpack_require__.d(getter, { a: getter });
|
|
3900
|
+
/******/ return getter;
|
|
3901
|
+
/******/ };
|
|
3902
|
+
/******/ })();
|
|
3903
|
+
/******/
|
|
3904
|
+
/******/ /* webpack/runtime/define property getters */
|
|
3905
|
+
/******/ (() => {
|
|
3906
|
+
/******/ // define getter/value functions for harmony exports
|
|
3907
|
+
/******/ __webpack_require__.d = (exports, definition) => {
|
|
3908
|
+
/******/ if(Array.isArray(definition)) {
|
|
3909
|
+
/******/ var i = 0;
|
|
3910
|
+
/******/ while(i < definition.length) {
|
|
3911
|
+
/******/ var key = definition[i++];
|
|
3912
|
+
/******/ var binding = definition[i++];
|
|
3913
|
+
/******/ if(!__webpack_require__.o(exports, key)) {
|
|
3914
|
+
/******/ if(binding === 0) {
|
|
3915
|
+
/******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
|
|
3916
|
+
/******/ } else {
|
|
3917
|
+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
|
|
3918
|
+
/******/ }
|
|
3919
|
+
/******/ } else if(binding === 0) { i++; }
|
|
3920
|
+
/******/ }
|
|
3921
|
+
/******/ } else {
|
|
3922
|
+
/******/ for(var key in definition) {
|
|
3923
|
+
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
3924
|
+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
3925
|
+
/******/ }
|
|
3926
|
+
/******/ }
|
|
3927
|
+
/******/ }
|
|
3928
|
+
/******/ };
|
|
3929
|
+
/******/ })();
|
|
3930
|
+
/******/
|
|
3931
|
+
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
3932
|
+
/******/ (() => {
|
|
3933
|
+
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
3934
|
+
/******/ })();
|
|
3935
|
+
/******/
|
|
3936
|
+
/******/ /* webpack/runtime/make namespace object */
|
|
3937
|
+
/******/ (() => {
|
|
3938
|
+
/******/ // define __esModule on exports
|
|
3939
|
+
/******/ __webpack_require__.r = (exports) => {
|
|
3940
|
+
/******/ if(Symbol.toStringTag) {
|
|
3941
|
+
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3942
|
+
/******/ }
|
|
3943
|
+
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
3944
|
+
/******/ };
|
|
3945
|
+
/******/ })();
|
|
3946
|
+
/******/
|
|
3947
|
+
/************************************************************************/
|
|
3948
|
+
/******/
|
|
3949
|
+
/******/ // startup
|
|
3950
|
+
/******/ // Load entry module and return exports
|
|
3951
|
+
/******/ // This entry module used 'module' so it can't be inlined
|
|
3952
|
+
/******/ let __webpack_exports__ = __webpack_require__("36b027867bab");
|
|
3953
|
+
/******/
|
|
3954
|
+
/******/ })()
|
|
3955
|
+
;
|