@yuneta/gobj-ui 5.2.0 → 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 +386 -43
- package/dist/gobj-ui.es.js +520 -177
- 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_treedb_schema.js +1 -0
- 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,1567 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* c_yui_node.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_NODE — a navigable position, and the whole navigation
|
|
5
|
+
* model in one recursive gclass.
|
|
6
|
+
*
|
|
7
|
+
* THE MODEL
|
|
8
|
+
* ---------
|
|
9
|
+
* A node is a gobj. Nodes form a TREE (the gobj tree IS the
|
|
10
|
+
* navigation tree — no parallel structure), and the URL is the
|
|
11
|
+
* path of node ids from the tree's `base_route` down:
|
|
12
|
+
*
|
|
13
|
+
* #/cards/energy/north/m1
|
|
14
|
+
* \____/\____________/
|
|
15
|
+
* base node ids
|
|
16
|
+
*
|
|
17
|
+
* Every node holds HOW IT WANTS ITS CHILDREN SEEN — the
|
|
18
|
+
* `projection` (cards, tabs, vertical, backbar…, per breakpoint),
|
|
19
|
+
* and how it wants the WAY IN shown: `projection.path` draws the
|
|
20
|
+
* trail down to the user as one breadcrumb line, the alternative
|
|
21
|
+
* to stacking one chrome strip per level.
|
|
22
|
+
* That is the piece that removes "two levels of menu" as a
|
|
23
|
+
* concept: there is no primary/secondary nav, there is a parent
|
|
24
|
+
* projecting its children, recursively, at any depth. Rendering
|
|
25
|
+
* is DRY: a projection IS a C_YUI_NAV render config, so every
|
|
26
|
+
* level reuses the same renderer, the same item contract, the
|
|
27
|
+
* same i18n and the same click event as the shell's own menus.
|
|
28
|
+
*
|
|
29
|
+
* A node may have CONTENT (a gclass to mount), CHILDREN, or both:
|
|
30
|
+
* a section with its own page and sub-pages is one node with
|
|
31
|
+
* both, not two concepts.
|
|
32
|
+
*
|
|
33
|
+
* DECLARATIVE == DYNAMIC
|
|
34
|
+
* ----------------------
|
|
35
|
+
* The runtime API is the contract; the declared `children` attr
|
|
36
|
+
* is only its first caller. mt_create does not build the tree
|
|
37
|
+
* by a private path — it sends itself one EV_ADD_NODE per
|
|
38
|
+
* declared child, exactly what yui_node_add() does at 3pm with
|
|
39
|
+
* the app running. If the boot path could build something the
|
|
40
|
+
* API cannot, that would be an API bug, not a difference between
|
|
41
|
+
* "config" and "runtime".
|
|
42
|
+
*
|
|
43
|
+
* THE ROOT
|
|
44
|
+
* --------
|
|
45
|
+
* The root node is where the shell's own root used to be: its
|
|
46
|
+
* children are the app's primary options, and it projects them
|
|
47
|
+
* into ZONES of the space (a left rail, a bottom bar) instead of
|
|
48
|
+
* into its own body — a render config with a `zone` is mounted
|
|
49
|
+
* through yui_shell_zone(). That is the only asymmetry in the
|
|
50
|
+
* model, and it is not an exception to it: `menu.primary.render`
|
|
51
|
+
* always was a per-zone projection, it just had no owner that
|
|
52
|
+
* could hold it. Declared as `config.shell.tree`, which
|
|
53
|
+
* synthesizes ONE route entry owning the whole url space.
|
|
54
|
+
*
|
|
55
|
+
* WHERE THE TREE ENDS
|
|
56
|
+
* -------------------
|
|
57
|
+
* One gobj per structural node is right; one gobj per meter
|
|
58
|
+
* reading is not. A node marks the boundary with `link`: a
|
|
59
|
+
* pointer into a data space (a timeranger — millions of raw
|
|
60
|
+
* records, series/time, key/value) plus the viewer suited to that
|
|
61
|
+
* shape. A link node is ALWAYS the tip of the structural path:
|
|
62
|
+
* the url keeps going, but the tail is handed to the viewer as
|
|
63
|
+
* `EV_ROUTE_CHANGED {base, subpath}` — the SAME contract the shell
|
|
64
|
+
* gives a view (ROUTING.md §5), so a viewer cannot tell whether it
|
|
65
|
+
* was mounted by the shell at a declared route or by a node deep
|
|
66
|
+
* in a tree. Below a link there are no nodes, and declaring
|
|
67
|
+
* `children` there is a config error.
|
|
68
|
+
*
|
|
69
|
+
* ROUTING (see ROUTING.md)
|
|
70
|
+
* ------------------------
|
|
71
|
+
* The tree owns everything below `base_route` through the
|
|
72
|
+
* shell's `subpath`: ONE declared route, arbitrary depth. Clicks
|
|
73
|
+
* never mutate the view — a projection click publishes
|
|
74
|
+
* EV_NAV_CLICKED, the node turns it into a URL change (push), and
|
|
75
|
+
* the shell's EV_ROUTE_CHANGED walks back down the tree as
|
|
76
|
+
* EV_ACTIVATE. Intent → URL → view, so Back/Forward, F5 and deep
|
|
77
|
+
* links are correct by construction.
|
|
78
|
+
*
|
|
79
|
+
* Only the ROOT node (the one whose parent is not a C_YUI_NODE)
|
|
80
|
+
* talks to the shell: it subscribes to EV_ROUTE_CHANGED, resolves
|
|
81
|
+
* the subpath and activates the branch. Inner nodes only ever
|
|
82
|
+
* hear their parent.
|
|
83
|
+
*
|
|
84
|
+
* A path segment that names no living child is not silently
|
|
85
|
+
* swallowed: it logs and the URL is rewritten (replace) to the
|
|
86
|
+
* deepest living ancestor — the tree is dynamic, so the ground
|
|
87
|
+
* CAN disappear under a bookmark.
|
|
88
|
+
*
|
|
89
|
+
* Copyright (c) 2026, ArtGins.
|
|
90
|
+
* All Rights Reserved.
|
|
91
|
+
***********************************************************************/
|
|
92
|
+
import {
|
|
93
|
+
SDATA,
|
|
94
|
+
SDATA_END,
|
|
95
|
+
data_type_t,
|
|
96
|
+
event_flag_t,
|
|
97
|
+
gclass_create,
|
|
98
|
+
gclass_find_by_name,
|
|
99
|
+
log_error,
|
|
100
|
+
log_warning,
|
|
101
|
+
gobj_create_pure_child,
|
|
102
|
+
gobj_destroy,
|
|
103
|
+
gobj_start,
|
|
104
|
+
gobj_stop,
|
|
105
|
+
gobj_is_running,
|
|
106
|
+
gobj_parent,
|
|
107
|
+
gobj_gclass_name,
|
|
108
|
+
gobj_read_attr,
|
|
109
|
+
gobj_read_pointer_attr,
|
|
110
|
+
gobj_write_attr,
|
|
111
|
+
gobj_subscribe_event,
|
|
112
|
+
gobj_unsubscribe_event,
|
|
113
|
+
gobj_send_event,
|
|
114
|
+
gobj_change_state,
|
|
115
|
+
gobj_current_state,
|
|
116
|
+
createElement2,
|
|
117
|
+
empty_string,
|
|
118
|
+
is_array,
|
|
119
|
+
} from "@yuneta/gobj-js";
|
|
120
|
+
|
|
121
|
+
import {
|
|
122
|
+
split_subpath,
|
|
123
|
+
head_tail,
|
|
124
|
+
join_route,
|
|
125
|
+
projection_renders,
|
|
126
|
+
child_nav_items,
|
|
127
|
+
chrome_visible,
|
|
128
|
+
normalize_spec,
|
|
129
|
+
} from "./node_tree_model.js";
|
|
130
|
+
|
|
131
|
+
import {
|
|
132
|
+
yui_shell_of,
|
|
133
|
+
yui_shell_zone,
|
|
134
|
+
yui_shell_navigate,
|
|
135
|
+
yui_shell_set_sub_routes,
|
|
136
|
+
} from "./c_yui_shell.js";
|
|
137
|
+
|
|
138
|
+
import "./c_yui_node.css";
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
/***************************************************************
|
|
142
|
+
* Constants
|
|
143
|
+
***************************************************************/
|
|
144
|
+
const GCLASS_NAME = "C_YUI_NODE";
|
|
145
|
+
|
|
146
|
+
/* Internal children are named apart from node children, whose gobj
|
|
147
|
+
* name IS their node id (so the `machine` trace reads like the URL). */
|
|
148
|
+
const CONTENT_NAME = "__content__";
|
|
149
|
+
const NAV_PREFIX = "__nav_";
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/***************************************************************
|
|
153
|
+
* Attrs
|
|
154
|
+
***************************************************************/
|
|
155
|
+
const attrs_table = [
|
|
156
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
157
|
+
|
|
158
|
+
SDATA(data_type_t.DTP_STRING, "node_id", 0, "", "Id of this node — it IS its url segment (immutable)"),
|
|
159
|
+
SDATA(data_type_t.DTP_STRING, "label", 0, "", "Human label (i18n key); falls back to node_id"),
|
|
160
|
+
SDATA(data_type_t.DTP_STRING, "icon", 0, "", "Icon css class"),
|
|
161
|
+
SDATA(data_type_t.DTP_STRING, "tooltip", 0, "", "Tooltip/aria label (i18n key)"),
|
|
162
|
+
SDATA(data_type_t.DTP_BOOLEAN, "disabled", 0, false, "Rendered but not navigable"),
|
|
163
|
+
SDATA(data_type_t.DTP_INTEGER, "chrome_depth", 0, -1, "Chrome strips painted above the tip; -1 = inherit, 0 = none"),
|
|
164
|
+
SDATA(data_type_t.DTP_JSON, "aliases", 0, null, "Former ids of this node — old urls keep resolving (rewritten)"),
|
|
165
|
+
SDATA(data_type_t.DTP_JSON, "projection", 0, null, "How this node shows its children: layout | {index,chrome}"),
|
|
166
|
+
SDATA(data_type_t.DTP_JSON, "content", 0, null, "{gclass, kw} mounted when this node is the tip of the path"),
|
|
167
|
+
SDATA(data_type_t.DTP_JSON, "link", 0, null, "{kind, gclass, kw} — structure ends here; the viewer owns the subpath"),
|
|
168
|
+
SDATA(data_type_t.DTP_JSON, "children", 0, null, "Declared child specs — the first caller of the runtime API"),
|
|
169
|
+
|
|
170
|
+
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "ROOT only: the declared route the whole tree hangs from"),
|
|
171
|
+
SDATA(data_type_t.DTP_STRING, "tree_version", 0, "", "ROOT only: version of the tree CONTRACT (its paths are public urls)"),
|
|
172
|
+
|
|
173
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement (shell view contract)"),
|
|
174
|
+
SDATA_END()
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
let PRIVATE_DATA = {
|
|
178
|
+
children: null, /* child NODE gobjs, in order */
|
|
179
|
+
navs: null, /* live C_YUI_NAV gobjs inside my own body */
|
|
180
|
+
zone_navs: null, /* navs projected into the SPACE, by key */
|
|
181
|
+
zone_sig: null, /* their last item signature */
|
|
182
|
+
content_gobj: null,
|
|
183
|
+
active_child: null, /* child NODE gobj currently on the path */
|
|
184
|
+
chrome_depth: null, /* effective for the active path (null = all) */
|
|
185
|
+
distance: 0, /* segments from me down to the tip */
|
|
186
|
+
chain: null, /* nodes from my child down to the tip */
|
|
187
|
+
is_root: false,
|
|
188
|
+
$chrome: null,
|
|
189
|
+
$body: null
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let __gclass__ = null;
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/******************************
|
|
198
|
+
* Framework Methods
|
|
199
|
+
******************************/
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/***************************************************************
|
|
205
|
+
* Framework Method: Create
|
|
206
|
+
***************************************************************/
|
|
207
|
+
function mt_create(gobj)
|
|
208
|
+
{
|
|
209
|
+
let priv = gobj.priv;
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* CHILD subscription model
|
|
213
|
+
*/
|
|
214
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
215
|
+
if(!subscriber) {
|
|
216
|
+
subscriber = gobj_parent(gobj);
|
|
217
|
+
}
|
|
218
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
219
|
+
|
|
220
|
+
priv.children = [];
|
|
221
|
+
priv.navs = [];
|
|
222
|
+
priv.zone_navs = {};
|
|
223
|
+
priv.zone_sig = {};
|
|
224
|
+
|
|
225
|
+
let parent = gobj_parent(gobj);
|
|
226
|
+
priv.is_root = !parent || gobj_gclass_name(parent) !== GCLASS_NAME;
|
|
227
|
+
|
|
228
|
+
if(empty_string(gobj_read_attr(gobj, "node_id"))) {
|
|
229
|
+
log_error(`${GCLASS_NAME}: created without 'node_id' — the id IS the url segment`);
|
|
230
|
+
}
|
|
231
|
+
if(priv.is_root && empty_string(gobj_read_attr(gobj, "base_route"))) {
|
|
232
|
+
log_error(`${GCLASS_NAME}: root node created without 'base_route'`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
build_ui(gobj);
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* The declared tree is not a privileged path: it is a caller of
|
|
239
|
+
* the same runtime API, one EV_ADD_NODE per child.
|
|
240
|
+
*/
|
|
241
|
+
let declared = gobj_read_attr(gobj, "children");
|
|
242
|
+
if(declared !== null && declared !== undefined) {
|
|
243
|
+
if(!is_array(declared)) {
|
|
244
|
+
log_error(`${GCLASS_NAME} '${node_path_str(gobj)}': 'children' must be an array`);
|
|
245
|
+
} else {
|
|
246
|
+
for(let spec of declared) {
|
|
247
|
+
gobj_send_event(gobj, "EV_ADD_NODE", {spec: spec}, gobj);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/***************************************************************
|
|
254
|
+
* Framework Method: Start
|
|
255
|
+
***************************************************************/
|
|
256
|
+
function mt_start(gobj)
|
|
257
|
+
{
|
|
258
|
+
let priv = gobj.priv;
|
|
259
|
+
|
|
260
|
+
for(let child of priv.children) {
|
|
261
|
+
if(!gobj_is_running(child)) {
|
|
262
|
+
gobj_start(child);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if(!priv.is_root) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/*
|
|
271
|
+
* Only the root talks to the shell. Subscribing in mt_start keeps
|
|
272
|
+
* it symmetric with the unsubscribe in mt_stop; the shell mounts a
|
|
273
|
+
* view (create → appendChild → start) and only THEN broadcasts
|
|
274
|
+
* EV_ROUTE_CHANGED, so this still precedes the first broadcast.
|
|
275
|
+
*/
|
|
276
|
+
let shell = yui_shell_of(gobj);
|
|
277
|
+
if(!shell) {
|
|
278
|
+
log_error(`${GCLASS_NAME}: no shell — the tree cannot route`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
gobj_subscribe_event(shell, "EV_ROUTE_CHANGED", {}, gobj);
|
|
282
|
+
publish_sub_routes(gobj);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/***************************************************************
|
|
286
|
+
* Framework Method: Stop
|
|
287
|
+
***************************************************************/
|
|
288
|
+
function mt_stop(gobj)
|
|
289
|
+
{
|
|
290
|
+
let priv = gobj.priv;
|
|
291
|
+
|
|
292
|
+
if(priv.is_root) {
|
|
293
|
+
let shell = yui_shell_of(gobj);
|
|
294
|
+
if(shell) {
|
|
295
|
+
gobj_unsubscribe_event(shell, "EV_ROUTE_CHANGED", {}, gobj);
|
|
296
|
+
yui_shell_set_sub_routes(shell, gobj_read_attr(gobj, "base_route"), null);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
deactivate(gobj);
|
|
301
|
+
|
|
302
|
+
/* Symmetric with mt_start: what this node started, it stops —
|
|
303
|
+
* the framework destroys children, and destroying a RUNNING gobj
|
|
304
|
+
* is an error. */
|
|
305
|
+
if(priv.content_gobj && gobj_is_running(priv.content_gobj)) {
|
|
306
|
+
gobj_stop(priv.content_gobj);
|
|
307
|
+
}
|
|
308
|
+
for(let key of Object.keys(priv.zone_navs)) {
|
|
309
|
+
let nav = priv.zone_navs[key];
|
|
310
|
+
if(gobj_is_running(nav)) {
|
|
311
|
+
gobj_stop(nav);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for(let child of priv.children) {
|
|
315
|
+
if(gobj_is_running(child)) {
|
|
316
|
+
gobj_stop(child);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/***************************************************************
|
|
322
|
+
* Framework Method: Destroy
|
|
323
|
+
***************************************************************/
|
|
324
|
+
function mt_destroy(gobj)
|
|
325
|
+
{
|
|
326
|
+
let priv = gobj.priv;
|
|
327
|
+
|
|
328
|
+
/* gobj_destroy() destroys the children BEFORE calling mt_destroy,
|
|
329
|
+
* so the content and the projection navs are already gone: reaching
|
|
330
|
+
* for them here only logs "gobj NULL or DESTROYED". Drop the
|
|
331
|
+
* references and own nothing but the DOM. */
|
|
332
|
+
priv.content_gobj = null;
|
|
333
|
+
priv.navs = [];
|
|
334
|
+
priv.zone_navs = {};
|
|
335
|
+
priv.zone_sig = {};
|
|
336
|
+
priv.active_child = null;
|
|
337
|
+
|
|
338
|
+
let $c = gobj_read_attr(gobj, "$container");
|
|
339
|
+
if($c && $c.parentNode) {
|
|
340
|
+
$c.parentNode.removeChild($c);
|
|
341
|
+
}
|
|
342
|
+
gobj_write_attr(gobj, "$container", null);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
/***************************
|
|
349
|
+
* Local Methods
|
|
350
|
+
***************************/
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
/************************************************************
|
|
356
|
+
* Build the (empty) DOM skeleton. Inner DOM is built when the
|
|
357
|
+
* node is activated and torn down when it leaves the path, so
|
|
358
|
+
* an inactive branch costs one empty <div>.
|
|
359
|
+
************************************************************/
|
|
360
|
+
function build_ui(gobj)
|
|
361
|
+
{
|
|
362
|
+
let priv = gobj.priv;
|
|
363
|
+
let node_id = gobj_read_attr(gobj, "node_id");
|
|
364
|
+
|
|
365
|
+
let $container = createElement2(
|
|
366
|
+
["div", {class: `${GCLASS_NAME} NODE_VIEW`, "data-node-id": node_id}, [
|
|
367
|
+
["div", {class: "NODE_CHROME"}],
|
|
368
|
+
["div", {class: "NODE_BODY"}]
|
|
369
|
+
]]
|
|
370
|
+
);
|
|
371
|
+
priv.$chrome = $container.querySelector(".NODE_CHROME");
|
|
372
|
+
priv.$body = $container.querySelector(".NODE_BODY");
|
|
373
|
+
|
|
374
|
+
gobj_write_attr(gobj, "$container", $container);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/************************************************************
|
|
378
|
+
* The tree's root node (the one whose parent is not a node).
|
|
379
|
+
************************************************************/
|
|
380
|
+
function tree_root_of(gobj)
|
|
381
|
+
{
|
|
382
|
+
let g = gobj;
|
|
383
|
+
while(true) {
|
|
384
|
+
let parent = gobj_parent(g);
|
|
385
|
+
if(!parent || gobj_gclass_name(parent) !== GCLASS_NAME) {
|
|
386
|
+
return g;
|
|
387
|
+
}
|
|
388
|
+
g = parent;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/************************************************************
|
|
393
|
+
* Node ids from the root DOWN TO this node, root excluded
|
|
394
|
+
* (the root's own segment is already inside base_route).
|
|
395
|
+
************************************************************/
|
|
396
|
+
function node_ids(gobj)
|
|
397
|
+
{
|
|
398
|
+
let ids = [];
|
|
399
|
+
let g = gobj;
|
|
400
|
+
while(true) {
|
|
401
|
+
let parent = gobj_parent(g);
|
|
402
|
+
if(!parent || gobj_gclass_name(parent) !== GCLASS_NAME) {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
ids.unshift(gobj_read_attr(g, "node_id"));
|
|
406
|
+
g = parent;
|
|
407
|
+
}
|
|
408
|
+
return ids;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/************************************************************
|
|
412
|
+
* Canonical route of a node — the URL that lands exactly here.
|
|
413
|
+
************************************************************/
|
|
414
|
+
function route_of(gobj)
|
|
415
|
+
{
|
|
416
|
+
let root = tree_root_of(gobj);
|
|
417
|
+
return join_route(gobj_read_attr(root, "base_route"), node_ids(gobj));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/************************************************************
|
|
421
|
+
* Human path for logs: "<base>/a/b".
|
|
422
|
+
************************************************************/
|
|
423
|
+
function node_path_str(gobj)
|
|
424
|
+
{
|
|
425
|
+
return route_of(gobj);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/************************************************************
|
|
429
|
+
* Child node by id, or null.
|
|
430
|
+
************************************************************/
|
|
431
|
+
function find_child(gobj, node_id)
|
|
432
|
+
{
|
|
433
|
+
for(let child of gobj.priv.children) {
|
|
434
|
+
if(gobj_read_attr(child, "node_id") === node_id) {
|
|
435
|
+
return child;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/************************************************************
|
|
442
|
+
* Child node whose FORMER id is `node_id`, or null.
|
|
443
|
+
*
|
|
444
|
+
* The tree's paths are a contract — a url a client may have
|
|
445
|
+
* bookmarked, scripted or been sold as a door into the system.
|
|
446
|
+
* So an id is never rewritten in place: a rename ships the old
|
|
447
|
+
* id in `aliases`, the old url keeps resolving, and the shell
|
|
448
|
+
* rewrites it to the canonical one. That is what makes a
|
|
449
|
+
* version bump migratable instead of breaking.
|
|
450
|
+
************************************************************/
|
|
451
|
+
function find_child_by_alias(gobj, node_id)
|
|
452
|
+
{
|
|
453
|
+
for(let child of gobj.priv.children) {
|
|
454
|
+
let aliases = gobj_read_attr(child, "aliases");
|
|
455
|
+
if(is_array(aliases) && aliases.indexOf(node_id) >= 0) {
|
|
456
|
+
return child;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/************************************************************
|
|
463
|
+
* Is this node the boundary between structure and data?
|
|
464
|
+
************************************************************/
|
|
465
|
+
function has_link(gobj)
|
|
466
|
+
{
|
|
467
|
+
let link = gobj_read_attr(gobj, "link");
|
|
468
|
+
return !!(link && link.gclass);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/************************************************************
|
|
472
|
+
* What this node mounts when it is the tip: its link's viewer
|
|
473
|
+
* (the data case) or its content (the structural case).
|
|
474
|
+
************************************************************/
|
|
475
|
+
function view_spec(gobj)
|
|
476
|
+
{
|
|
477
|
+
let link = gobj_read_attr(gobj, "link");
|
|
478
|
+
if(link && link.gclass) {
|
|
479
|
+
return link;
|
|
480
|
+
}
|
|
481
|
+
return gobj_read_attr(gobj, "content");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/************************************************************
|
|
485
|
+
* Effective chrome depth for the active path: the DEEPEST
|
|
486
|
+
* declaration wins (a node close to the leaf knows better how
|
|
487
|
+
* much chrome its corner deserves than the root does), falling
|
|
488
|
+
* back to what the parent handed down, then to unlimited.
|
|
489
|
+
*
|
|
490
|
+
* This is why an intermediate node may exist for no other
|
|
491
|
+
* reason than to hold this number.
|
|
492
|
+
************************************************************/
|
|
493
|
+
function resolve_path_info(gobj, subpath, inherited)
|
|
494
|
+
{
|
|
495
|
+
let effective = (typeof inherited === "number") ? inherited : null;
|
|
496
|
+
|
|
497
|
+
let own = gobj_read_attr(gobj, "chrome_depth");
|
|
498
|
+
if(typeof own === "number" && own >= 0) {
|
|
499
|
+
effective = own;
|
|
500
|
+
}
|
|
501
|
+
if(has_link(gobj)) {
|
|
502
|
+
return {distance: 0, chrome_depth: effective, chain: []};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Distance is counted in STRUCTURAL segments: the tail a viewer
|
|
506
|
+
* owns below a link is url, not tree, and counting it would push
|
|
507
|
+
* every ancestor out of a chrome_depth budget it never spent. */
|
|
508
|
+
let distance = 0;
|
|
509
|
+
let chain = [];
|
|
510
|
+
let g = gobj;
|
|
511
|
+
for(let seg of split_subpath(subpath)) {
|
|
512
|
+
let child = find_child(g, seg) || find_child_by_alias(g, seg);
|
|
513
|
+
if(!child) {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
distance++;
|
|
517
|
+
chain.push(child);
|
|
518
|
+
let declared = gobj_read_attr(child, "chrome_depth");
|
|
519
|
+
if(typeof declared === "number" && declared >= 0) {
|
|
520
|
+
effective = declared;
|
|
521
|
+
}
|
|
522
|
+
g = child;
|
|
523
|
+
if(has_link(child)) {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return {distance: distance, chrome_depth: effective, chain: chain};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/************************************************************
|
|
531
|
+
* Resolve a path of ids ("a/b/c") from `node` downwards.
|
|
532
|
+
* Returns the deepest LIVING node plus the segments that did
|
|
533
|
+
* not resolve — the caller decides what a dead tail means.
|
|
534
|
+
************************************************************/
|
|
535
|
+
function walk_path(node, path)
|
|
536
|
+
{
|
|
537
|
+
let segs = split_subpath(path);
|
|
538
|
+
let g = node;
|
|
539
|
+
let i = 0;
|
|
540
|
+
while(i < segs.length) {
|
|
541
|
+
let next = find_child(g, segs[i]);
|
|
542
|
+
if(!next) {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
g = next;
|
|
546
|
+
i++;
|
|
547
|
+
}
|
|
548
|
+
return {node: g, missing: segs.slice(i)};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/************************************************************
|
|
552
|
+
* Destroy the C_YUI_NAV gobjs rendering this node's projection.
|
|
553
|
+
************************************************************/
|
|
554
|
+
function clear_navs(gobj)
|
|
555
|
+
{
|
|
556
|
+
let priv = gobj.priv;
|
|
557
|
+
for(let nav of priv.navs) {
|
|
558
|
+
if(gobj_is_running(nav)) {
|
|
559
|
+
gobj_stop(nav);
|
|
560
|
+
}
|
|
561
|
+
gobj_destroy(nav);
|
|
562
|
+
}
|
|
563
|
+
priv.navs = [];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/************************************************************
|
|
567
|
+
* Render this node's projection of its children, in one mode:
|
|
568
|
+
* "index" — I am the tip: the projection is the page.
|
|
569
|
+
* "chrome" — a child is showing: the projection is its chrome.
|
|
570
|
+
************************************************************/
|
|
571
|
+
function render_projection(gobj, mode, $where, active_route, active_id, zones_only)
|
|
572
|
+
{
|
|
573
|
+
let priv = gobj.priv;
|
|
574
|
+
|
|
575
|
+
if(!priv.children.length) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
let renders = projection_renders(gobj_read_attr(gobj, "projection"), mode);
|
|
579
|
+
if(!renders.length) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let my_route = route_of(gobj);
|
|
584
|
+
let base = my_route === "/" ? "" : my_route;
|
|
585
|
+
let specs = [];
|
|
586
|
+
for(let child of priv.children) {
|
|
587
|
+
specs.push({
|
|
588
|
+
id: gobj_read_attr(child, "node_id"),
|
|
589
|
+
label: gobj_read_attr(child, "label"),
|
|
590
|
+
icon: gobj_read_attr(child, "icon"),
|
|
591
|
+
tooltip: gobj_read_attr(child, "tooltip"),
|
|
592
|
+
disabled: gobj_read_attr(child, "disabled")
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
let items = child_nav_items(specs, (id) => `${base}/${id}`);
|
|
596
|
+
|
|
597
|
+
let i = 0;
|
|
598
|
+
for(let render of renders) {
|
|
599
|
+
/* A projection with a `zone` goes into the SPACE (the root
|
|
600
|
+
* rail/bar), not into this node's body — and it PERSISTS. It
|
|
601
|
+
* is the app's standing chrome: rebuilding it on every
|
|
602
|
+
* navigation would throw away its scroll and blink the rail,
|
|
603
|
+
* so it is created once and told where the user is. */
|
|
604
|
+
if(render.zone) {
|
|
605
|
+
project_into_zone(gobj, render, items, active_route, active_id);
|
|
606
|
+
i++;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if(zones_only) {
|
|
610
|
+
i++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
let $target = $where;
|
|
614
|
+
let nav = gobj_create_pure_child(
|
|
615
|
+
`${NAV_PREFIX}${mode}_${i}__`,
|
|
616
|
+
"C_YUI_NAV",
|
|
617
|
+
{
|
|
618
|
+
menu_id: `node.${my_route}`,
|
|
619
|
+
nav_label: gobj_read_attr(gobj, "label") || gobj_read_attr(gobj, "node_id"),
|
|
620
|
+
menu_items: items,
|
|
621
|
+
layout: render.layout,
|
|
622
|
+
zone: render.zone || "",
|
|
623
|
+
icon_pos: render.icon_pos || "top",
|
|
624
|
+
show_label: render.show_label !== false,
|
|
625
|
+
show_on: render.show_on || "",
|
|
626
|
+
level: render.zone ? "primary" : "secondary",
|
|
627
|
+
/* A backbar goes UP: back to this node's own index. */
|
|
628
|
+
back_route: (render.layout === "backbar") ? my_route : "",
|
|
629
|
+
active_route: active_route || ""
|
|
630
|
+
},
|
|
631
|
+
gobj
|
|
632
|
+
);
|
|
633
|
+
gobj_start(nav);
|
|
634
|
+
let $nav = gobj_read_attr(nav, "$container");
|
|
635
|
+
if($nav) {
|
|
636
|
+
$target.appendChild($nav);
|
|
637
|
+
}
|
|
638
|
+
priv.navs.push(nav);
|
|
639
|
+
i++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/************************************************************
|
|
644
|
+
* Project into a zone of the SPACE, once, and keep it.
|
|
645
|
+
*
|
|
646
|
+
* Rebuilt only when the children actually changed (the runtime
|
|
647
|
+
* API added or removed one); otherwise the nav is just told the
|
|
648
|
+
* new position, which is what C_YUI_NAV's EV_ROUTE_CHANGED is
|
|
649
|
+
* for. Unknown zone: nothing is built rather than mounted
|
|
650
|
+
* nowhere — a menu that silently vanishes is the worse failure.
|
|
651
|
+
************************************************************/
|
|
652
|
+
function project_into_zone(gobj, render, items, active_route, active_id)
|
|
653
|
+
{
|
|
654
|
+
let priv = gobj.priv;
|
|
655
|
+
let key = `${render.zone}|${render.layout}|${render.show_on || ""}`;
|
|
656
|
+
let sig = items.map((it) => it.id).join(",");
|
|
657
|
+
let nav = priv.zone_navs[key];
|
|
658
|
+
|
|
659
|
+
if(!nav) {
|
|
660
|
+
let $zone = yui_shell_zone(yui_shell_of(gobj), render.zone);
|
|
661
|
+
if(!$zone) {
|
|
662
|
+
return; /* Error already logged */
|
|
663
|
+
}
|
|
664
|
+
nav = gobj_create_pure_child(
|
|
665
|
+
`${NAV_PREFIX}zone_${render.zone}_${render.layout}__`,
|
|
666
|
+
"C_YUI_NAV",
|
|
667
|
+
{
|
|
668
|
+
menu_id: `node.${route_of(gobj)}`,
|
|
669
|
+
nav_label: gobj_read_attr(gobj, "label") || gobj_read_attr(gobj, "node_id"),
|
|
670
|
+
menu_items: items,
|
|
671
|
+
zone: render.zone,
|
|
672
|
+
layout: render.layout,
|
|
673
|
+
icon_pos: render.icon_pos || "left",
|
|
674
|
+
show_label: render.show_label !== false,
|
|
675
|
+
show_on: render.show_on || "",
|
|
676
|
+
level: "primary"
|
|
677
|
+
},
|
|
678
|
+
gobj
|
|
679
|
+
);
|
|
680
|
+
gobj_start(nav);
|
|
681
|
+
let $nav = gobj_read_attr(nav, "$container");
|
|
682
|
+
if($nav) {
|
|
683
|
+
$zone.appendChild($nav);
|
|
684
|
+
}
|
|
685
|
+
priv.zone_navs[key] = nav;
|
|
686
|
+
priv.zone_sig[key] = sig;
|
|
687
|
+
} else if(priv.zone_sig[key] !== sig) {
|
|
688
|
+
gobj_send_event(nav, "EV_SET_ITEMS", {items: items}, gobj);
|
|
689
|
+
priv.zone_sig[key] = sig;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
gobj_send_event(nav, "EV_ROUTE_CHANGED", {
|
|
693
|
+
route: active_route || "",
|
|
694
|
+
item: {id: active_id || ""}
|
|
695
|
+
}, gobj);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/************************************************************
|
|
699
|
+
* Mount this node's own content (lazily, once). A node with
|
|
700
|
+
* content AND children shows the content first and projects
|
|
701
|
+
* its children under it.
|
|
702
|
+
************************************************************/
|
|
703
|
+
function mount_content(gobj)
|
|
704
|
+
{
|
|
705
|
+
let priv = gobj.priv;
|
|
706
|
+
let content = view_spec(gobj);
|
|
707
|
+
|
|
708
|
+
if(!content || !content.gclass) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
if(priv.content_gobj) {
|
|
712
|
+
return priv.content_gobj;
|
|
713
|
+
}
|
|
714
|
+
if(!gclass_find_by_name(content.gclass)) {
|
|
715
|
+
log_error(
|
|
716
|
+
`${GCLASS_NAME} '${node_path_str(gobj)}': content gclass ` +
|
|
717
|
+
`'${content.gclass}' is not registered`
|
|
718
|
+
);
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let view = gobj_create_pure_child(
|
|
723
|
+
CONTENT_NAME, content.gclass, content.kw || {}, gobj
|
|
724
|
+
);
|
|
725
|
+
gobj_start(view);
|
|
726
|
+
priv.content_gobj = view;
|
|
727
|
+
return view;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/************************************************************
|
|
731
|
+
* Tear down the mounted content, orderly: a RUNNING gobj must
|
|
732
|
+
* be stopped before it is destroyed.
|
|
733
|
+
************************************************************/
|
|
734
|
+
function destroy_content(gobj)
|
|
735
|
+
{
|
|
736
|
+
let priv = gobj.priv;
|
|
737
|
+
|
|
738
|
+
if(!priv.content_gobj) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if(gobj_is_running(priv.content_gobj)) {
|
|
742
|
+
gobj_stop(priv.content_gobj);
|
|
743
|
+
}
|
|
744
|
+
gobj_destroy(priv.content_gobj);
|
|
745
|
+
priv.content_gobj = null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/************************************************************
|
|
749
|
+
* Paint state ST_SELF: I am the tip of the path.
|
|
750
|
+
************************************************************/
|
|
751
|
+
function render_self(gobj)
|
|
752
|
+
{
|
|
753
|
+
let priv = gobj.priv;
|
|
754
|
+
|
|
755
|
+
clear_navs(gobj);
|
|
756
|
+
priv.$chrome.replaceChildren();
|
|
757
|
+
priv.$body.replaceChildren();
|
|
758
|
+
|
|
759
|
+
let view = mount_content(gobj);
|
|
760
|
+
if(view) {
|
|
761
|
+
let $v = gobj_read_attr(view, "$container");
|
|
762
|
+
if($v) {
|
|
763
|
+
let $slot = createElement2(["div", {class: "NODE_CONTENT"}]);
|
|
764
|
+
$slot.appendChild($v);
|
|
765
|
+
priv.$body.appendChild($slot);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
render_path(gobj, priv.$chrome);
|
|
770
|
+
|
|
771
|
+
if(priv.children.length) {
|
|
772
|
+
let $index = createElement2(["div", {class: "NODE_INDEX"}]);
|
|
773
|
+
priv.$body.appendChild($index);
|
|
774
|
+
render_projection(gobj, "index", $index, "", "");
|
|
775
|
+
/* The zone projection is standing chrome: it lives across states,
|
|
776
|
+
* so its chrome renders must be refreshed here too or the rail
|
|
777
|
+
* would keep highlighting the child we just left. ZONES ONLY —
|
|
778
|
+
* a body-bound chrome render drawn next to the index would be
|
|
779
|
+
* the same children listed twice on one screen. */
|
|
780
|
+
render_projection(gobj, "chrome", $index, "", "", true);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if(!view) {
|
|
785
|
+
priv.$body.appendChild(createElement2(
|
|
786
|
+
["div", {class: "NODE_EMPTY", i18n: "nothing here yet"},
|
|
787
|
+
"Nothing here yet."]
|
|
788
|
+
));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/************************************************************
|
|
793
|
+
* Paint state ST_CHILD: a child of mine is on the path.
|
|
794
|
+
************************************************************/
|
|
795
|
+
function render_child(gobj, child)
|
|
796
|
+
{
|
|
797
|
+
let priv = gobj.priv;
|
|
798
|
+
|
|
799
|
+
clear_navs(gobj);
|
|
800
|
+
priv.$chrome.replaceChildren();
|
|
801
|
+
priv.$body.replaceChildren();
|
|
802
|
+
|
|
803
|
+
/* Every ancestor painting its own chrome stacks one strip per
|
|
804
|
+
* level. `chrome_depth` is how a node caps that for its corner
|
|
805
|
+
* of the tree (see resolve_chrome_depth). */
|
|
806
|
+
if(chrome_visible(priv.distance, priv.chrome_depth)) {
|
|
807
|
+
render_projection(gobj, "chrome", priv.$chrome, route_of(child),
|
|
808
|
+
gobj_read_attr(child, "node_id"));
|
|
809
|
+
}
|
|
810
|
+
render_path(gobj, priv.$chrome);
|
|
811
|
+
|
|
812
|
+
let $slot = createElement2(["div", {class: "NODE_CHILD"}]);
|
|
813
|
+
let $child = gobj_read_attr(child, "$container");
|
|
814
|
+
if($child) {
|
|
815
|
+
$slot.appendChild($child);
|
|
816
|
+
}
|
|
817
|
+
priv.$body.appendChild($slot);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/************************************************************
|
|
821
|
+
* Render the trail down to where the user is, as one line.
|
|
822
|
+
*
|
|
823
|
+
* The other two projections show a node's CHILDREN; this one shows
|
|
824
|
+
* the PATH, so its items are me plus the chain below me — which is
|
|
825
|
+
* why it is rendered by ONE node (whoever declares it) instead of
|
|
826
|
+
* by every ancestor. Same item contract as any other nav, so the
|
|
827
|
+
* crumbs click and translate like everything else.
|
|
828
|
+
************************************************************/
|
|
829
|
+
function render_path(gobj, $where)
|
|
830
|
+
{
|
|
831
|
+
let priv = gobj.priv;
|
|
832
|
+
let renders = projection_renders(gobj_read_attr(gobj, "projection"), "path");
|
|
833
|
+
|
|
834
|
+
if(!renders.length) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/* From the TREE ROOT down to the tip, not from here: a trail that
|
|
839
|
+
* starts halfway ("South hall › Meter 3") answers the question it
|
|
840
|
+
* was drawn to answer only by half. The declaring node decides
|
|
841
|
+
* HOW its corner shows the way in; it does not decide where the
|
|
842
|
+
* way in starts. */
|
|
843
|
+
let path = [];
|
|
844
|
+
let g = gobj;
|
|
845
|
+
while(g) {
|
|
846
|
+
path.unshift(g);
|
|
847
|
+
let parent = gobj_parent(g);
|
|
848
|
+
g = (parent && gobj_gclass_name(parent) === GCLASS_NAME) ? parent : null;
|
|
849
|
+
}
|
|
850
|
+
let items = path.concat(priv.chain || []).map((node) => ({
|
|
851
|
+
id: gobj_read_attr(node, "node_id"),
|
|
852
|
+
name: gobj_read_attr(node, "label") || gobj_read_attr(node, "node_id"),
|
|
853
|
+
icon: gobj_read_attr(node, "icon") || "",
|
|
854
|
+
route: route_of(node)
|
|
855
|
+
}));
|
|
856
|
+
let tip = items[items.length - 1];
|
|
857
|
+
|
|
858
|
+
let i = 0;
|
|
859
|
+
for(let render of renders) {
|
|
860
|
+
let nav = gobj_create_pure_child(
|
|
861
|
+
`${NAV_PREFIX}path_${i}__`,
|
|
862
|
+
"C_YUI_NAV",
|
|
863
|
+
{
|
|
864
|
+
menu_id: `node.path.${route_of(gobj)}`,
|
|
865
|
+
nav_label: gobj_read_attr(gobj, "label") || "",
|
|
866
|
+
menu_items: items,
|
|
867
|
+
layout: render.layout,
|
|
868
|
+
icon_pos: render.icon_pos || "left",
|
|
869
|
+
show_label: render.show_label !== false,
|
|
870
|
+
show_on: render.show_on || "",
|
|
871
|
+
level: "secondary",
|
|
872
|
+
active_route: tip.route
|
|
873
|
+
},
|
|
874
|
+
gobj
|
|
875
|
+
);
|
|
876
|
+
gobj_start(nav);
|
|
877
|
+
let $nav = gobj_read_attr(nav, "$container");
|
|
878
|
+
if($nav) {
|
|
879
|
+
$where.appendChild($nav);
|
|
880
|
+
}
|
|
881
|
+
priv.navs.push(nav);
|
|
882
|
+
i++;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/************************************************************
|
|
887
|
+
* Leave the path: tear down my rendering and my active child's.
|
|
888
|
+
************************************************************/
|
|
889
|
+
function deactivate(gobj)
|
|
890
|
+
{
|
|
891
|
+
let priv = gobj.priv;
|
|
892
|
+
|
|
893
|
+
if(priv.active_child) {
|
|
894
|
+
gobj_send_event(priv.active_child, "EV_DEACTIVATE", {}, gobj);
|
|
895
|
+
priv.active_child = null;
|
|
896
|
+
}
|
|
897
|
+
clear_navs(gobj);
|
|
898
|
+
if(priv.$chrome) {
|
|
899
|
+
priv.$chrome.replaceChildren();
|
|
900
|
+
}
|
|
901
|
+
if(priv.$body) {
|
|
902
|
+
priv.$body.replaceChildren();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/************************************************************
|
|
907
|
+
* Enter (or re-enter) the path with `subpath` as my tail.
|
|
908
|
+
* Returns the new state name.
|
|
909
|
+
************************************************************/
|
|
910
|
+
function activate(gobj, subpath, inherited_depth)
|
|
911
|
+
{
|
|
912
|
+
let priv = gobj.priv;
|
|
913
|
+
let {head, tail} = head_tail(subpath);
|
|
914
|
+
|
|
915
|
+
let info = resolve_path_info(gobj, subpath, inherited_depth);
|
|
916
|
+
priv.distance = info.distance;
|
|
917
|
+
priv.chrome_depth = info.chrome_depth;
|
|
918
|
+
priv.chain = info.chain;
|
|
919
|
+
|
|
920
|
+
/* Structure ends here: whatever is left of the url is data, and it
|
|
921
|
+
* belongs to the viewer. Re-render only the first time — paging
|
|
922
|
+
* through data must not rebuild the DOM under the viewer. */
|
|
923
|
+
if(has_link(gobj)) {
|
|
924
|
+
if(!priv.content_gobj || gobj_current_state(gobj) !== "ST_SELF") {
|
|
925
|
+
if(priv.active_child) {
|
|
926
|
+
gobj_send_event(priv.active_child, "EV_DEACTIVATE", {}, gobj);
|
|
927
|
+
priv.active_child = null;
|
|
928
|
+
}
|
|
929
|
+
render_self(gobj);
|
|
930
|
+
}
|
|
931
|
+
forward_subpath(gobj, subpath);
|
|
932
|
+
return "ST_SELF";
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if(!head) {
|
|
936
|
+
if(priv.active_child) {
|
|
937
|
+
gobj_send_event(priv.active_child, "EV_DEACTIVATE", {}, gobj);
|
|
938
|
+
priv.active_child = null;
|
|
939
|
+
}
|
|
940
|
+
render_self(gobj);
|
|
941
|
+
return "ST_SELF";
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
let child = find_child(gobj, head);
|
|
945
|
+
if(!child) {
|
|
946
|
+
/* A FORMER id: the url is still part of the contract, it just
|
|
947
|
+
* is not the canonical spelling any more. Rewrite it (replace
|
|
948
|
+
* — code decided) and carry on; the route change that follows
|
|
949
|
+
* re-enters here on the canonical path. */
|
|
950
|
+
let renamed = find_child_by_alias(gobj, head);
|
|
951
|
+
if(renamed) {
|
|
952
|
+
let shell = yui_shell_of(gobj);
|
|
953
|
+
if(shell) {
|
|
954
|
+
let canonical = tail ?
|
|
955
|
+
`${route_of(renamed)}/${tail}` : route_of(renamed);
|
|
956
|
+
yui_shell_navigate(shell, canonical, {replace: true});
|
|
957
|
+
}
|
|
958
|
+
child = renamed;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if(!child) {
|
|
962
|
+
/* The ground moved under a bookmark (or a typo): land on the
|
|
963
|
+
* deepest living ancestor — me — and say so. CODE decided the
|
|
964
|
+
* move, so it is a replace (ROUTING.md §2). */
|
|
965
|
+
log_warning(
|
|
966
|
+
`${GCLASS_NAME} '${node_path_str(gobj)}': no child '${head}' — ` +
|
|
967
|
+
`falling back to this node`
|
|
968
|
+
);
|
|
969
|
+
let shell = yui_shell_of(gobj);
|
|
970
|
+
if(shell) {
|
|
971
|
+
yui_shell_navigate(shell, route_of(gobj), {replace: true});
|
|
972
|
+
}
|
|
973
|
+
if(priv.active_child) {
|
|
974
|
+
gobj_send_event(priv.active_child, "EV_DEACTIVATE", {}, gobj);
|
|
975
|
+
priv.active_child = null;
|
|
976
|
+
}
|
|
977
|
+
render_self(gobj);
|
|
978
|
+
return "ST_SELF";
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if(priv.active_child && priv.active_child !== child) {
|
|
982
|
+
gobj_send_event(priv.active_child, "EV_DEACTIVATE", {}, gobj);
|
|
983
|
+
}
|
|
984
|
+
priv.active_child = child;
|
|
985
|
+
render_child(gobj, child);
|
|
986
|
+
gobj_send_event(child, "EV_ACTIVATE", {
|
|
987
|
+
subpath: tail,
|
|
988
|
+
chrome_depth: priv.chrome_depth
|
|
989
|
+
}, gobj);
|
|
990
|
+
return "ST_CHILD";
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/************************************************************
|
|
994
|
+
* Hand the data tail to the viewer, in the SAME shape the shell
|
|
995
|
+
* gives a view (ROUTING.md §5): `base` is this node's canonical
|
|
996
|
+
* route — what the viewer builds its own deep links from — and
|
|
997
|
+
* `subpath` is its tail, empty meaning "the viewer's home".
|
|
998
|
+
*
|
|
999
|
+
* A viewer behind a link therefore cannot tell whether the shell
|
|
1000
|
+
* mounted it at a declared route or a node did, deep in a tree.
|
|
1001
|
+
* It MUST declare EV_ROUTE_CHANGED: a data space with no
|
|
1002
|
+
* addressable position inside it would not need a link at all.
|
|
1003
|
+
************************************************************/
|
|
1004
|
+
function forward_subpath(gobj, subpath)
|
|
1005
|
+
{
|
|
1006
|
+
let priv = gobj.priv;
|
|
1007
|
+
|
|
1008
|
+
if(!priv.content_gobj) {
|
|
1009
|
+
return; /* Error already logged by mount_content() */
|
|
1010
|
+
}
|
|
1011
|
+
let base = route_of(gobj);
|
|
1012
|
+
gobj_send_event(priv.content_gobj, "EV_ROUTE_CHANGED", {
|
|
1013
|
+
route: subpath ? `${base}/${subpath}` : base,
|
|
1014
|
+
base: base,
|
|
1015
|
+
subpath: subpath || ""
|
|
1016
|
+
}, gobj);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/************************************************************
|
|
1020
|
+
* Repaint after the tree changed under me, without moving the
|
|
1021
|
+
* user: same state, fresh projection.
|
|
1022
|
+
************************************************************/
|
|
1023
|
+
function repaint(gobj)
|
|
1024
|
+
{
|
|
1025
|
+
let priv = gobj.priv;
|
|
1026
|
+
let state = gobj_current_state(gobj);
|
|
1027
|
+
|
|
1028
|
+
if(state === "ST_SELF") {
|
|
1029
|
+
render_self(gobj);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if(state === "ST_CHILD" && priv.active_child) {
|
|
1033
|
+
render_child(gobj, priv.active_child);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/************************************************************
|
|
1038
|
+
* Contribute the whole tree to the shell's site map (ROUTING
|
|
1039
|
+
* §5.4) — a pull-at-render registry, so the map shows every
|
|
1040
|
+
* level instead of one declared route.
|
|
1041
|
+
************************************************************/
|
|
1042
|
+
function publish_sub_routes(gobj)
|
|
1043
|
+
{
|
|
1044
|
+
let priv = gobj.priv;
|
|
1045
|
+
|
|
1046
|
+
if(!priv.is_root) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
let shell = yui_shell_of(gobj);
|
|
1050
|
+
if(!shell) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
yui_shell_set_sub_routes(
|
|
1054
|
+
shell, gobj_read_attr(gobj, "base_route"), sub_route_nodes(gobj)
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function sub_route_nodes(gobj)
|
|
1059
|
+
{
|
|
1060
|
+
let nodes = [];
|
|
1061
|
+
for(let child of gobj.priv.children) {
|
|
1062
|
+
nodes.push({
|
|
1063
|
+
route: route_of(child),
|
|
1064
|
+
label: gobj_read_attr(child, "label") || gobj_read_attr(child, "node_id"),
|
|
1065
|
+
icon: gobj_read_attr(child, "icon") || "",
|
|
1066
|
+
children: sub_route_nodes(child)
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return nodes;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/************************************************************
|
|
1073
|
+
* Tell the root the shape changed (site map + any host), by
|
|
1074
|
+
* bubbling one event up the gobj tree. Sent, not published:
|
|
1075
|
+
* the root's parent is the SHELL, whose FSM must not receive
|
|
1076
|
+
* events of a gclass it does not know about.
|
|
1077
|
+
************************************************************/
|
|
1078
|
+
function notify_tree_changed(gobj)
|
|
1079
|
+
{
|
|
1080
|
+
let parent = gobj_parent(gobj);
|
|
1081
|
+
if(parent && gobj_gclass_name(parent) === GCLASS_NAME) {
|
|
1082
|
+
gobj_send_event(parent, "EV_NODE_TREE_CHANGED", {}, gobj);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
publish_sub_routes(gobj);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
/***************************
|
|
1092
|
+
* Actions
|
|
1093
|
+
***************************/
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
/************************************************************
|
|
1099
|
+
* The shell moved the URL (root only).
|
|
1100
|
+
************************************************************/
|
|
1101
|
+
function ac_route_changed(gobj, event, kw, src)
|
|
1102
|
+
{
|
|
1103
|
+
let base = kw.base || "";
|
|
1104
|
+
let my_base = gobj_read_attr(gobj, "base_route");
|
|
1105
|
+
|
|
1106
|
+
if(base !== my_base) {
|
|
1107
|
+
/* Another view owns the URL now. A keep_alive tree stays
|
|
1108
|
+
* created but must not keep painting a stale branch. */
|
|
1109
|
+
deactivate(gobj);
|
|
1110
|
+
gobj_change_state(gobj, "ST_OFF");
|
|
1111
|
+
return 0;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
gobj_change_state(gobj, activate(gobj, kw.subpath || "", null));
|
|
1115
|
+
return 0;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/************************************************************
|
|
1119
|
+
* My parent put me on the path.
|
|
1120
|
+
************************************************************/
|
|
1121
|
+
function ac_activate(gobj, event, kw, src)
|
|
1122
|
+
{
|
|
1123
|
+
gobj_change_state(gobj, activate(gobj, kw.subpath || "", kw.chrome_depth));
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/************************************************************
|
|
1128
|
+
* My parent took me off the path.
|
|
1129
|
+
************************************************************/
|
|
1130
|
+
function ac_deactivate(gobj, event, kw, src)
|
|
1131
|
+
{
|
|
1132
|
+
deactivate(gobj);
|
|
1133
|
+
gobj_change_state(gobj, "ST_OFF");
|
|
1134
|
+
return 0;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/************************************************************
|
|
1138
|
+
* A projection was clicked. The node never navigates itself
|
|
1139
|
+
* by mutating state: it changes the URL and lets the route
|
|
1140
|
+
* come back down (ROUTING.md §1.3).
|
|
1141
|
+
************************************************************/
|
|
1142
|
+
function ac_nav_clicked(gobj, event, kw, src)
|
|
1143
|
+
{
|
|
1144
|
+
let route = kw.route || "";
|
|
1145
|
+
|
|
1146
|
+
if(empty_string(route)) {
|
|
1147
|
+
log_error(`${GCLASS_NAME} '${node_path_str(gobj)}': EV_NAV_CLICKED without route`);
|
|
1148
|
+
return -1;
|
|
1149
|
+
}
|
|
1150
|
+
let shell = yui_shell_of(gobj);
|
|
1151
|
+
if(!shell) {
|
|
1152
|
+
log_error(`${GCLASS_NAME} '${node_path_str(gobj)}': no shell to navigate to '${route}'`);
|
|
1153
|
+
return -1;
|
|
1154
|
+
}
|
|
1155
|
+
yui_shell_navigate(shell, route);
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/************************************************************
|
|
1160
|
+
* Runtime API: add a child. Same path the declared tree takes.
|
|
1161
|
+
************************************************************/
|
|
1162
|
+
function ac_add_node(gobj, event, kw, src)
|
|
1163
|
+
{
|
|
1164
|
+
let priv = gobj.priv;
|
|
1165
|
+
let errors = [];
|
|
1166
|
+
let spec = normalize_spec(kw.spec, errors, `${node_path_str(gobj)}/`);
|
|
1167
|
+
|
|
1168
|
+
if(!spec) {
|
|
1169
|
+
for(let e of errors) {
|
|
1170
|
+
log_error(`${GCLASS_NAME}: ${e}`);
|
|
1171
|
+
}
|
|
1172
|
+
return -1;
|
|
1173
|
+
}
|
|
1174
|
+
if(find_child(gobj, spec.id)) {
|
|
1175
|
+
log_error(
|
|
1176
|
+
`${GCLASS_NAME} '${node_path_str(gobj)}': child '${spec.id}' ` +
|
|
1177
|
+
`already exists — sibling ids are url segments`
|
|
1178
|
+
);
|
|
1179
|
+
return -1;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* A DTP_JSON attr rejects null (json2data FAILED), so a node
|
|
1183
|
+
* without projection/content simply does not declare them. */
|
|
1184
|
+
let kw_child = {
|
|
1185
|
+
node_id: spec.id,
|
|
1186
|
+
label: spec.label,
|
|
1187
|
+
icon: spec.icon,
|
|
1188
|
+
tooltip: spec.tooltip,
|
|
1189
|
+
disabled: spec.disabled,
|
|
1190
|
+
chrome_depth: spec.chrome_depth,
|
|
1191
|
+
children: spec.children
|
|
1192
|
+
};
|
|
1193
|
+
if(spec.aliases && spec.aliases.length) {
|
|
1194
|
+
kw_child.aliases = spec.aliases;
|
|
1195
|
+
}
|
|
1196
|
+
if(spec.projection) {
|
|
1197
|
+
kw_child.projection = spec.projection;
|
|
1198
|
+
}
|
|
1199
|
+
if(spec.content) {
|
|
1200
|
+
kw_child.content = spec.content;
|
|
1201
|
+
}
|
|
1202
|
+
if(spec.link) {
|
|
1203
|
+
kw_child.link = spec.link;
|
|
1204
|
+
}
|
|
1205
|
+
let child = gobj_create_pure_child(spec.id, GCLASS_NAME, kw_child, gobj);
|
|
1206
|
+
|
|
1207
|
+
let index = (typeof kw.index === "number") ? kw.index : priv.children.length;
|
|
1208
|
+
if(index < 0) {
|
|
1209
|
+
index = 0;
|
|
1210
|
+
}
|
|
1211
|
+
if(index > priv.children.length) {
|
|
1212
|
+
index = priv.children.length;
|
|
1213
|
+
}
|
|
1214
|
+
priv.children.splice(index, 0, child);
|
|
1215
|
+
|
|
1216
|
+
if(gobj_is_running(gobj)) {
|
|
1217
|
+
gobj_start(child);
|
|
1218
|
+
}
|
|
1219
|
+
repaint(gobj);
|
|
1220
|
+
notify_tree_changed(gobj);
|
|
1221
|
+
return 0;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/************************************************************
|
|
1225
|
+
* Runtime API: remove a child (and its whole subtree).
|
|
1226
|
+
************************************************************/
|
|
1227
|
+
function ac_remove_node(gobj, event, kw, src)
|
|
1228
|
+
{
|
|
1229
|
+
let priv = gobj.priv;
|
|
1230
|
+
let node_id = kw.node_id || "";
|
|
1231
|
+
let child = find_child(gobj, node_id);
|
|
1232
|
+
|
|
1233
|
+
if(!child) {
|
|
1234
|
+
log_error(
|
|
1235
|
+
`${GCLASS_NAME} '${node_path_str(gobj)}': cannot remove '${node_id}' — no such child`
|
|
1236
|
+
);
|
|
1237
|
+
return -1;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/* Removing the branch the user is standing on: move them to the
|
|
1241
|
+
* nearest living ancestor (me) BEFORE the gobj dies. */
|
|
1242
|
+
let was_active = (priv.active_child === child);
|
|
1243
|
+
if(was_active) {
|
|
1244
|
+
gobj_send_event(child, "EV_DEACTIVATE", {}, gobj);
|
|
1245
|
+
priv.active_child = null;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let idx = priv.children.indexOf(child);
|
|
1249
|
+
priv.children.splice(idx, 1);
|
|
1250
|
+
if(gobj_is_running(child)) {
|
|
1251
|
+
gobj_stop(child);
|
|
1252
|
+
}
|
|
1253
|
+
gobj_destroy(child);
|
|
1254
|
+
|
|
1255
|
+
if(was_active) {
|
|
1256
|
+
let shell = yui_shell_of(gobj);
|
|
1257
|
+
if(shell) {
|
|
1258
|
+
yui_shell_navigate(shell, route_of(gobj), {replace: true});
|
|
1259
|
+
}
|
|
1260
|
+
gobj_change_state(gobj, "ST_SELF");
|
|
1261
|
+
render_self(gobj);
|
|
1262
|
+
} else {
|
|
1263
|
+
repaint(gobj);
|
|
1264
|
+
}
|
|
1265
|
+
notify_tree_changed(gobj);
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/************************************************************
|
|
1270
|
+
* Runtime API: change how I show my children.
|
|
1271
|
+
************************************************************/
|
|
1272
|
+
function ac_set_projection(gobj, event, kw, src)
|
|
1273
|
+
{
|
|
1274
|
+
let errors = [];
|
|
1275
|
+
/* Validate through the same door as a whole spec: a projection
|
|
1276
|
+
* swap must not be the one path that accepts garbage. */
|
|
1277
|
+
let probe = normalize_spec(
|
|
1278
|
+
{id: gobj_read_attr(gobj, "node_id"), projection: kw.projection},
|
|
1279
|
+
errors, `${node_path_str(gobj)}`
|
|
1280
|
+
);
|
|
1281
|
+
if(!probe) {
|
|
1282
|
+
for(let e of errors) {
|
|
1283
|
+
log_error(`${GCLASS_NAME}: ${e}`);
|
|
1284
|
+
}
|
|
1285
|
+
return -1;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
gobj_write_attr(gobj, "projection", kw.projection || {});
|
|
1289
|
+
repaint(gobj);
|
|
1290
|
+
return 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/************************************************************
|
|
1294
|
+
* Runtime API: change how much stacked chrome my corner shows.
|
|
1295
|
+
*
|
|
1296
|
+
* Declarative == dynamic: `chrome_depth` is how a branch trades
|
|
1297
|
+
* stacked strips for something else, so it has to be reachable
|
|
1298
|
+
* at runtime like every other part of the shape. Paired with
|
|
1299
|
+
* EV_SET_PROJECTION this is what lets an app offer "tabs or
|
|
1300
|
+
* breadcrumb" as a live choice.
|
|
1301
|
+
************************************************************/
|
|
1302
|
+
function ac_set_chrome_depth(gobj, event, kw, src)
|
|
1303
|
+
{
|
|
1304
|
+
let depth = kw.chrome_depth;
|
|
1305
|
+
|
|
1306
|
+
if(typeof depth !== "number" || depth < -1 || Math.floor(depth) !== depth) {
|
|
1307
|
+
log_error(
|
|
1308
|
+
`${GCLASS_NAME} '${node_path_str(gobj)}': chrome_depth must be an ` +
|
|
1309
|
+
`integer >= 0 (or -1 to inherit) — got '${depth}'`
|
|
1310
|
+
);
|
|
1311
|
+
return -1;
|
|
1312
|
+
}
|
|
1313
|
+
gobj_write_attr(gobj, "chrome_depth", depth);
|
|
1314
|
+
|
|
1315
|
+
/* The depth is resolved along the ACTIVE PATH, so the node that
|
|
1316
|
+
* re-renders it is the one the shell is talking to: ask the tree
|
|
1317
|
+
* root to re-apply the current route rather than repainting only
|
|
1318
|
+
* from here, which would leave the ancestors' strips behind. */
|
|
1319
|
+
let shell = yui_shell_of(gobj);
|
|
1320
|
+
if(shell) {
|
|
1321
|
+
yui_shell_navigate(shell, gobj_read_attr(shell, "current_route") ||
|
|
1322
|
+
route_of(gobj), {replace: true});
|
|
1323
|
+
}
|
|
1324
|
+
return 0;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/************************************************************
|
|
1328
|
+
* Runtime API: change (or set) my content.
|
|
1329
|
+
************************************************************/
|
|
1330
|
+
function ac_set_content(gobj, event, kw, src)
|
|
1331
|
+
{
|
|
1332
|
+
let priv = gobj.priv;
|
|
1333
|
+
let errors = [];
|
|
1334
|
+
let probe = normalize_spec(
|
|
1335
|
+
{id: gobj_read_attr(gobj, "node_id"), content: kw.content},
|
|
1336
|
+
errors, `${node_path_str(gobj)}`
|
|
1337
|
+
);
|
|
1338
|
+
if(!probe) {
|
|
1339
|
+
for(let e of errors) {
|
|
1340
|
+
log_error(`${GCLASS_NAME}: ${e}`);
|
|
1341
|
+
}
|
|
1342
|
+
return -1;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
destroy_content(gobj);
|
|
1346
|
+
gobj_write_attr(gobj, "content", probe.content || {});
|
|
1347
|
+
repaint(gobj);
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/************************************************************
|
|
1352
|
+
* A descendant changed shape; keep bubbling to the root, which
|
|
1353
|
+
* owns the site-map contribution.
|
|
1354
|
+
************************************************************/
|
|
1355
|
+
function ac_tree_changed(gobj, event, kw, src)
|
|
1356
|
+
{
|
|
1357
|
+
notify_tree_changed(gobj);
|
|
1358
|
+
return 0;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
/***************************
|
|
1365
|
+
* Public API
|
|
1366
|
+
***************************/
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
/************************************************************
|
|
1372
|
+
* The runtime API. Every call is an event: the machine trace
|
|
1373
|
+
* IS the audit log of how the tree got its shape.
|
|
1374
|
+
************************************************************/
|
|
1375
|
+
function yui_node_add(node, spec, index)
|
|
1376
|
+
{
|
|
1377
|
+
return gobj_send_event(node, "EV_ADD_NODE", {spec: spec, index: index}, node);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function yui_node_remove(node, node_id)
|
|
1381
|
+
{
|
|
1382
|
+
return gobj_send_event(node, "EV_REMOVE_NODE", {node_id: node_id}, node);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function yui_node_set_projection(node, projection)
|
|
1386
|
+
{
|
|
1387
|
+
return gobj_send_event(node, "EV_SET_PROJECTION", {projection: projection}, node);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function yui_node_set_content(node, content)
|
|
1391
|
+
{
|
|
1392
|
+
return gobj_send_event(node, "EV_SET_CONTENT", {content: content}, node);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function yui_node_set_chrome_depth(node, depth)
|
|
1396
|
+
{
|
|
1397
|
+
return gobj_send_event(node, "EV_SET_CHROME_DEPTH",
|
|
1398
|
+
{chrome_depth: depth}, node);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/************************************************************
|
|
1402
|
+
* Resolve a path of ids from a node ("" = the node itself).
|
|
1403
|
+
* Returns null when any segment is missing — callers that mean
|
|
1404
|
+
* "the deepest living one" must say so.
|
|
1405
|
+
************************************************************/
|
|
1406
|
+
function yui_node_find(node, path)
|
|
1407
|
+
{
|
|
1408
|
+
let r = walk_path(node, path);
|
|
1409
|
+
if(r.missing.length) {
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
return r.node;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/************************************************************
|
|
1416
|
+
* Canonical route of a node — for a host building shortcuts
|
|
1417
|
+
* (toolbar quick links) without hardcoding paths.
|
|
1418
|
+
************************************************************/
|
|
1419
|
+
function yui_node_route(node)
|
|
1420
|
+
{
|
|
1421
|
+
return route_of(node);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/************************************************************
|
|
1425
|
+
* Version of the tree CONTRACT this node belongs to.
|
|
1426
|
+
*
|
|
1427
|
+
* There is deliberately NO reparent/move API: once published, a
|
|
1428
|
+
* node's path is a url someone may depend on, so the shape is a
|
|
1429
|
+
* versioned contract, not runtime state. Renames migrate
|
|
1430
|
+
* through `aliases`; anything a version bump cannot cover is a
|
|
1431
|
+
* new tree, declared as such.
|
|
1432
|
+
************************************************************/
|
|
1433
|
+
function yui_node_tree_version(node)
|
|
1434
|
+
{
|
|
1435
|
+
return gobj_read_attr(tree_root_of(node), "tree_version") || "";
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
/***************************************************************
|
|
1440
|
+
* FSM
|
|
1441
|
+
***************************************************************/
|
|
1442
|
+
/*---------------------------------------------*
|
|
1443
|
+
* Global methods table
|
|
1444
|
+
*---------------------------------------------*/
|
|
1445
|
+
const gmt = {
|
|
1446
|
+
mt_create: mt_create,
|
|
1447
|
+
mt_start: mt_start,
|
|
1448
|
+
mt_stop: mt_stop,
|
|
1449
|
+
mt_destroy: mt_destroy
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
/***************************************************************
|
|
1453
|
+
* Create the GClass
|
|
1454
|
+
***************************************************************/
|
|
1455
|
+
function create_gclass(gclass_name)
|
|
1456
|
+
{
|
|
1457
|
+
if(__gclass__) {
|
|
1458
|
+
log_error(`GClass ALREADY created: ${gclass_name}`);
|
|
1459
|
+
return -1;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/*---------------------------------------------*
|
|
1463
|
+
* States
|
|
1464
|
+
*
|
|
1465
|
+
* ST_OFF — not on the active path (nothing painted).
|
|
1466
|
+
* ST_SELF — I am the tip: my content and/or my index
|
|
1467
|
+
* projection of my children.
|
|
1468
|
+
* ST_CHILD — a child of mine is on the path: my chrome
|
|
1469
|
+
* projection plus that child's body.
|
|
1470
|
+
*
|
|
1471
|
+
* The mutation events are legal in every state on purpose:
|
|
1472
|
+
* the tree is dynamic, so a node can gain or lose children
|
|
1473
|
+
* while nobody is looking at it.
|
|
1474
|
+
*---------------------------------------------*/
|
|
1475
|
+
const states = [
|
|
1476
|
+
["ST_OFF", [
|
|
1477
|
+
["EV_ROUTE_CHANGED", ac_route_changed, null],
|
|
1478
|
+
["EV_ACTIVATE", ac_activate, null],
|
|
1479
|
+
["EV_ADD_NODE", ac_add_node, null],
|
|
1480
|
+
["EV_REMOVE_NODE", ac_remove_node, null],
|
|
1481
|
+
["EV_SET_PROJECTION", ac_set_projection, null],
|
|
1482
|
+
["EV_SET_CHROME_DEPTH", ac_set_chrome_depth, null],
|
|
1483
|
+
["EV_SET_CONTENT", ac_set_content, null],
|
|
1484
|
+
["EV_NODE_TREE_CHANGED", ac_tree_changed, null]
|
|
1485
|
+
]],
|
|
1486
|
+
["ST_SELF", [
|
|
1487
|
+
["EV_ROUTE_CHANGED", ac_route_changed, null],
|
|
1488
|
+
["EV_ACTIVATE", ac_activate, null],
|
|
1489
|
+
["EV_DEACTIVATE", ac_deactivate, null],
|
|
1490
|
+
["EV_NAV_CLICKED", ac_nav_clicked, null],
|
|
1491
|
+
["EV_ADD_NODE", ac_add_node, null],
|
|
1492
|
+
["EV_REMOVE_NODE", ac_remove_node, null],
|
|
1493
|
+
["EV_SET_PROJECTION", ac_set_projection, null],
|
|
1494
|
+
["EV_SET_CHROME_DEPTH", ac_set_chrome_depth, null],
|
|
1495
|
+
["EV_SET_CONTENT", ac_set_content, null],
|
|
1496
|
+
["EV_NODE_TREE_CHANGED", ac_tree_changed, null]
|
|
1497
|
+
]],
|
|
1498
|
+
["ST_CHILD", [
|
|
1499
|
+
["EV_ROUTE_CHANGED", ac_route_changed, null],
|
|
1500
|
+
["EV_ACTIVATE", ac_activate, null],
|
|
1501
|
+
["EV_DEACTIVATE", ac_deactivate, null],
|
|
1502
|
+
["EV_NAV_CLICKED", ac_nav_clicked, null],
|
|
1503
|
+
["EV_ADD_NODE", ac_add_node, null],
|
|
1504
|
+
["EV_REMOVE_NODE", ac_remove_node, null],
|
|
1505
|
+
["EV_SET_PROJECTION", ac_set_projection, null],
|
|
1506
|
+
["EV_SET_CHROME_DEPTH", ac_set_chrome_depth, null],
|
|
1507
|
+
["EV_SET_CONTENT", ac_set_content, null],
|
|
1508
|
+
["EV_NODE_TREE_CHANGED", ac_tree_changed, null]
|
|
1509
|
+
]]
|
|
1510
|
+
];
|
|
1511
|
+
|
|
1512
|
+
/*---------------------------------------------*
|
|
1513
|
+
* Events
|
|
1514
|
+
*---------------------------------------------*/
|
|
1515
|
+
const event_types = [
|
|
1516
|
+
["EV_ROUTE_CHANGED", 0],
|
|
1517
|
+
["EV_ACTIVATE", 0],
|
|
1518
|
+
["EV_DEACTIVATE", 0],
|
|
1519
|
+
["EV_NAV_CLICKED", 0],
|
|
1520
|
+
["EV_ADD_NODE", 0],
|
|
1521
|
+
["EV_REMOVE_NODE", 0],
|
|
1522
|
+
["EV_SET_PROJECTION", 0],
|
|
1523
|
+
["EV_SET_CHROME_DEPTH", 0],
|
|
1524
|
+
["EV_SET_CONTENT", 0],
|
|
1525
|
+
["EV_NODE_TREE_CHANGED", 0]
|
|
1526
|
+
];
|
|
1527
|
+
|
|
1528
|
+
__gclass__ = gclass_create(
|
|
1529
|
+
gclass_name,
|
|
1530
|
+
event_types,
|
|
1531
|
+
states,
|
|
1532
|
+
gmt,
|
|
1533
|
+
0, // lmt,
|
|
1534
|
+
attrs_table,
|
|
1535
|
+
PRIVATE_DATA,
|
|
1536
|
+
0, // authz_table,
|
|
1537
|
+
0, // command_table,
|
|
1538
|
+
0, // s_user_trace_level
|
|
1539
|
+
0 // gclass_flag
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
if(!__gclass__) {
|
|
1543
|
+
return -1;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return 0;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/***************************************************************
|
|
1550
|
+
* Register GClass
|
|
1551
|
+
***************************************************************/
|
|
1552
|
+
function register_c_yui_node()
|
|
1553
|
+
{
|
|
1554
|
+
return create_gclass(GCLASS_NAME);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export {
|
|
1558
|
+
register_c_yui_node,
|
|
1559
|
+
yui_node_add,
|
|
1560
|
+
yui_node_remove,
|
|
1561
|
+
yui_node_set_projection,
|
|
1562
|
+
yui_node_set_content,
|
|
1563
|
+
yui_node_set_chrome_depth,
|
|
1564
|
+
yui_node_find,
|
|
1565
|
+
yui_node_route,
|
|
1566
|
+
yui_node_tree_version
|
|
1567
|
+
};
|