@tiaforge-ui/sinter 0.1.0 → 0.3.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/README.md +39 -12
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +9 -8
- package/dist/check/shadow.d.ts +3 -1
- package/dist/check/shadow.js +53 -5
- package/dist/cli.js +2 -2
- package/dist/compiler/check-unit.d.ts +26 -2
- package/dist/compiler/compile.d.ts +4 -0
- package/dist/compiler/compile.js +314 -30
- package/dist/compiler/diagnostic-docs.js +20 -5
- package/dist/compiler/diagnostics.d.ts +3 -0
- package/dist/compiler/diagnostics.js +3 -0
- package/dist/compiler/expr.d.ts +24 -0
- package/dist/compiler/expr.js +53 -8
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +70 -16
- package/dist/compiler/vocabulary.d.ts +1 -0
- package/dist/compiler/vocabulary.js +9 -3
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/ir.d.ts +30 -1
- package/dist/listbox.d.ts +3 -0
- package/dist/listbox.js +66 -0
- package/dist/lsp/typescript.js +1 -1
- package/dist/lsp/workspace.js +3 -2
- package/dist/paginate.js +18 -4
- package/dist/project.d.ts +6 -0
- package/dist/project.js +22 -19
- package/dist/runtime/app.d.ts +35 -1
- package/dist/runtime/app.js +178 -40
- package/dist/runtime/config.d.ts +10 -3
- package/dist/runtime/config.js +10 -3
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/request.js +6 -1
- package/dist/runtime/store.d.ts +7 -4
- package/dist/runtime/store.js +154 -41
- package/dist/sse.js +8 -5
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -2
- package/dist/vite.js +4 -2
- package/docs/diagnostics.md +35 -5
- package/html-custom-data.json +46 -2
- package/llms.txt +112 -6
- package/package.json +6 -1
package/dist/ir.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export interface Paginate {
|
|
|
8
8
|
/** keyset/append: object literal evaluated with the boundary item as `$.$p` */
|
|
9
9
|
cursor: number | null;
|
|
10
10
|
directionParam: string;
|
|
11
|
+
/** keyset/append: the value sent for each direction, where it differs from the direction's name */
|
|
12
|
+
directionValues?: Partial<Record<'first' | 'prev' | 'next' | 'last', string>>;
|
|
11
13
|
pageParam: string;
|
|
14
|
+
/** keyset: offset parameter that lets `page(name, n)` jump; the server skips (n - 1) * size items */
|
|
15
|
+
offsetParam?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface ResourceDef {
|
|
14
18
|
name: string;
|
|
@@ -29,6 +33,14 @@ export interface ResourceDef {
|
|
|
29
33
|
data: number | null;
|
|
30
34
|
/** `cache`: the oldest cached data to show, in ms (0: any age); absent when not cached */
|
|
31
35
|
cache?: number;
|
|
36
|
+
/** `call="ns.fn(args)"`: loaded from an action function instead of `src` (then empty) */
|
|
37
|
+
call?: CallDef;
|
|
38
|
+
/** `each="a in accounts"`: one request per list item (src evaluated with `item` in scope), merged into one collection */
|
|
39
|
+
each?: {
|
|
40
|
+
item: string;
|
|
41
|
+
list: number;
|
|
42
|
+
of?: string;
|
|
43
|
+
};
|
|
32
44
|
}
|
|
33
45
|
export interface EventDef {
|
|
34
46
|
name: string;
|
|
@@ -42,6 +54,8 @@ export interface EventDef {
|
|
|
42
54
|
export interface SyncDef {
|
|
43
55
|
src: UrlTemplate;
|
|
44
56
|
events: EventDef[];
|
|
57
|
+
/** index of the node (in `roots`) whose scope `src` and the events' expressions are evaluated in, -1 (or absent) for page scope */
|
|
58
|
+
scope?: number;
|
|
45
59
|
}
|
|
46
60
|
export type BindingKind = 'text' | 'attr' | 'class' | 'style' | 'show' | 'model' | 'prop' | 'for' | 'if' | 'else' | 'on';
|
|
47
61
|
export interface Binding {
|
|
@@ -66,6 +80,8 @@ export interface Binding {
|
|
|
66
80
|
e?: 1;
|
|
67
81
|
/** the expression reads a loop index, so it re-runs when only the row's position changed */
|
|
68
82
|
i?: 1;
|
|
83
|
+
/** handler on an element without a method: `data-sn-debounce` milliseconds */
|
|
84
|
+
ms?: number;
|
|
69
85
|
}
|
|
70
86
|
export interface NodeDef {
|
|
71
87
|
/** `data-sn` id (roots) */
|
|
@@ -80,6 +96,8 @@ export interface NodeDef {
|
|
|
80
96
|
sid?: number;
|
|
81
97
|
/** persist key for the whole state object */
|
|
82
98
|
persist?: string;
|
|
99
|
+
/** the element is a `<dialog>` or inside one of its block, so its bindings wait while that dialog is closed */
|
|
100
|
+
dl?: 1;
|
|
83
101
|
b: Binding[];
|
|
84
102
|
}
|
|
85
103
|
export interface TemplateDef {
|
|
@@ -95,9 +113,16 @@ export interface TemplateDef {
|
|
|
95
113
|
/** for: function that finds the bound elements of one cloned row: (first content node) => elements, in `nodes` order */
|
|
96
114
|
find?: number;
|
|
97
115
|
}
|
|
116
|
+
/** a local action call: `args` evaluates the arguments (an array), `run(args, ctx)` calls the function */
|
|
117
|
+
export interface CallDef {
|
|
118
|
+
args: number;
|
|
119
|
+
run: number;
|
|
120
|
+
}
|
|
98
121
|
export interface ActionDef {
|
|
99
|
-
|
|
122
|
+
/** `CALL`: `data-sn-call`, with `call` set and an empty `url` */
|
|
123
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'CALL';
|
|
100
124
|
url: UrlTemplate;
|
|
125
|
+
call?: CallDef;
|
|
101
126
|
body: number | null;
|
|
102
127
|
trigger: string;
|
|
103
128
|
debounce: number;
|
|
@@ -109,6 +134,10 @@ export interface ActionDef {
|
|
|
109
134
|
w: string[];
|
|
110
135
|
/** element is a form (serialize, disable in block mode) */
|
|
111
136
|
form: boolean;
|
|
137
|
+
/** `data-sn-confirm`: the question asked before the action runs (none when it is falsy) */
|
|
138
|
+
confirm?: number;
|
|
139
|
+
/** `data-sn-encoding="form"`: the body goes as application/x-www-form-urlencoded */
|
|
140
|
+
enc?: 'form';
|
|
112
141
|
}
|
|
113
142
|
export interface IR {
|
|
114
143
|
v: 1;
|
package/dist/listbox.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** the input's list: `aria-controls`, else the nearest listbox around it */
|
|
2
|
+
const listOf = (input) => {
|
|
3
|
+
const id = input.getAttribute('aria-controls');
|
|
4
|
+
if (id)
|
|
5
|
+
return input.ownerDocument.getElementById(id);
|
|
6
|
+
for (let p = input.parentElement; p; p = p.parentElement) {
|
|
7
|
+
const list = p.querySelector('[role="listbox"]');
|
|
8
|
+
if (list)
|
|
9
|
+
return list;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
const options = (input) => {
|
|
14
|
+
const list = listOf(input);
|
|
15
|
+
if (!list || list.closest('[hidden]'))
|
|
16
|
+
return [];
|
|
17
|
+
return [...list.querySelectorAll('[role="option"]')].filter((o) => !o.closest('[hidden]'));
|
|
18
|
+
};
|
|
19
|
+
const mark = (all, next) => {
|
|
20
|
+
for (const o of all)
|
|
21
|
+
o.setAttribute('aria-selected', String(o == next));
|
|
22
|
+
next?.scrollIntoView?.({ block: 'nearest' });
|
|
23
|
+
};
|
|
24
|
+
const listbox = {
|
|
25
|
+
name: 'listbox',
|
|
26
|
+
install(app) {
|
|
27
|
+
const root = app.root;
|
|
28
|
+
root.addEventListener('keydown', (e) => {
|
|
29
|
+
const input = e.target.closest?.('[role="combobox"]');
|
|
30
|
+
if (!input || e.isComposing)
|
|
31
|
+
return;
|
|
32
|
+
const all = options(input);
|
|
33
|
+
if (!all.length)
|
|
34
|
+
return;
|
|
35
|
+
const at = all.findIndex((o) => o.getAttribute('aria-selected') == 'true');
|
|
36
|
+
const key = e.key;
|
|
37
|
+
if (key == 'ArrowDown' || key == 'ArrowUp') {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
mark(all, key == 'ArrowDown' ? all[Math.min(at + 1, all.length - 1)] : at > 0 ? all[at - 1] : null);
|
|
40
|
+
}
|
|
41
|
+
else if ((key == 'Enter' && at >= 0) || (key == 'Tab' && (at >= 0 || all.length == 1))) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
mark(all, null);
|
|
44
|
+
all[Math.max(at, 0)].click();
|
|
45
|
+
}
|
|
46
|
+
else if (key == 'Escape') {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
mark(all, null);
|
|
49
|
+
input.dispatchEvent(new CustomEvent('dismiss', { bubbles: true }));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
root.addEventListener('input', (e) => {
|
|
53
|
+
const input = e.target.closest?.('[role="combobox"]');
|
|
54
|
+
if (input)
|
|
55
|
+
mark(options(input), null);
|
|
56
|
+
});
|
|
57
|
+
root.addEventListener('mousedown', (e) => {
|
|
58
|
+
// only in lists that belong to a combobox: keep the focus in the input
|
|
59
|
+
const option = e.target.closest?.('[role="option"]');
|
|
60
|
+
const list = option?.closest('[role="listbox"]');
|
|
61
|
+
if (list && [...root.querySelectorAll('[role="combobox"]')].some((c) => listOf(c) == list))
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export default listbox;
|
package/dist/lsp/typescript.js
CHANGED
|
@@ -59,7 +59,7 @@ export class TypeScriptBridge {
|
|
|
59
59
|
this.filesChanged([support.declarations, ...support.openapi]);
|
|
60
60
|
this.support = support;
|
|
61
61
|
for (const [page, unit] of units) {
|
|
62
|
-
const s = shadowOf(unit, support, this.root, project.helpers);
|
|
62
|
+
const s = shadowOf(unit, support, this.root, project.helpers, project.actions);
|
|
63
63
|
const prev = this.shadows.get(page);
|
|
64
64
|
if (prev?.text === s.text) {
|
|
65
65
|
prev.unit = unit;
|
package/dist/lsp/workspace.js
CHANGED
|
@@ -48,13 +48,14 @@ export class Workspace {
|
|
|
48
48
|
}
|
|
49
49
|
const p = project(root, options);
|
|
50
50
|
const inputs = new Set(VITE_CONFIGS.map((c) => join(root, c)));
|
|
51
|
-
for (const f of [p.helpersFile, p.types, ...p.openapi.map((s) => s.file)])
|
|
51
|
+
for (const f of [p.helpersFile, p.actionsFile, p.types, ...p.openapi.map((s) => s.file)])
|
|
52
52
|
if (f)
|
|
53
53
|
inputs.add(f);
|
|
54
54
|
for (const spec of options.openapi == null ? [] : [].concat(options.openapi))
|
|
55
55
|
inputs.add(resolve(root, spec));
|
|
56
56
|
for (const ext of ['.js', '.ts']) {
|
|
57
57
|
inputs.add(join(root, 'src/sinter.helpers' + ext));
|
|
58
|
+
inputs.add(join(root, 'src/sinter.actions' + ext));
|
|
58
59
|
inputs.add(join(root, 'src/sinter.types' + ext));
|
|
59
60
|
}
|
|
60
61
|
inputs.add(join(root, 'src/sinter.types.d.ts'));
|
|
@@ -166,7 +167,7 @@ export class Workspace {
|
|
|
166
167
|
}
|
|
167
168
|
try {
|
|
168
169
|
const out = compile(html, {
|
|
169
|
-
file: page, root: state.root, dev: false, helpers: p.helpers, config: p.config, readFile, check: true, openapi: p.openapi,
|
|
170
|
+
file: page, root: state.root, dev: false, helpers: p.helpers, actions: p.actions, config: p.config, readFile, check: true, openapi: p.openapi,
|
|
170
171
|
});
|
|
171
172
|
state.includes.set(page, out.files.map((f) => resolve(f)).filter((f) => f !== page));
|
|
172
173
|
state.diagnostics.set(page, out.diagnostics);
|
package/dist/paginate.js
CHANGED
|
@@ -7,14 +7,14 @@ class PagerImpl {
|
|
|
7
7
|
this.cfg = loader.def.paginate;
|
|
8
8
|
this.move = this.first();
|
|
9
9
|
const m = loader.res.meta;
|
|
10
|
-
if (this.cfg.mode === 'offset')
|
|
10
|
+
if (this.cfg.mode === 'offset' || this.cfg.offsetParam)
|
|
11
11
|
m.page = 1;
|
|
12
12
|
Object.assign(m, { pages: null, from: null, to: null, hasPrev: false, hasNext: false });
|
|
13
13
|
}
|
|
14
14
|
base(dir, size = this.cfg.size) {
|
|
15
15
|
const p = { [this.cfg.sizeParam]: String(size) };
|
|
16
16
|
if (dir)
|
|
17
|
-
p[this.cfg.directionParam] = dir;
|
|
17
|
+
p[this.cfg.directionParam] = this.cfg.directionValues?.[dir] ?? dir;
|
|
18
18
|
return p;
|
|
19
19
|
}
|
|
20
20
|
first() {
|
|
@@ -49,8 +49,10 @@ class PagerImpl {
|
|
|
49
49
|
scope.$p = item;
|
|
50
50
|
const c = this.loader.app.fns[this.cfg.cursor](scope);
|
|
51
51
|
const out = {};
|
|
52
|
+
// like a URL template's query parameters: null and undefined fields are left out
|
|
52
53
|
for (const [k, v] of Object.entries(c ?? {}))
|
|
53
|
-
|
|
54
|
+
if (v != null)
|
|
55
|
+
out[k] = String(v);
|
|
54
56
|
return out;
|
|
55
57
|
};
|
|
56
58
|
let move;
|
|
@@ -80,7 +82,16 @@ class PagerImpl {
|
|
|
80
82
|
}
|
|
81
83
|
else {
|
|
82
84
|
if (typeof target === 'number') {
|
|
83
|
-
this.
|
|
85
|
+
if (!this.cfg.offsetParam) {
|
|
86
|
+
this.loader.app.warn('keyset pagination cannot jump to a page number; add offset-param or use paginate="offset"');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// the server skips (n - 1) pages; prev/next continue by cursor from there
|
|
90
|
+
let n = Math.max(1, Math.floor(target));
|
|
91
|
+
if (meta.pages != null)
|
|
92
|
+
n = Math.min(n, meta.pages);
|
|
93
|
+
move = { dir: 'page', page: n, params: { [this.cfg.offsetParam]: String((n - 1) * this.cfg.size), ...this.base('first') }, from, to };
|
|
94
|
+
void this.loader.load(move);
|
|
84
95
|
return;
|
|
85
96
|
}
|
|
86
97
|
if (this.cfg.mode === 'append' && target !== 'next')
|
|
@@ -158,6 +169,9 @@ class PagerImpl {
|
|
|
158
169
|
hasPrev = full;
|
|
159
170
|
break;
|
|
160
171
|
}
|
|
172
|
+
// keyset with jumps: the page that holds the last item shown (after `last`, the last page)
|
|
173
|
+
if (this.cfg.offsetParam && move.dir !== 'page')
|
|
174
|
+
meta.page = to != null ? Math.max(1, Math.ceil(to / size)) : null;
|
|
161
175
|
Object.assign(meta, { pages, from, to, hasPrev, hasNext });
|
|
162
176
|
this.loader.res.onDirty('meta');
|
|
163
177
|
}
|
package/dist/project.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { OpenApiSpec } from './compiler/openapi.js';
|
|
|
3
3
|
export interface ProjectOptions {
|
|
4
4
|
/** helpers module, relative to the root (default `src/sinter.helpers.js`, or `.ts`) */
|
|
5
5
|
helpers?: string;
|
|
6
|
+
/** actions module for `data-sn-call` and `<sn-resource call>` (default `src/sinter.actions.js`, or `.ts`) */
|
|
7
|
+
actions?: string;
|
|
6
8
|
/** app configuration module imported before mount (default `src/sinter.config.js`, or `.ts`) */
|
|
7
9
|
config?: string;
|
|
8
10
|
/** module whose exported types `type="…"` on `<sn-resource>` may name (default `src/sinter.types.ts`, `.d.ts` or `.js`) */
|
|
@@ -15,6 +17,10 @@ export interface Project {
|
|
|
15
17
|
helpers: HelperManifest;
|
|
16
18
|
/** absolute path of the helpers module, or null */
|
|
17
19
|
helpersFile: string | null;
|
|
20
|
+
/** the actions module's functions and namespaces (same shape as helpers) */
|
|
21
|
+
actions: HelperManifest;
|
|
22
|
+
/** absolute path of the actions module, or null */
|
|
23
|
+
actionsFile: string | null;
|
|
18
24
|
/** root-relative URL of the config module (`/src/sinter.config.js`), or null */
|
|
19
25
|
config: string | null;
|
|
20
26
|
/** absolute path of the types module, or null */
|
package/dist/project.js
CHANGED
|
@@ -20,25 +20,28 @@ export function project(root, options = {}) {
|
|
|
20
20
|
return readFileSync(real, 'utf8');
|
|
21
21
|
};
|
|
22
22
|
const url = (rel) => '/' + rel.split(sep).join('/');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const manifest = (given, base) => {
|
|
24
|
+
const rel = find(given, base);
|
|
25
|
+
const file = rel ? resolve(root, rel) : null;
|
|
26
|
+
if (!rel || !file)
|
|
27
|
+
return [{ module: null, names: [], namespaces: {} }, null];
|
|
28
|
+
return [readHelperManifest(readFileSync(file, 'utf8'), {
|
|
29
|
+
module: url(rel),
|
|
30
|
+
file,
|
|
31
|
+
readFile,
|
|
32
|
+
resolve: (spec, from) => {
|
|
33
|
+
if (!spec.startsWith('.'))
|
|
34
|
+
return null;
|
|
35
|
+
const b = resolve(dirname(from), spec);
|
|
36
|
+
for (const q of [b, b.replace(/\.js$/, '.ts'), `${b}.js`, `${b}.ts`])
|
|
37
|
+
if (existsSync(q))
|
|
38
|
+
return q;
|
|
33
39
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
40
|
+
},
|
|
41
|
+
}), file];
|
|
42
|
+
};
|
|
43
|
+
const [helpers, helpersFile] = manifest(options.helpers, 'src/sinter.helpers');
|
|
44
|
+
const [actions, actionsFile] = manifest(options.actions, 'src/sinter.actions');
|
|
42
45
|
const configRel = find(options.config, 'src/sinter.config');
|
|
43
46
|
const typesRel = find(options.types, 'src/sinter.types', ['.ts', '.d.ts', '.js']);
|
|
44
47
|
const openapi = [];
|
|
@@ -70,7 +73,7 @@ export function project(root, options = {}) {
|
|
|
70
73
|
openapi.push({ file, doc });
|
|
71
74
|
}
|
|
72
75
|
return {
|
|
73
|
-
root, helpers, helpersFile, readFile, openapi, diagnostics,
|
|
76
|
+
root, helpers, helpersFile, actions, actionsFile, readFile, openapi, diagnostics,
|
|
74
77
|
config: configRel ? url(configRel) : null,
|
|
75
78
|
types: typesRel ? resolve(root, typesRel) : null,
|
|
76
79
|
};
|
package/dist/runtime/app.d.ts
CHANGED
|
@@ -47,18 +47,42 @@ export interface Pager {
|
|
|
47
47
|
appends(move: unknown): boolean;
|
|
48
48
|
received(items: unknown[], total: number | null, move: unknown): void;
|
|
49
49
|
}
|
|
50
|
+
/** The last argument of an action function (`data-sn-call`, `<sn-resource call>`). */
|
|
51
|
+
export interface CallContext {
|
|
52
|
+
/** aborts when the page is hidden (pagehide) or, for a resource, when its inputs change */
|
|
53
|
+
signal: AbortSignal;
|
|
54
|
+
/** a request through the app's middleware chain (401 handling, origins, timeout) */
|
|
55
|
+
request(url: string, init?: SendInit): Promise<unknown>;
|
|
56
|
+
/** the element whose action this is; null for a resource */
|
|
57
|
+
el: Element | null;
|
|
58
|
+
}
|
|
50
59
|
export interface ResourceLoader {
|
|
51
60
|
def: ResourceDef;
|
|
52
61
|
res: Resource;
|
|
53
62
|
app: App;
|
|
54
63
|
pager: Pager | null;
|
|
64
|
+
/** first load (or embedded/cached data) */
|
|
65
|
+
start(): void;
|
|
66
|
+
/** `src`, `when` (or an `each` list) may have changed */
|
|
67
|
+
changed(): void;
|
|
55
68
|
load(move?: unknown): Promise<void>;
|
|
56
|
-
refetch(): Promise<void>;
|
|
69
|
+
refetch(hint?: object): Promise<void>;
|
|
70
|
+
/** for feature modules: the scope `src` and `when` are evaluated in */
|
|
71
|
+
scope(): any;
|
|
72
|
+
/** for feature modules: a shared GET (concurrent loads of one URL share one request) */
|
|
73
|
+
get(url: string, signal: AbortSignal): Promise<unknown>;
|
|
74
|
+
/** for feature modules: the collection items of a response body (`items` path, checked, cleaned) */
|
|
75
|
+
items(body: unknown): any[];
|
|
76
|
+
/** for feature modules: a failed load: `$meta.error`, then the unhandled-error path */
|
|
77
|
+
failed(e: unknown): void;
|
|
78
|
+
loading(on: boolean): void;
|
|
57
79
|
}
|
|
58
80
|
export interface App {
|
|
59
81
|
readonly ir: IR;
|
|
60
82
|
readonly fns: Fn[];
|
|
61
83
|
readonly env: Env;
|
|
84
|
+
/** the element or document this instance is mounted on */
|
|
85
|
+
readonly root: Document | Element;
|
|
62
86
|
readonly req: RequestLayer;
|
|
63
87
|
/** the base scope: resources, status, magic names */
|
|
64
88
|
readonly page: any;
|
|
@@ -67,6 +91,9 @@ export interface App {
|
|
|
67
91
|
optimistic?: {
|
|
68
92
|
begin(fn: number, scope: any): () => void;
|
|
69
93
|
};
|
|
94
|
+
each?: {
|
|
95
|
+
fanOut(loader: ResourceLoader): void;
|
|
96
|
+
};
|
|
70
97
|
paginate?: {
|
|
71
98
|
pager(loader: ResourceLoader): Pager;
|
|
72
99
|
page(name: string, target: string | number): void;
|
|
@@ -108,6 +135,8 @@ export interface App {
|
|
|
108
135
|
state<T = any>(el: Element): StateHandle<T>;
|
|
109
136
|
destroy(): void;
|
|
110
137
|
url(tpl: UrlTemplate, scope: any): string;
|
|
138
|
+
/** the scope of root node `i` (its state), or the page scope for -1 */
|
|
139
|
+
scopeOf(i: number): any;
|
|
111
140
|
mark(path: string, scope?: any): void;
|
|
112
141
|
flush(): void;
|
|
113
142
|
record(change: () => void): () => void;
|
|
@@ -122,4 +151,9 @@ export declare function withParams(url: string, params: Record<string, string> |
|
|
|
122
151
|
/** Indexes (into `seq`) of a longest increasing subsequence, ignoring -1 entries. */
|
|
123
152
|
export declare function lis(seq: number[]): Set<number>;
|
|
124
153
|
/** Serializes a form's named controls to nested JSON, or FormData if it has a file input. */
|
|
154
|
+
/**
|
|
155
|
+
* A body as form fields (`data-sn-encoding="form"`): a form's own fields, or an object's keys with
|
|
156
|
+
* arrays as repeated keys; null and undefined values are left out. Files stay multipart.
|
|
157
|
+
*/
|
|
158
|
+
export declare function formBody(body: unknown): URLSearchParams | FormData | null;
|
|
125
159
|
export declare function serialize(form: HTMLFormElement): unknown;
|