@yuneta/gobj-ui 5.2.1 → 5.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 +165 -0
- package/dist/gobj-ui.cjs.js +385 -41
- package/dist/gobj-ui.es.js +385 -41
- package/package.json +1 -1
- package/src/c_yui_json.css +23 -3
- package/src/c_yui_json.js +17 -3
- package/src/c_yui_nav.js +63 -3
- package/src/c_yui_node.css +77 -0
- package/src/c_yui_node.js +1567 -0
- package/src/c_yui_shell.css +18 -0
- package/src/c_yui_shell.js +206 -4
- package/src/c_yui_window.js +8 -3
- package/src/node_tree_model.js +463 -0
- package/src/node_tree_model.test.js +282 -0
- package/src/route_map_model.js +48 -0
- package/src/route_map_model.test.js +70 -0
- package/src/route_resolver.js +11 -0
- package/src/shell_route_map.css +36 -2
- package/src/shell_route_map.js +194 -45
- package/src/yui_dev.js +3 -3
- package/src/yui_icons.css +13 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* node_tree_model.js
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers for C_YUI_NODE (no gobj, no DOM, no imports) —
|
|
5
|
+
* kept apart so they are trivially unit-testable, like
|
|
6
|
+
* route_resolver.js and route_map_model.js.
|
|
7
|
+
*
|
|
8
|
+
* The model: a navigable position is a NODE, nodes form a
|
|
9
|
+
* TREE, and the URL is the path of node ids from the tree's
|
|
10
|
+
* base route down. A node holds HOW IT WANTS ITS CHILDREN
|
|
11
|
+
* SEEN (the projection) — so "two levels of menu" stops being
|
|
12
|
+
* a concept: there is only a parent projecting its children,
|
|
13
|
+
* recursively, at any depth.
|
|
14
|
+
*
|
|
15
|
+
* Where the STRUCTURAL tree ends, a node declares a `link`:
|
|
16
|
+
* a pointer into a data space (a timeranger — millions of raw
|
|
17
|
+
* records, series/time, key/value) plus the viewer suited to
|
|
18
|
+
* that shape. Below a link there are no more nodes: the URL
|
|
19
|
+
* keeps going, but its tail belongs to the viewer. That is
|
|
20
|
+
* the scale boundary — one gobj per structural node is right,
|
|
21
|
+
* one gobj per meter reading is not.
|
|
22
|
+
*
|
|
23
|
+
* Validation lives HERE (normalize_spec), once, at the door;
|
|
24
|
+
* every other helper assumes it received validated data, so
|
|
25
|
+
* it never has to decide what to do with garbage in silence.
|
|
26
|
+
* Diagnostics are RETURNED (this file cannot log — it has no
|
|
27
|
+
* imports by design); C_YUI_NODE logs them.
|
|
28
|
+
*
|
|
29
|
+
* Copyright (c) 2026, ArtGins.
|
|
30
|
+
* All Rights Reserved.
|
|
31
|
+
***********************************************************************/
|
|
32
|
+
|
|
33
|
+
/* The layouts C_YUI_NAV can render a projection with. Kept in sync
|
|
34
|
+
* with SUPPORTED_LAYOUTS in c_yui_nav.js — a projection is just a nav
|
|
35
|
+
* render config, so an unknown layout is a config error here rather
|
|
36
|
+
* than a fallback surprise three levels deep in the tree. */
|
|
37
|
+
const VALID_LAYOUTS = [
|
|
38
|
+
"vertical", "icon-bar", "tabs", "drawer", "submenu", "accordion", "cards",
|
|
39
|
+
"backbar", "breadcrumb"
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/* A node with no declared projection shows its children as cards: the
|
|
43
|
+
* drill-down landing is the shape this whole model exists to serve. */
|
|
44
|
+
const DEFAULT_INDEX_RENDER = {layout: "cards"};
|
|
45
|
+
|
|
46
|
+
/************************************************************
|
|
47
|
+
* "a/b/c" → ["a","b","c"]. Empty segments are dropped, so
|
|
48
|
+
* a subpath arriving as "/a//b/" behaves like "a/b" (the
|
|
49
|
+
* shell normalizes routes, but a subpath is a tail it slices
|
|
50
|
+
* out and a caller may build one by hand).
|
|
51
|
+
************************************************************/
|
|
52
|
+
export function split_subpath(subpath)
|
|
53
|
+
{
|
|
54
|
+
if(!subpath) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return String(subpath).split("/").filter((s) => s.length > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/************************************************************
|
|
61
|
+
* First segment + the rest: {head, tail}. This is the whole
|
|
62
|
+
* of how a node decides "is this for me, or for a child?" —
|
|
63
|
+
* head names the child, tail is that child's own subpath.
|
|
64
|
+
************************************************************/
|
|
65
|
+
export function head_tail(subpath)
|
|
66
|
+
{
|
|
67
|
+
let segs = split_subpath(subpath);
|
|
68
|
+
if(segs.length === 0) {
|
|
69
|
+
return {head: "", tail: ""};
|
|
70
|
+
}
|
|
71
|
+
return {head: segs[0], tail: segs.slice(1).join("/")};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/************************************************************
|
|
75
|
+
* base_route + ids → the canonical route of a node.
|
|
76
|
+
* ("/cards", ["energy","north"]) → "/cards/energy/north"
|
|
77
|
+
************************************************************/
|
|
78
|
+
export function join_route(base_route, ids)
|
|
79
|
+
{
|
|
80
|
+
let base = String(base_route || "/");
|
|
81
|
+
if(base.charAt(0) !== "/") {
|
|
82
|
+
base = "/" + base;
|
|
83
|
+
}
|
|
84
|
+
base = base.replace(/\/{2,}/g, "/");
|
|
85
|
+
if(base.length > 1) {
|
|
86
|
+
base = base.replace(/\/+$/, "");
|
|
87
|
+
}
|
|
88
|
+
let tail = (ids || []).filter((s) => s && String(s).length > 0).join("/");
|
|
89
|
+
if(!tail) {
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
if(base === "/") {
|
|
93
|
+
return "/" + tail;
|
|
94
|
+
}
|
|
95
|
+
return base + "/" + tail;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/************************************************************
|
|
99
|
+
* The render configs a node uses to project its children in
|
|
100
|
+
* one of the two modes:
|
|
101
|
+
*
|
|
102
|
+
* "index" — I am the tip of the path: the projection IS
|
|
103
|
+
* the page (a card grid, a list…).
|
|
104
|
+
* "chrome" — a child of mine is showing: the projection is
|
|
105
|
+
* the chrome around it (a tab strip, a backbar).
|
|
106
|
+
* "path" — a child of mine is showing: the TRAIL down to
|
|
107
|
+
* where the user is, as one line (a breadcrumb).
|
|
108
|
+
* The other two project a node's CHILDREN; this one
|
|
109
|
+
* projects the path, which is why it is the answer
|
|
110
|
+
* when the stacked strips cost more vertical room
|
|
111
|
+
* than they are worth.
|
|
112
|
+
*
|
|
113
|
+
* Accepted shapes of `projection`:
|
|
114
|
+
* "cards" → index cards, no chrome
|
|
115
|
+
* {layout:"cards", …} → index only, no chrome
|
|
116
|
+
* {index:…, chrome:…} → both, each a render config
|
|
117
|
+
* or an ARRAY of them (that is
|
|
118
|
+
* how tabs>=tablet + backbar
|
|
119
|
+
* <tablet coexist)
|
|
120
|
+
*
|
|
121
|
+
* Input is assumed validated by normalize_spec().
|
|
122
|
+
************************************************************/
|
|
123
|
+
export function projection_renders(projection, mode)
|
|
124
|
+
{
|
|
125
|
+
if(projection === null || projection === undefined || projection === "") {
|
|
126
|
+
if(mode === "index") {
|
|
127
|
+
return [Object.assign({}, DEFAULT_INDEX_RENDER)];
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
if(typeof projection === "string") {
|
|
132
|
+
if(mode === "index") {
|
|
133
|
+
return [{layout: projection}];
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let has_modes = ("index" in projection) || ("chrome" in projection) ||
|
|
139
|
+
("path" in projection);
|
|
140
|
+
if(!has_modes) {
|
|
141
|
+
if(mode === "index") {
|
|
142
|
+
return [Object.assign({}, projection)];
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let raw = projection[mode];
|
|
148
|
+
if(raw === null || raw === undefined) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
if(typeof raw === "string") {
|
|
152
|
+
return [{layout: raw}];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let list = Array.isArray(raw) ? raw : [raw];
|
|
156
|
+
let out = [];
|
|
157
|
+
for(let r of list) {
|
|
158
|
+
out.push(Object.assign({}, r));
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/************************************************************
|
|
164
|
+
* Children → C_YUI_NAV `menu_items`. The nav is the renderer
|
|
165
|
+
* of every projection (cards, tabs, vertical, backbar…), so a
|
|
166
|
+
* projection is DRY with the shell's own menus: same item
|
|
167
|
+
* contract, same click event, same i18n and icon handling.
|
|
168
|
+
*
|
|
169
|
+
* `route_of(id)` returns the canonical route of a child — the
|
|
170
|
+
* node supplies it, so this file stays route-agnostic.
|
|
171
|
+
************************************************************/
|
|
172
|
+
export function child_nav_items(children, route_of)
|
|
173
|
+
{
|
|
174
|
+
let items = [];
|
|
175
|
+
for(let c of (children || [])) {
|
|
176
|
+
let item = {
|
|
177
|
+
id: c.id,
|
|
178
|
+
name: c.label || c.id,
|
|
179
|
+
route: route_of(c.id)
|
|
180
|
+
};
|
|
181
|
+
if(c.icon) {
|
|
182
|
+
item.icon = c.icon;
|
|
183
|
+
}
|
|
184
|
+
if(c.tooltip) {
|
|
185
|
+
item.tooltip = c.tooltip;
|
|
186
|
+
}
|
|
187
|
+
if(c.disabled) {
|
|
188
|
+
item.disabled = true;
|
|
189
|
+
}
|
|
190
|
+
items.push(item);
|
|
191
|
+
}
|
|
192
|
+
return items;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/************************************************************
|
|
196
|
+
* Does the ancestor at `distance` from the tip paint its chrome?
|
|
197
|
+
*
|
|
198
|
+
* `depth` is the effective `chrome_depth` of the active path: the
|
|
199
|
+
* number of chrome strips painted ABOVE the current position.
|
|
200
|
+
* `null` means unlimited — every ancestor paints, which is the
|
|
201
|
+
* recursion in its raw form and stacks one strip per level.
|
|
202
|
+
*
|
|
203
|
+
* distance 1 is the tip's own parent, so `depth: 1` leaves exactly
|
|
204
|
+
* one strip and `depth: 0` leaves none.
|
|
205
|
+
************************************************************/
|
|
206
|
+
export function chrome_visible(distance, depth)
|
|
207
|
+
{
|
|
208
|
+
if(depth === null || depth === undefined) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return distance <= depth;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/************************************************************
|
|
215
|
+
* Validate + fill one node spec. Returns the normalized spec,
|
|
216
|
+
* or null when it is unusable; every rejection pushes a
|
|
217
|
+
* human-readable line into `errors` (the caller logs them —
|
|
218
|
+
* this file has no imports). `where` is a path-ish label used
|
|
219
|
+
* to locate the offending node in the message.
|
|
220
|
+
*
|
|
221
|
+
* Recursive: children are normalized too, so ONE call at the
|
|
222
|
+
* door validates a whole declared tree.
|
|
223
|
+
************************************************************/
|
|
224
|
+
export function normalize_spec(spec, errors, where)
|
|
225
|
+
{
|
|
226
|
+
let at = where || "/";
|
|
227
|
+
|
|
228
|
+
if(!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
229
|
+
errors.push(`node spec at '${at}' is not an object`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let id = spec.id;
|
|
234
|
+
if(typeof id !== "string" || id.length === 0) {
|
|
235
|
+
errors.push(`node at '${at}' has no 'id' (the id IS the url segment)`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if(id.indexOf("/") >= 0) {
|
|
239
|
+
errors.push(`node id '${id}' at '${at}' contains '/' — an id is ONE url segment`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let out = {
|
|
244
|
+
id: id,
|
|
245
|
+
label: (typeof spec.label === "string" && spec.label) ? spec.label : id,
|
|
246
|
+
icon: (typeof spec.icon === "string") ? spec.icon : "",
|
|
247
|
+
tooltip: (typeof spec.tooltip === "string") ? spec.tooltip : "",
|
|
248
|
+
disabled: !!spec.disabled,
|
|
249
|
+
/* -1 = not declared here (the path inherits it). A node whose
|
|
250
|
+
* only job is to cap the stacked chrome of its subtree is a
|
|
251
|
+
* legitimate, and expected, intermediate node. */
|
|
252
|
+
chrome_depth: -1,
|
|
253
|
+
aliases: [],
|
|
254
|
+
projection: null,
|
|
255
|
+
content: null,
|
|
256
|
+
link: null,
|
|
257
|
+
children: []
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if(spec.chrome_depth !== undefined && spec.chrome_depth !== null) {
|
|
261
|
+
let d = spec.chrome_depth;
|
|
262
|
+
/* -1 is this function's OWN "not declared" output: each level of
|
|
263
|
+
* the tree re-normalizes the children the level above handed it,
|
|
264
|
+
* so normalize_spec has to accept what it produces. */
|
|
265
|
+
if(typeof d !== "number" || d < -1 || Math.floor(d) !== d) {
|
|
266
|
+
errors.push(
|
|
267
|
+
`node '${at}${id}': 'chrome_depth' must be an integer >= 0 ` +
|
|
268
|
+
`(0 = no chrome strip, omit it to inherit)`
|
|
269
|
+
);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
out.chrome_depth = d;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* The tree is a CONTRACT: its paths are urls a client may hold.
|
|
276
|
+
* An id therefore never changes — a rename ships the old id as an
|
|
277
|
+
* alias, and the old url keeps resolving (rewritten to the
|
|
278
|
+
* canonical one), which is what makes a version bump migratable. */
|
|
279
|
+
if(spec.aliases !== undefined && spec.aliases !== null) {
|
|
280
|
+
if(!Array.isArray(spec.aliases)) {
|
|
281
|
+
errors.push(`node '${at}${id}': 'aliases' must be an array of ids`);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
for(let a of spec.aliases) {
|
|
285
|
+
if(typeof a !== "string" || a.length === 0 || a.indexOf("/") >= 0) {
|
|
286
|
+
errors.push(
|
|
287
|
+
`node '${at}${id}': alias '${a}' is not a valid id ` +
|
|
288
|
+
`(one url segment)`
|
|
289
|
+
);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
out.aliases.push(a);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let proj_errors = validate_projection(spec.projection, `${at}${id}`);
|
|
297
|
+
if(proj_errors.length) {
|
|
298
|
+
for(let e of proj_errors) {
|
|
299
|
+
errors.push(e);
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if(spec.projection !== undefined) {
|
|
304
|
+
out.projection = spec.projection;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if(spec.content !== undefined && spec.content !== null) {
|
|
308
|
+
let c = spec.content;
|
|
309
|
+
if(typeof c !== "object" || Array.isArray(c)) {
|
|
310
|
+
errors.push(`node '${at}${id}': 'content' must be an object {gclass, kw?}`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if(typeof c.gclass !== "string" || c.gclass.length === 0) {
|
|
314
|
+
errors.push(`node '${at}${id}': 'content.gclass' is required`);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
out.content = {
|
|
318
|
+
gclass: c.gclass,
|
|
319
|
+
kw: (c.kw && typeof c.kw === "object") ? c.kw : {}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* A link is where structure stops and data begins: it mounts a
|
|
324
|
+
* viewer and OWNS everything below it in the url. It therefore
|
|
325
|
+
* cannot also be a branch (children) or carry a separate content —
|
|
326
|
+
* declaring both is a contradiction about who owns the subpath,
|
|
327
|
+
* and a silent winner would be the worst outcome. */
|
|
328
|
+
if(spec.link !== undefined && spec.link !== null) {
|
|
329
|
+
let l = spec.link;
|
|
330
|
+
if(typeof l !== "object" || Array.isArray(l)) {
|
|
331
|
+
errors.push(`node '${at}${id}': 'link' must be an object {kind, gclass, kw?}`);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
if(typeof l.kind !== "string" || l.kind.length === 0) {
|
|
335
|
+
errors.push(
|
|
336
|
+
`node '${at}${id}': 'link.kind' is required — name the data ` +
|
|
337
|
+
`space it points into (e.g. "tranger")`
|
|
338
|
+
);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if(typeof l.gclass !== "string" || l.gclass.length === 0) {
|
|
342
|
+
errors.push(
|
|
343
|
+
`node '${at}${id}': 'link.gclass' is required — the viewer ` +
|
|
344
|
+
`suited to that data`
|
|
345
|
+
);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if(out.content) {
|
|
349
|
+
errors.push(
|
|
350
|
+
`node '${at}${id}': declares both 'link' and 'content' — a ` +
|
|
351
|
+
`link IS the node's content`
|
|
352
|
+
);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
out.link = {
|
|
356
|
+
kind: l.kind,
|
|
357
|
+
gclass: l.gclass,
|
|
358
|
+
kw: (l.kw && typeof l.kw === "object") ? l.kw : {}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if(spec.children !== undefined && spec.children !== null) {
|
|
363
|
+
if(!Array.isArray(spec.children)) {
|
|
364
|
+
errors.push(`node '${at}${id}': 'children' must be an array`);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if(out.link && spec.children.length) {
|
|
368
|
+
errors.push(
|
|
369
|
+
`node '${at}${id}': declares both 'link' and 'children' — ` +
|
|
370
|
+
`below a link the url belongs to the viewer, not to nodes`
|
|
371
|
+
);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
let seen = {};
|
|
375
|
+
for(let child of spec.children) {
|
|
376
|
+
let n = normalize_spec(child, errors, `${at}${id}/`);
|
|
377
|
+
if(!n) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if(seen[n.id]) {
|
|
381
|
+
errors.push(
|
|
382
|
+
`node '${at}${id}': duplicated child id '${n.id}' — ` +
|
|
383
|
+
`sibling ids are url segments and must be unique`
|
|
384
|
+
);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
seen[n.id] = true;
|
|
388
|
+
out.children.push(n);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/************************************************************
|
|
396
|
+
* Shape check of a `projection` value. Returns an array of
|
|
397
|
+
* error lines (empty when valid).
|
|
398
|
+
************************************************************/
|
|
399
|
+
function validate_projection(projection, at)
|
|
400
|
+
{
|
|
401
|
+
let errors = [];
|
|
402
|
+
|
|
403
|
+
if(projection === undefined || projection === null) {
|
|
404
|
+
return errors;
|
|
405
|
+
}
|
|
406
|
+
if(typeof projection === "string") {
|
|
407
|
+
if(VALID_LAYOUTS.indexOf(projection) < 0) {
|
|
408
|
+
errors.push(`node '${at}': unknown projection layout '${projection}'`);
|
|
409
|
+
}
|
|
410
|
+
return errors;
|
|
411
|
+
}
|
|
412
|
+
if(typeof projection !== "object" || Array.isArray(projection)) {
|
|
413
|
+
errors.push(`node '${at}': 'projection' must be a string or an object`);
|
|
414
|
+
return errors;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let has_modes = ("index" in projection) || ("chrome" in projection) ||
|
|
418
|
+
("path" in projection);
|
|
419
|
+
if(!has_modes) {
|
|
420
|
+
check_render(projection, at, "projection", errors);
|
|
421
|
+
return errors;
|
|
422
|
+
}
|
|
423
|
+
for(let mode of ["index", "chrome"]) {
|
|
424
|
+
if(!(mode in projection)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
let raw = projection[mode];
|
|
428
|
+
if(raw === null || raw === undefined) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
let list = Array.isArray(raw) ? raw : [raw];
|
|
432
|
+
for(let r of list) {
|
|
433
|
+
if(typeof r === "string") {
|
|
434
|
+
if(VALID_LAYOUTS.indexOf(r) < 0) {
|
|
435
|
+
errors.push(`node '${at}': unknown ${mode} layout '${r}'`);
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
check_render(r, at, `projection.${mode}`, errors);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return errors;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/************************************************************
|
|
446
|
+
* One render config: {layout, icon_pos?, show_label?, show_on?}
|
|
447
|
+
************************************************************/
|
|
448
|
+
function check_render(render, at, what, errors)
|
|
449
|
+
{
|
|
450
|
+
if(!render || typeof render !== "object" || Array.isArray(render)) {
|
|
451
|
+
errors.push(`node '${at}': '${what}' must be an object {layout, …}`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if(typeof render.layout !== "string" || render.layout.length === 0) {
|
|
455
|
+
errors.push(`node '${at}': '${what}' has no 'layout'`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if(VALID_LAYOUTS.indexOf(render.layout) < 0) {
|
|
459
|
+
errors.push(`node '${at}': unknown ${what} layout '${render.layout}'`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export { VALID_LAYOUTS };
|