@helping-ai-workflow/md2doc 3.5.0 → 3.6.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/lib/editor/client.js +44 -5
- package/lib/editor/server.js +21 -2
- package/lib/editor/wave-codec.js +160 -3
- package/lib/editor/wave-draw.js +1470 -0
- package/lib/editor/wave-geometry.js +469 -19
- package/lib/editor/wave-panels.js +1102 -0
- package/lib/editor/wave-store.js +36 -1
- package/lib/editor/wave-ui.js +1807 -1802
- package/lib/md2doc.js +520 -50
- package/package.json +2 -2
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wave-panels — the toolbar, the right-hand panels (meta fields, the edge
|
|
5
|
+
* inspector) and the lane rail. Moved out of `wave-ui.js`, behaviour
|
|
6
|
+
* unchanged line for line — this module is a pure move, not a rewrite.
|
|
7
|
+
*
|
|
8
|
+
* Plain CommonJS, like `wave-codec.js` / `wave-geometry.js` / `wave-store.js`
|
|
9
|
+
* / `wave-draw.js` next to it, and for the identical reason `wave-draw.js`'s
|
|
10
|
+
* own header already gives: `wave-ui.js` is injected into the edit page as a
|
|
11
|
+
* bare `<script>` with no `require()` of its own, so a UMD header here would
|
|
12
|
+
* be reaching for a `require` that is undefined in the browser. `wave-ui.js`
|
|
13
|
+
* takes this module in as `opts.panels` (see `lib/editor/server.js`'s shim
|
|
14
|
+
* and `lib/editor/client.js`'s `openWaveEditor`), the same way it already
|
|
15
|
+
* takes `opts.draw`.
|
|
16
|
+
*
|
|
17
|
+
* This layer does not require `wave-draw.js` — the dependency direction
|
|
18
|
+
* between the two extracted layers is one-way, and neither needs the other:
|
|
19
|
+
* anything both would need travels through `wave-ui.js` or through
|
|
20
|
+
* `wave-geometry.js`.
|
|
21
|
+
*
|
|
22
|
+
* `createPanels(deps)` is called from `wave-ui.js` right after `panel`
|
|
23
|
+
* exists, which is BEFORE several of the DOM elements the returned paint
|
|
24
|
+
* functions need (`metaBar`'s own input fields, the edge inspector's
|
|
25
|
+
* `shapeSel`/`edgeLabelInput`, the lane rail's `laneCol`, the preview's
|
|
26
|
+
* `previewHost`/`previewWrap`, the status line's `status`) exist. Rather
|
|
27
|
+
* than reorder any of `wave-ui.js`'s original DOM-scaffolding statements to
|
|
28
|
+
* satisfy this factory's own call site — the brief this module was built
|
|
29
|
+
* from forbids reordering, and scaffolding order is what keeps the
|
|
30
|
+
* toolbar's visual layout unchanged — `deps` is a single object
|
|
31
|
+
* `wave-ui.js` keeps a reference to and PUBLISHES more fields onto as it
|
|
32
|
+
* creates each element, in their original unchanged position. The functions
|
|
33
|
+
* below that need one of those later fields read it off `deps` directly at
|
|
34
|
+
* their own call time (`deps.laneCol`, `deps.edgeBar`, …) rather than
|
|
35
|
+
* caching it into a local at the top of this factory — by the time any of
|
|
36
|
+
* them actually runs (the first `render()`, or a click on an
|
|
37
|
+
* already-mounted control), every field they read has long since been
|
|
38
|
+
* published. Only what is genuinely ready the moment `createPanels()` runs
|
|
39
|
+
* (`d`, `geometry`, `codec`, `SIZES`, `actions`, `overlay`, `panel`, the
|
|
40
|
+
* preview's id constants, the wavedrom engine reference) is cached into a
|
|
41
|
+
* local up front.
|
|
42
|
+
*
|
|
43
|
+
* v3.6.0 Task 12: `toolButton()` used to always append into a single
|
|
44
|
+
* `cycleBar` div cached from `deps.cycleBar` — the toolbar's only button
|
|
45
|
+
* row at the time. Now that the toolbar is eight captioned
|
|
46
|
+
* `.ed-wave-tool-group` sections (`toolGroup()`, below), `toolButton()`
|
|
47
|
+
* takes the target group as its last argument instead, and `deps` no
|
|
48
|
+
* longer carries `cycleBar` at all — nothing in this file needs a single
|
|
49
|
+
* shared bar to exist before `createPanels()` runs anymore.
|
|
50
|
+
*
|
|
51
|
+
* `actions` is the callback bag for the handful of `wave-ui.js`-owned pieces
|
|
52
|
+
* of state a moved function still has to read or write, now that it is no
|
|
53
|
+
* longer a closure over `wave-ui.js`'s own variables:
|
|
54
|
+
*
|
|
55
|
+
* commit(name, fn) run one gesture (constraint 5 in wave-ui.js's
|
|
56
|
+
* own header)
|
|
57
|
+
* render() repaint the whole dialog
|
|
58
|
+
* selectionSet(v) write-only: `laneRow`'s own click handler
|
|
59
|
+
* writes the one selection this dialog has —
|
|
60
|
+
* `{laneIndex, laneTo, from, to}`, a lane
|
|
61
|
+
* range plus the cycle range on its first
|
|
62
|
+
* lane. Reads go through `state.selection`
|
|
63
|
+
* instead, since every one of them happens
|
|
64
|
+
* synchronously inside a `renderLanes` call
|
|
65
|
+
* and a getter would be one more indirection
|
|
66
|
+
* over the value `state` already carries
|
|
67
|
+
* focusOverrideGet()/Set(key) the one-shot "where should the keyboard land
|
|
68
|
+
* after this repaint" override — read AND
|
|
69
|
+
* written from inside `renameGroup`'s deferred
|
|
70
|
+
* `finish`/`retire`/`cancel` closures, so both
|
|
71
|
+
* halves are needed, not only the setter a
|
|
72
|
+
* write-only caller like `laneRow`'s drop
|
|
73
|
+
* handler uses
|
|
74
|
+
* groupRenameGet()/Set(v) the rail's one open rename session — a mutex
|
|
75
|
+
* `renameGroup`'s own deferred closures test
|
|
76
|
+
* and clear over time, and `wave-ui.js`'s
|
|
77
|
+
* `render()` also reads directly (unchanged,
|
|
78
|
+
* since `render()` itself did not move) to
|
|
79
|
+
* decide whether to retire it before a repaint
|
|
80
|
+
*
|
|
81
|
+
* `close` is not in `actions`: no function that moved here calls it (checked
|
|
82
|
+
* against every line in this file, not assumed).
|
|
83
|
+
*
|
|
84
|
+
* `commitBanner` is exported despite being internal to the eight meta-field
|
|
85
|
+
* change listeners that are its only callers — a deviation from this task's
|
|
86
|
+
* own brief, which called it an internal helper. Those eight listeners stay
|
|
87
|
+
* in `wave-ui.js`, at their original position, because the input elements
|
|
88
|
+
* they close over (`headInput`, `footInput`, …) are `wave-ui.js` locals a
|
|
89
|
+
* caller needs to go on holding for `deps.headInput` etc. below to work; the
|
|
90
|
+
* alternative — building those eight fields and their listeners inside this
|
|
91
|
+
* factory instead, so `commitBanner` could stay private — would mean
|
|
92
|
+
* reconstructing `wave-ui.js`'s own scaffolding order inside this file,
|
|
93
|
+
* which is the reordering this whole factory is built to avoid. Exporting
|
|
94
|
+
* one more trivial function was the smaller change.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
function createPanels(deps) {
|
|
98
|
+
const d = deps.d;
|
|
99
|
+
const geometry = deps.geometry;
|
|
100
|
+
const codec = deps.codec;
|
|
101
|
+
const SIZES = deps.SIZES;
|
|
102
|
+
const actions = deps.actions;
|
|
103
|
+
const overlay = deps.overlay;
|
|
104
|
+
const panel = deps.panel;
|
|
105
|
+
const PREVIEW_INDEX = deps.PREVIEW_INDEX;
|
|
106
|
+
const PREVIEW_ID_PREFIX = deps.PREVIEW_ID_PREFIX;
|
|
107
|
+
const wavedrom = deps.wavedrom;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A plain, unfrozen copy of a document.
|
|
111
|
+
*
|
|
112
|
+
* The store's documents are deep-frozen — that is what stops a stray write
|
|
113
|
+
* from corrupting an undo entry — and wavedrom's renderer WRITES INTO the
|
|
114
|
+
* source object it is handed. Under this file's strict prologue that is a
|
|
115
|
+
* throw, so the preview gets its own copy every time. Bounded by the codec's
|
|
116
|
+
* own nesting ceiling so a pathological document cannot run the stack out
|
|
117
|
+
* here either.
|
|
118
|
+
*/
|
|
119
|
+
function plainCopy(value, depth) {
|
|
120
|
+
if (depth > 64 || value === null || typeof value !== 'object') return value;
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (let i = 0; i < value.length; i++) out.push(plainCopy(value[i], depth + 1));
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], depth + 1);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* v3.6.0 Task 12: one captioned toolbar section — a `.ed-wave-tool-group`
|
|
133
|
+
* with a `.ed-wave-tool-caption` at the front.
|
|
134
|
+
*
|
|
135
|
+
* The caption sits to the LEFT of the section's buttons, not above them:
|
|
136
|
+
* the toolbar is a horizontal bar, and a caption row above each group
|
|
137
|
+
* would make it two lines tall on top of a row that already wraps — the
|
|
138
|
+
* 電位 group's 22 brushes alone wrap to two rows at this dialog's fixed
|
|
139
|
+
* viewport (MEASURED, Task 10: 22 × 38px against a 728px container).
|
|
140
|
+
* Vertical space is what a tall diagram (60+ cycles) is already short of,
|
|
141
|
+
* so this reorganisation may not spend more of it.
|
|
142
|
+
*/
|
|
143
|
+
function toolGroup(caption) {
|
|
144
|
+
const g = d.createElement('div');
|
|
145
|
+
g.className = 'ed-wave-tool-group';
|
|
146
|
+
const cap = d.createElement('span');
|
|
147
|
+
cap.className = 'ed-wave-tool-caption';
|
|
148
|
+
cap.textContent = caption;
|
|
149
|
+
g.appendChild(cap);
|
|
150
|
+
return g;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `group` (last arg, required) is the `.ed-wave-tool-group` this button
|
|
154
|
+
// belongs to — see `toolGroup()` above and this module's own header for
|
|
155
|
+
// why there is no longer a single shared bar this could default to.
|
|
156
|
+
function toolButton(cls, label, hint, fn, group) {
|
|
157
|
+
const b = d.createElement('button');
|
|
158
|
+
b.type = 'button';
|
|
159
|
+
b.className = cls;
|
|
160
|
+
b.setAttribute('data-focus-key', cls);
|
|
161
|
+
b.textContent = label;
|
|
162
|
+
b.title = hint;
|
|
163
|
+
b.addEventListener('click', fn);
|
|
164
|
+
group.appendChild(b);
|
|
165
|
+
return b;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* v3.6.0 Task 13: one collapsible right-hand panel — a `.ed-wave-section`
|
|
170
|
+
* with its own `.ed-wave-section-title` header button and a
|
|
171
|
+
* `.ed-wave-section-body` the header toggles.
|
|
172
|
+
*
|
|
173
|
+
* `open` seeds `data-open` — the section's expanded state — but that state
|
|
174
|
+
* lives ONLY on the attribute for the life of this one editor session; it
|
|
175
|
+
* is never read back out of, or written into, the document. A session that
|
|
176
|
+
* opens with 關聯線/文件/Lane expanded and 預覽/原始碼 collapsed always
|
|
177
|
+
* starts that way again next time, by design: the three that default open
|
|
178
|
+
* are the ones a gesture on the canvas immediately needs (an edge just
|
|
179
|
+
* got selected, a lane just got selected, the ruler fields), and the two
|
|
180
|
+
* that default closed are read-only references a user opens on demand
|
|
181
|
+
* rather than panels they edit through.
|
|
182
|
+
*
|
|
183
|
+
* Returns `{el, body}` — the section element to place in the DOM, and the
|
|
184
|
+
* body div a caller fills in. Kept separate rather than handing back only
|
|
185
|
+
* `el` and making every caller `querySelector('.ed-wave-section-body')`
|
|
186
|
+
* back out of it: every call site already knows which body it wants the
|
|
187
|
+
* instant it asks for the section.
|
|
188
|
+
*/
|
|
189
|
+
function section(key, title, open) {
|
|
190
|
+
const s = d.createElement('section');
|
|
191
|
+
s.className = 'ed-wave-section';
|
|
192
|
+
s.setAttribute('data-section', key);
|
|
193
|
+
if (open) s.setAttribute('data-open', '');
|
|
194
|
+
const h = d.createElement('button');
|
|
195
|
+
h.type = 'button';
|
|
196
|
+
h.className = 'ed-wave-section-title';
|
|
197
|
+
h.setAttribute('data-focus-key', 'section-' + key);
|
|
198
|
+
h.textContent = title;
|
|
199
|
+
h.addEventListener('click', function () {
|
|
200
|
+
// v3.6.0 Task 13: under the <1100px collapse (md2doc.js's own media
|
|
201
|
+
// query), the rail's section titles are the only thing still
|
|
202
|
+
// visible/clickable — without this, a narrow-viewport user could
|
|
203
|
+
// toggle `data-open` all day and never see a body, because
|
|
204
|
+
// `.ed-wave-side:not([data-expanded]) .ed-wave-section-body` stays
|
|
205
|
+
// `display:none` regardless. Harmless at a wide viewport: nothing
|
|
206
|
+
// there matches `[data-expanded]` outside that media query, so this
|
|
207
|
+
// is a no-op attribute there.
|
|
208
|
+
//
|
|
209
|
+
// The FIRST click while still collapsed only reveals the rail and
|
|
210
|
+
// does not also toggle the section it landed on: 關聯線/文件/Lane
|
|
211
|
+
// all default `data-open`, so a plain toggle here would make that
|
|
212
|
+
// very first click look like "the user just closed the panel they
|
|
213
|
+
// pressed" the instant the rail became wide enough to show it.
|
|
214
|
+
const wasExpanded = side.hasAttribute('data-expanded');
|
|
215
|
+
side.setAttribute('data-expanded', '');
|
|
216
|
+
if (!wasExpanded) return;
|
|
217
|
+
if (s.hasAttribute('data-open')) s.removeAttribute('data-open');
|
|
218
|
+
else s.setAttribute('data-open', '');
|
|
219
|
+
});
|
|
220
|
+
s.appendChild(h);
|
|
221
|
+
const body = d.createElement('div');
|
|
222
|
+
body.className = 'ed-wave-section-body';
|
|
223
|
+
s.appendChild(body);
|
|
224
|
+
return { el: s, body: body };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// v3.6.0 Task 13: the whole right-hand column, built eagerly here rather
|
|
228
|
+
// than incrementally published onto `deps` the way `metaBar`/`edgeBar`/
|
|
229
|
+
// `laneCol`/`previewWrap`/`status` are (see this module's own header for
|
|
230
|
+
// why THOSE are lazy): every section body a caller could want already
|
|
231
|
+
// exists the instant `createPanels()` returns, because `section()` above
|
|
232
|
+
// needs nothing but `d`. `wave-ui.js` appends `panels.side` into `.ed-wave-
|
|
233
|
+
// body` once, in the position the old `.ed-wave-preview` column used to
|
|
234
|
+
// occupy, and targets `panels.sections.<key>` for everything that used to
|
|
235
|
+
// go into `metaBar`/`edgeBar`/`previewWrap` directly.
|
|
236
|
+
//
|
|
237
|
+
// Order is the fixed, tested order: edge, document, lane, preview, source.
|
|
238
|
+
// Open by default: edge/document/lane (a gesture on the canvas — selecting
|
|
239
|
+
// an edge, selecting a lane, typing a ruler field — has to see its own
|
|
240
|
+
// panel without an extra click). Closed by default: preview/source (the
|
|
241
|
+
// engine's own picture and the exact bytes a save would write are both
|
|
242
|
+
// references a user consults on demand, not panels edited through).
|
|
243
|
+
const side = d.createElement('div');
|
|
244
|
+
side.className = 'ed-wave-side';
|
|
245
|
+
|
|
246
|
+
// v3.6.0 Task 13 fix round 1: a way back. Expanding the rail (a click on
|
|
247
|
+
// any section title, below) used to be a one-way door under the <1100px
|
|
248
|
+
// collapse — nothing ever removed `data-expanded` again, so the first
|
|
249
|
+
// click at a narrow viewport permanently forfeited canvas space for the
|
|
250
|
+
// rest of the session, which works directly against this whole version's
|
|
251
|
+
// own goal. `display: none` outside `[data-expanded]` (md2doc.js's CSS)
|
|
252
|
+
// keeps this invisible and out of the tab order everywhere else — it only
|
|
253
|
+
// ever matters in the one state it exists for.
|
|
254
|
+
const collapseBtn = d.createElement('button');
|
|
255
|
+
collapseBtn.type = 'button';
|
|
256
|
+
collapseBtn.className = 'ed-wave-side-collapse';
|
|
257
|
+
collapseBtn.setAttribute('data-focus-key', 'side-collapse');
|
|
258
|
+
collapseBtn.title = '收合右欄';
|
|
259
|
+
collapseBtn.textContent = '» 收合';
|
|
260
|
+
collapseBtn.addEventListener('click', function () {
|
|
261
|
+
side.removeAttribute('data-expanded');
|
|
262
|
+
});
|
|
263
|
+
side.appendChild(collapseBtn);
|
|
264
|
+
|
|
265
|
+
const sections = {};
|
|
266
|
+
for (const def of [
|
|
267
|
+
['edge', '關聯線', true],
|
|
268
|
+
['document', '文件', true],
|
|
269
|
+
['lane', 'Lane', true],
|
|
270
|
+
['preview', 'WaveDrom 預覽', false],
|
|
271
|
+
['source', '原始碼', false],
|
|
272
|
+
]) {
|
|
273
|
+
const built = section(def[0], def[1], def[2]);
|
|
274
|
+
side.appendChild(built.el);
|
|
275
|
+
sections[def[0]] = built.body;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* v3.6.0 Task 13 fix round 3: expand the rail programmatically — the SAME
|
|
280
|
+
* `data-expanded` attribute a section title's first click (`section()`
|
|
281
|
+
* above) and the collapse button's own opposite (`collapseBtn` above)
|
|
282
|
+
* already use, called instead from `wave-ui.js`'s `render()` on the one
|
|
283
|
+
* transition that needs it without a click at all: selecting an edge at
|
|
284
|
+
* a narrow viewport, whose only feedback surface is a section this attribute
|
|
285
|
+
* governs. A no-op outside the `<1100px` media query, same as those two
|
|
286
|
+
* call sites — nothing there matches `[data-expanded]` at a wider one.
|
|
287
|
+
*/
|
|
288
|
+
function expandSide() {
|
|
289
|
+
side.setAttribute('data-expanded', '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// v3.6.0 Task 13: `container` is the section body a field belongs in —
|
|
293
|
+
// explicit at the call site, the same reasoning `toolButton`'s own
|
|
294
|
+
// trailing `group` argument (Task 12) already gives for why there is no
|
|
295
|
+
// default to fall back on now that this dialog has more than one place a
|
|
296
|
+
// field could land.
|
|
297
|
+
function metaField(cls, label, placeholder, field, container) {
|
|
298
|
+
const wrap = d.createElement('label');
|
|
299
|
+
wrap.className = 'ed-wave-metafield';
|
|
300
|
+
const text = d.createElement('span');
|
|
301
|
+
text.textContent = label;
|
|
302
|
+
wrap.appendChild(text);
|
|
303
|
+
const input = d.createElement('input');
|
|
304
|
+
input.type = 'text';
|
|
305
|
+
input.className = cls;
|
|
306
|
+
input.setAttribute('data-focus-key', cls);
|
|
307
|
+
input.setAttribute('data-field', field);
|
|
308
|
+
input.placeholder = placeholder;
|
|
309
|
+
wrap.appendChild(input);
|
|
310
|
+
container.appendChild(wrap);
|
|
311
|
+
return input;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A numeral typed into `hscale`/`period`/`phase` is committed as a real
|
|
316
|
+
* JS `number`, not as the string the `<input>` holds — unlike the eight
|
|
317
|
+
* banner fields' `commitBanner`, which always writes a string and is
|
|
318
|
+
* correct to (see its own header comment: the engine's `captext` and
|
|
319
|
+
* `ticktock` both accept a plain string). These three are different:
|
|
320
|
+
* `wave-geometry.hscaleOf`/`layoutOf` gate on `typeof … === 'number'`, so
|
|
321
|
+
* a STRING `"2"` would silently read as "unset" on THIS canvas — this
|
|
322
|
+
* batch's own default of 1/0 — while the pinned engine's own preview still
|
|
323
|
+
* honours it (its `tonumber()` coerces a string with `>`). That is the
|
|
324
|
+
* exact same-idea-two-definitions divergence this batch has already paid
|
|
325
|
+
* for three times elsewhere (see `md2doc`'s CLAUDE.md). Blank clears the
|
|
326
|
+
* field (the `undefined` sentinel `setConfigField`/`setLaneField` both
|
|
327
|
+
* read); anything that is not a plain, trimmed number is left as the raw
|
|
328
|
+
* string — both this canvas's `typeof` gate and the engine's own
|
|
329
|
+
* `tonumber()` already fall back to the SAME default (1 for hscale/period,
|
|
330
|
+
* 0 for phase) for a non-numeric value, so there is nothing to reconcile
|
|
331
|
+
* for that case, only for a well-formed one.
|
|
332
|
+
*/
|
|
333
|
+
function parseNumericField(value) {
|
|
334
|
+
if (value === '') return undefined;
|
|
335
|
+
return /^-?\d+(\.\d+)?$/.test(value.trim()) ? Number(value) : value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function commitConfigField(key, value) {
|
|
339
|
+
actions.commit('config.' + key, function (doc) {
|
|
340
|
+
return codec.setConfigField(doc, key, parseNumericField(value));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function commitLaneField(at, key, value) {
|
|
345
|
+
actions.commit('lane.' + key, function (doc) {
|
|
346
|
+
return codec.setLaneField(doc, at, key, parseNumericField(value));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// v3.5.0 Task 13: widened from `(which, value)` — always writing `.text`
|
|
351
|
+
// — to `(which, key, value)` so the same function also carries the other
|
|
352
|
+
// five ruler fields.
|
|
353
|
+
//
|
|
354
|
+
// Fix round 2: the write itself moved into `codec.setBannerField` rather
|
|
355
|
+
// than staying inline here. The inline version's own no-op guard —
|
|
356
|
+
// `value === '' && (hadObj[key] === undefined || hadObj[key] === null)`
|
|
357
|
+
// — only refused to WRITE when the key had never been set; once a key
|
|
358
|
+
// WAS set, clearing the field fell through to `next[which][key] = ''`,
|
|
359
|
+
// an empty string rather than an absent key. `.text` hid that: the
|
|
360
|
+
// engine's `captext` treats `''` the same as absent (a plain truthy
|
|
361
|
+
// check). `tick`/`tock`/`every` do not — `ticktock` in
|
|
362
|
+
// `node_modules/wavedrom/lib/render-marks.js` only skips a field that is
|
|
363
|
+
// `=== undefined`, so `''` reached its array branch, `Number('')` is
|
|
364
|
+
// `0`, and the engine drew a full default 0-based ruler on a field that
|
|
365
|
+
// LOOKED empty on screen — the exact confident-wrong-state class this
|
|
366
|
+
// file exists to avoid, reintroduced by the write side after the read
|
|
367
|
+
// side (`tickTockText`) got it right. `setBannerField` deletes the key
|
|
368
|
+
// (and the parent object, if that empties it) instead of writing `''`,
|
|
369
|
+
// closing that gap once for all six fields rather than special-casing
|
|
370
|
+
// `.text`'s accidental safety.
|
|
371
|
+
//
|
|
372
|
+
// This also gives these six fields their first test seam:
|
|
373
|
+
// `commitBanner` itself is a closure with no unit-test access, but
|
|
374
|
+
// `setBannerField` is a plain, exported, pure function — pinned in
|
|
375
|
+
// `test/wave-codec.test.js`.
|
|
376
|
+
function commitBanner(which, key, value) {
|
|
377
|
+
actions.commit(which + '.' + key, function (doc) { return codec.setBannerField(doc, which, key, value); });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function say(text) {
|
|
381
|
+
deps.status.textContent = text;
|
|
382
|
+
overlay.setAttribute('data-wave-status', text);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Show or hide the inspector row, and — when it is showing — fill it in
|
|
387
|
+
* from the selected edge's own entry.
|
|
388
|
+
*
|
|
389
|
+
* The ONLY thing that may set `edgeBar.hidden`, so "is an edge selected"
|
|
390
|
+
* and "is there a box on screen" cannot come apart: precondition T1 at
|
|
391
|
+
* the top of this file's docstring for the overlay itself, applied to
|
|
392
|
+
* this row. Called only from `render()`, after `selectedEdge` has already
|
|
393
|
+
* been clamped, so the `codec.parseEdge` below is not expected to fail —
|
|
394
|
+
* it is checked anyway because "expected to" is not "guaranteed to".
|
|
395
|
+
*/
|
|
396
|
+
function renderEdgeBar(doc, state) {
|
|
397
|
+
const selectedEdge = state.selectedEdge;
|
|
398
|
+
overlay.setAttribute('data-wave-selected-edge',
|
|
399
|
+
selectedEdge === null ? '' : String(selectedEdge));
|
|
400
|
+
deps.edgeBar.hidden = selectedEdge === null;
|
|
401
|
+
if (selectedEdge === null) return;
|
|
402
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
403
|
+
if (cur === null) { deps.edgeBar.hidden = true; return; }
|
|
404
|
+
deps.shapeSel.value = cur.shape;
|
|
405
|
+
deps.edgeLabelInput.value = cur.label;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* `tick`/`every`/`tock` are read by the pinned engine as a number, a
|
|
410
|
+
* string, or an array of either (`ticktock` in
|
|
411
|
+
* `node_modules/wavedrom/lib/render-marks.js`) — unlike `head`/`foot`'s
|
|
412
|
+
* own `.text`, which is always a plain string. A text `<input>` can only
|
|
413
|
+
* hold one shape, so a hand-authored numeric or array value is shown in
|
|
414
|
+
* its string form (an array space-joined, the same shape the engine's
|
|
415
|
+
* own `val.trim().split(/\s+/)` turns a plain string back into) rather
|
|
416
|
+
* than blanked out — blanking a field that actually holds `tick: 0`
|
|
417
|
+
* would show a confident wrong state, the same failure `renderUnmodelled`
|
|
418
|
+
* exists to avoid elsewhere in this file. Editing the field always
|
|
419
|
+
* commits a plain string back, which the engine treats identically to
|
|
420
|
+
* whichever shape it replaced.
|
|
421
|
+
*/
|
|
422
|
+
function tickTockText(v) {
|
|
423
|
+
if (typeof v === 'string') return v;
|
|
424
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
425
|
+
if (Array.isArray(v)) return v.join(' ');
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function renderMeta(doc) {
|
|
430
|
+
const h = doc.head;
|
|
431
|
+
const f = doc.foot;
|
|
432
|
+
deps.headInput.value = (h !== null && typeof h === 'object' && typeof h.text === 'string')
|
|
433
|
+
? h.text : '';
|
|
434
|
+
deps.footInput.value = (f !== null && typeof f === 'object' && typeof f.text === 'string')
|
|
435
|
+
? f.text : '';
|
|
436
|
+
deps.headTickInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tick) : '';
|
|
437
|
+
deps.headEveryInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.every) : '';
|
|
438
|
+
deps.footTockInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tock) : '';
|
|
439
|
+
deps.headTockInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tock) : '';
|
|
440
|
+
deps.footTickInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tick) : '';
|
|
441
|
+
deps.footEveryInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.every) : '';
|
|
442
|
+
// v3.6.0 Task 13: the document panel's ninth field. `config.hscale` is
|
|
443
|
+
// meant to hold a number (see `commitConfigField`'s own comment), but a
|
|
444
|
+
// hand-authored document can carry anything — `tickTockText` already
|
|
445
|
+
// does the identical "show it as a string either way" job for the six
|
|
446
|
+
// ruler fields, so it is reused here rather than a near-duplicate.
|
|
447
|
+
const cfg = doc.config;
|
|
448
|
+
deps.hscaleInput.value = (cfg !== null && typeof cfg === 'object')
|
|
449
|
+
? tickTockText(cfg.hscale) : '';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The Lane panel's two fields — `period`/`phase` for whichever lane the
|
|
454
|
+
* CYCLE selection currently names (`state.selection.laneIndex`, the same
|
|
455
|
+
* mark the brush/cycle toolbar already acts on — there is no separate
|
|
456
|
+
* "selected lane" concept in this dialog). Disabled, and blank, whenever
|
|
457
|
+
* nothing is selected: a control a gesture cannot currently reach is
|
|
458
|
+
* disabled rather than left to silently do nothing, the same rule
|
|
459
|
+
* `groupBtn`'s own disabled state already follows in `wave-ui.js`.
|
|
460
|
+
*/
|
|
461
|
+
function renderLaneField(doc, layout, state) {
|
|
462
|
+
const sel = state.selection;
|
|
463
|
+
const row = (sel !== null) ? layout.lanes[sel.laneIndex] : undefined;
|
|
464
|
+
if (row === undefined) {
|
|
465
|
+
deps.lanePeriodInput.disabled = true;
|
|
466
|
+
deps.lanePhaseInput.disabled = true;
|
|
467
|
+
deps.lanePeriodInput.value = '';
|
|
468
|
+
deps.lanePhaseInput.value = '';
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
deps.lanePeriodInput.disabled = false;
|
|
472
|
+
deps.lanePhaseInput.disabled = false;
|
|
473
|
+
const lane = (row.lane !== null && typeof row.lane === 'object') ? row.lane : {};
|
|
474
|
+
deps.lanePeriodInput.value = tickTockText(lane.period);
|
|
475
|
+
deps.lanePhaseInput.value = tickTockText(lane.phase);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The 原始碼 panel: `writeBack(originalText, doc).text` — the actual bytes
|
|
480
|
+
* a save would write — not a re-serialisation made for display. Reads
|
|
481
|
+
* `actions.sourceText()`, which walks the SAME `store.toPatch()` path
|
|
482
|
+
* `afterStoreMoved` already runs after every gesture (`wave-ui.js`), so
|
|
483
|
+
* this panel can never show a different answer than what 保留並關閉 would
|
|
484
|
+
* actually write.
|
|
485
|
+
*/
|
|
486
|
+
function renderSource() {
|
|
487
|
+
const pre = d.createElement('pre');
|
|
488
|
+
pre.className = 'ed-wave-source';
|
|
489
|
+
pre.textContent = actions.sourceText();
|
|
490
|
+
sections.source.replaceChildren(pre);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Say out loud which properties of this document the drawing is not
|
|
495
|
+
* showing.
|
|
496
|
+
*
|
|
497
|
+
* v3.6.0 Task 18: `config.hscale` and a lane's `period` / `phase` used to
|
|
498
|
+
* head this list — three properties that change what the engine paints and
|
|
499
|
+
* that the drawing ignored. Task 4 made all three part of `layoutOf`'s
|
|
500
|
+
* per-lane geometry (`cycleWidth` carries `period * hscale`, `originX`
|
|
501
|
+
* carries `phase`), and `test/wave-draw.test.js`'s `period: 4` and
|
|
502
|
+
* `hscale: 2` fixtures pin the ruler and grid pitch against the engine's
|
|
503
|
+
* own, so naming them as "not shown" is now the false statement. They are
|
|
504
|
+
* gone from the list; `found` stays as the place the NEXT such property
|
|
505
|
+
* lands, and is empty today.
|
|
506
|
+
*
|
|
507
|
+
* The brief for this task asked instead for a notice when the skin ratios
|
|
508
|
+
* could not be derived (`SKIN_METRICS.derived !== true`). That field does
|
|
509
|
+
* not exist: controller ruling R12 (Task 3) cancelled the module-load
|
|
510
|
+
* derivation, because the shipped `wavedrom.min.js` attaches no `waveSkin`
|
|
511
|
+
* to the page global, so a page-time derivation would read `undefined`
|
|
512
|
+
* every time and the notice would be permanent wallpaper rather than a
|
|
513
|
+
* signal. The ratios are pinned from a measurement, and
|
|
514
|
+
* `test/wave-geometry.test.js` re-derives them from the real package on
|
|
515
|
+
* every run — a wavedrom bump that moves the skin turns that test red and
|
|
516
|
+
* names the number to re-pin. That is where the failure is reported,
|
|
517
|
+
* because it is a build-time fact and there is no runtime state for a
|
|
518
|
+
* notice to read.
|
|
519
|
+
*
|
|
520
|
+
* Edges join this same notice for the opposite reason. As of this task
|
|
521
|
+
* they ARE drawn — `renderEdges` below — so the claim for them is not "the
|
|
522
|
+
* drawing ignores this property" but "this specific edge cannot be
|
|
523
|
+
* placed".
|
|
524
|
+
*
|
|
525
|
+
* final review finding 3: this used to re-derive only ONE of
|
|
526
|
+
* `geometry.edgeLayout`'s three skip reasons (the dangling-letter one) by
|
|
527
|
+
* walking `doc.edge` a second time with its own copy of that check —
|
|
528
|
+
* which is also the one case the pinned engine agrees with us on
|
|
529
|
+
* (measured: an edge naming an absent letter draws nothing there either).
|
|
530
|
+
* The other two — an unparseable shape, and a `node` index that has
|
|
531
|
+
* outlived its lane's length (reachable from the editor's own
|
|
532
|
+
* insert/delete-cycles buttons, since neither reindexes `node`) — are
|
|
533
|
+
* exactly where the engine DOES draw a line and we silently do not,
|
|
534
|
+
* which is the one divergence this whole overlay exists to prevent, and
|
|
535
|
+
* the old code said nothing about either.
|
|
536
|
+
*
|
|
537
|
+
* So this asks `geometry.edgeLayoutDetail` — the SAME walk `renderEdges`
|
|
538
|
+
* draws from — which edges it could not place and why, instead of
|
|
539
|
+
* re-deriving any check of its own. Skips are bucketed by whatever
|
|
540
|
+
* `reason` string comes back, not by a hardcoded list of the two or three
|
|
541
|
+
* reasons known today: a fourth `continue` added to `edgeLayoutDetail`
|
|
542
|
+
* later shows up here as its own bucket (worded generically if
|
|
543
|
+
* `REASON_TEXT` has not been taught a nicer sentence for it yet) with no
|
|
544
|
+
* change needed in this function, rather than escaping the notice the way
|
|
545
|
+
* the third one did until now.
|
|
546
|
+
*/
|
|
547
|
+
function renderUnmodelled(doc, layout) {
|
|
548
|
+
// Empty today — see the block comment above for what used to be pushed
|
|
549
|
+
// here and why it no longer is. Still assembled (and still published as
|
|
550
|
+
// `data-wave-unmodelled`) so the next genuinely-unmodelled property has
|
|
551
|
+
// somewhere to land, and so a test can tell "nothing to report" from
|
|
552
|
+
// "this notice was never rendered at all".
|
|
553
|
+
const found = [];
|
|
554
|
+
|
|
555
|
+
const detail = geometry.edgeLayoutDetail(doc, layout);
|
|
556
|
+
// Bucket every skip by its own reported `reason` — discovered from what
|
|
557
|
+
// came back, not enumerated in advance, so nothing here has to change
|
|
558
|
+
// for a reason this function has never heard of.
|
|
559
|
+
const buckets = new Map();
|
|
560
|
+
for (const skip of detail.skipped) {
|
|
561
|
+
if (!buckets.has(skip.reason)) buckets.set(skip.reason, []);
|
|
562
|
+
buckets.get(skip.reason).push(skip);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function letters(list) {
|
|
566
|
+
const out = [];
|
|
567
|
+
for (const skip of list) {
|
|
568
|
+
if (skip.edge === null) continue;
|
|
569
|
+
if (out.indexOf(skip.edge.from) === -1) out.push(skip.edge.from);
|
|
570
|
+
if (out.indexOf(skip.edge.to) === -1) out.push(skip.edge.to);
|
|
571
|
+
}
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Known reasons get a sentence that says what a user can act on.
|
|
576
|
+
// `dangling` matches the engine (nothing to reconcile against the
|
|
577
|
+
// preview); `unparseable` and `out-of-range` are the two the engine
|
|
578
|
+
// draws and we do not, so both point at the preview as the tie-breaker.
|
|
579
|
+
const REASON_TEXT = {
|
|
580
|
+
dangling: function (list) {
|
|
581
|
+
return '這個區塊有 ' + list.length + ' 條關聯線引用了不存在的 anchor(' +
|
|
582
|
+
letters(list).join('、') + '),左邊畫不出它們。';
|
|
583
|
+
},
|
|
584
|
+
unparseable: function (list) {
|
|
585
|
+
return '這個區塊有 ' + list.length +
|
|
586
|
+
' 條關聯線的寫法連引擎也認不得,左邊畫不出它們(跟右邊的 WaveDrom 預覽一致)。';
|
|
587
|
+
},
|
|
588
|
+
'out-of-range': function (list) {
|
|
589
|
+
return '這個區塊有 ' + list.length + ' 條關聯線的 anchor(' +
|
|
590
|
+
letters(list).join('、') + ')落在那條 lane 目前的長度之外,左邊畫不出它們' +
|
|
591
|
+
'——但右邊的 WaveDrom 預覽仍然會畫,兩邊會不一致,以預覽為準。';
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const sentences = [];
|
|
596
|
+
if (found.length > 0) {
|
|
597
|
+
sentences.push('這份文件用了 ' + found.join('、') +
|
|
598
|
+
',左邊的手繪波形不表現它們(寬度與相位會不一樣)——以右邊的 WaveDrom 預覽為準。');
|
|
599
|
+
}
|
|
600
|
+
for (const [reason, list] of buckets) {
|
|
601
|
+
const text = REASON_TEXT[reason];
|
|
602
|
+
sentences.push(text ? text(list) :
|
|
603
|
+
'這個區塊有 ' + list.length + ' 條關聯線因為「' + reason + '」畫不出來,' +
|
|
604
|
+
'以右邊的 WaveDrom 預覽為準。');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const danglingCount = buckets.has('dangling') ? buckets.get('dangling').length : 0;
|
|
608
|
+
overlay.setAttribute('data-wave-unmodelled', found.join(' '));
|
|
609
|
+
overlay.setAttribute('data-wave-dangling-edges', String(danglingCount));
|
|
610
|
+
// The total the dangling-only count above cannot say: every edge entry
|
|
611
|
+
// `edgeLayoutDetail` could not place, for any reason at all.
|
|
612
|
+
overlay.setAttribute('data-wave-skipped-edges', String(detail.skipped.length));
|
|
613
|
+
deps.unmodelled.hidden = sentences.length === 0;
|
|
614
|
+
deps.unmodelled.textContent = sentences.join(' ');
|
|
615
|
+
|
|
616
|
+
// …and the bigger version of the same honesty. This editor edits
|
|
617
|
+
// `signal:` and nothing else, so a `reg:` or `assign:` block opens a
|
|
618
|
+
// canvas with no lanes on it: `lanePaths()` is empty, `addLane()` hands
|
|
619
|
+
// the document straight back, and pressing + produced one status line
|
|
620
|
+
//「這個動作沒有改變任何東西」— true, and no explanation at all. An
|
|
621
|
+
// editor that looks broken is worse than one that says what it cannot
|
|
622
|
+
// do, and the notice above is the precedent: the drawing already tells
|
|
623
|
+
// the user which properties it is not showing them.
|
|
624
|
+
const kinds = [];
|
|
625
|
+
for (const key of ['reg', 'assign']) {
|
|
626
|
+
if (doc[key] !== undefined) kinds.push(key + ':');
|
|
627
|
+
}
|
|
628
|
+
const noLanes = codec.lanePaths(doc).length === 0;
|
|
629
|
+
overlay.setAttribute('data-wave-nolanes', noLanes ? '1' : '0');
|
|
630
|
+
deps.noSignal.hidden = !noLanes;
|
|
631
|
+
deps.noSignal.textContent = !noLanes ? '' :
|
|
632
|
+
(kinds.length === 0
|
|
633
|
+
? '這個區塊裡沒有 signal:,所以沒有可以編輯的 lane。'
|
|
634
|
+
: '這個區塊用的是 ' + kinds.join(' / ') +
|
|
635
|
+
',這個編輯器只編輯 signal: 的 lane。') +
|
|
636
|
+
'右邊的 WaveDrom 預覽仍然是引擎畫的原圖;要改這個區塊請用「⠿ → MD 原始碼」。';
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* The lane list.
|
|
641
|
+
*
|
|
642
|
+
* Rows are `layout.laneHeight` tall and in `layout.lanes` order, so row k
|
|
643
|
+
* here and row k of the drawing are the same lane by construction rather
|
|
644
|
+
* than by a second traversal that could disagree.
|
|
645
|
+
*/
|
|
646
|
+
function renderLanes(doc, layout, state) {
|
|
647
|
+
deps.laneCol.textContent = '';
|
|
648
|
+
const rows = d.createElement('div');
|
|
649
|
+
rows.className = 'ed-wave-lane-rows';
|
|
650
|
+
rows.style.height = layout.height + 'px';
|
|
651
|
+
deps.laneCol.appendChild(rows);
|
|
652
|
+
|
|
653
|
+
const spans = geometry.groupSpansOf(doc, layout);
|
|
654
|
+
|
|
655
|
+
// Group titles first, so they sit UNDER the rows in paint order and a
|
|
656
|
+
// lane's own controls stay clickable over them.
|
|
657
|
+
for (const span of spans) {
|
|
658
|
+
const tag = d.createElement('button');
|
|
659
|
+
tag.type = 'button';
|
|
660
|
+
tag.className = 'ed-wave-group';
|
|
661
|
+
tag.setAttribute('data-focus-key', 'group-' + span.from);
|
|
662
|
+
tag.textContent = span.title;
|
|
663
|
+
tag.title = '群組「' + span.title + '」——點一下改名';
|
|
664
|
+
tag.setAttribute('data-group-from', String(span.from));
|
|
665
|
+
tag.setAttribute('data-group-to', String(span.to));
|
|
666
|
+
// v3.6.0 Task 6: `layout.lanes[i].y`, not `i * layout.laneHeight` — lane
|
|
667
|
+
// 0 no longer starts at y=0 (the ruler/head-title band sits above it),
|
|
668
|
+
// and this rail is a separate scroll-synced column from the canvas that
|
|
669
|
+
// must line up row-for-row with it. `span.from` is always a real lane
|
|
670
|
+
// index (a group always has at least one lane under it), so
|
|
671
|
+
// `layout.lanes[span.from]` is never absent here.
|
|
672
|
+
tag.style.top = layout.lanes[span.from].y + 'px';
|
|
673
|
+
tag.style.height = ((span.to - span.from + 1) * layout.laneHeight) + 'px';
|
|
674
|
+
tag.style.left = ((span.depth - 1) * 16) + 'px';
|
|
675
|
+
tag.addEventListener('click', function () { renameGroup(span); });
|
|
676
|
+
rows.appendChild(tag);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (let i = 0; i < layout.lanes.length; i++) {
|
|
680
|
+
rows.appendChild(laneRow(doc, layout, i, state));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Which row a lane-handle drag started on, for the life of that one drag.
|
|
686
|
+
*
|
|
687
|
+
* NOT read out of the drag's own `DataTransfer`: that payload is writable
|
|
688
|
+
* by any page, so a drop handler trusting it would accept a drag begun in
|
|
689
|
+
* another window as a lane reorder. This is a closure variable, set by the
|
|
690
|
+
* one `dragstart` in this file and cleared by the matching `dragend` and
|
|
691
|
+
* by every `drop` that acts on it, so a drag that never started here is
|
|
692
|
+
* `null` here and every drop is a no-op.
|
|
693
|
+
*/
|
|
694
|
+
let laneDragFrom = null;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* The rail's one row: a drag handle, a name field, and — on a lane that is
|
|
698
|
+
* WaveDrom's own blank spacer — a badge saying so.
|
|
699
|
+
*
|
|
700
|
+
* v3.6.0 Task 14. Every row used to carry six controls (+ ▲ ▼ ✕ 複製
|
|
701
|
+
* 空白列), and the one row that hosted its group's 解散群組 carried seven.
|
|
702
|
+
* They did not fit: the rail is 200px, the name field was flexed down to
|
|
703
|
+
* 45px on the most crowded row (MEASURED in Task 12's own Shift+click
|
|
704
|
+
* comment, since deleted with the gesture it described), and a three-
|
|
705
|
+
* character Chinese caption in a 24px button is laid out vertically by the
|
|
706
|
+
* browser — the editor shipped a column of stacked glyphs where a label was
|
|
707
|
+
* meant. All six moved to the toolbar's 訊號/群組 sections, where they act
|
|
708
|
+
* on `selection`.
|
|
709
|
+
*
|
|
710
|
+
* That move is what retires the rail's SECOND selection — the set of row
|
|
711
|
+
* numbers Task 12 added to serve one button (建立群組), which then had to
|
|
712
|
+
* be carried across undo/redo beside the other four marks, clamped again
|
|
713
|
+
* in `render()`, and drawn with a tint of its own so a user could tell it
|
|
714
|
+
* from the cycle selection standing at the same time. There is one
|
|
715
|
+
* selection now: `selection`, whose `laneIndex`..`laneTo` is the lane range
|
|
716
|
+
* and whose `from`..`to` is the cycle range on `laneIndex`. Shift+click
|
|
717
|
+
* here and Shift+arrow on the canvas write the same mark.
|
|
718
|
+
*
|
|
719
|
+
* Clicking the row selects its lane; clicking the NAME FIELD does not.
|
|
720
|
+
* That exclusion is inherited from the gesture this replaces and is the
|
|
721
|
+
* same fact: `render()` rebuilds the whole rail, so a repaint fired from
|
|
722
|
+
* inside a field the user is still typing in rebuilds that field from the
|
|
723
|
+
* document — discarding uncommitted text with nothing on screen to explain
|
|
724
|
+
* it. The handle is always present and never flexes away, so there is
|
|
725
|
+
* always a click target for "select this lane" even on a row whose name
|
|
726
|
+
* fills the field.
|
|
727
|
+
*/
|
|
728
|
+
function laneRow(doc, layout, i, state) {
|
|
729
|
+
const row = d.createElement('div');
|
|
730
|
+
row.className = 'ed-wave-lane-row';
|
|
731
|
+
row.setAttribute('data-lane', String(i));
|
|
732
|
+
// v3.6.0 Task 6: `layout.lanes[i].y` carries the ruler/head-title band's
|
|
733
|
+
// offset, `i * layout.laneHeight` does not. Left as the latter this row
|
|
734
|
+
// would sit `layout.originY` px too high, out of step with its own
|
|
735
|
+
// canvas row.
|
|
736
|
+
row.style.top = layout.lanes[i].y + 'px';
|
|
737
|
+
row.style.height = layout.laneHeight + 'px';
|
|
738
|
+
row.style.paddingLeft = (layout.lanes[i].depth * 16) + 'px';
|
|
739
|
+
if (state.selection !== null &&
|
|
740
|
+
i >= state.selection.laneIndex && i <= state.selection.laneTo) {
|
|
741
|
+
row.classList.add('is-selected');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
row.addEventListener('click', function (ev) {
|
|
745
|
+
if (ev.target !== null && typeof ev.target.classList === 'object' &&
|
|
746
|
+
ev.target.classList.contains('ed-wave-lane-name')) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
ev.preventDefault();
|
|
750
|
+
ev.stopPropagation();
|
|
751
|
+
const sel = state.selection;
|
|
752
|
+
if (ev.shiftKey === true && sel !== null) {
|
|
753
|
+
// Extend, normalised so `laneIndex <= laneTo` always holds and no
|
|
754
|
+
// reader has to re-derive which end is which. Extending UPWARDS
|
|
755
|
+
// therefore moves `laneIndex` — and with it the row the cycle box
|
|
756
|
+
// sits on — to the new top of the range; the cycle numbers
|
|
757
|
+
// themselves are untouched.
|
|
758
|
+
const anchor = sel.laneIndex;
|
|
759
|
+
actions.selectionSet({
|
|
760
|
+
laneIndex: Math.min(anchor, i), laneTo: Math.max(anchor, i),
|
|
761
|
+
from: sel.from, to: sel.to,
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
// A plain click selects this lane at its first cycle. It IS a cycle
|
|
765
|
+
// selection, deliberately: there is one selection, and picking a
|
|
766
|
+
// lane from the rail has to leave the toolbar's cycle section
|
|
767
|
+
// pointing somewhere real rather than at a lane range with no
|
|
768
|
+
// cycles named.
|
|
769
|
+
actions.selectionSet({ laneIndex: i, laneTo: i, from: 0, to: 0 });
|
|
770
|
+
}
|
|
771
|
+
actions.render();
|
|
772
|
+
}, true);
|
|
773
|
+
|
|
774
|
+
// Reordering. `codec.moveLane` is the one place that knows what a
|
|
775
|
+
// destination position means once the lane has been lifted out (and that
|
|
776
|
+
// it may cross a group boundary on the way) — this gesture hands it two
|
|
777
|
+
// display positions and nothing else, rather than growing a second
|
|
778
|
+
// opinion about lane order in the rail.
|
|
779
|
+
const handle = d.createElement('span');
|
|
780
|
+
handle.className = 'ed-wave-lane-handle';
|
|
781
|
+
handle.setAttribute('draggable', 'true');
|
|
782
|
+
// Deliberately NO `data-focus-key`. `restoreFocus` nominates a key and
|
|
783
|
+
// calls `.focus()` on whatever carries it; a <span> with no tabindex
|
|
784
|
+
// cannot take focus, so a key here would be a silent route to
|
|
785
|
+
// `document.body` the moment anything nominated it — the exact state
|
|
786
|
+
// that turns the next Backspace into「delete the selected blocks」.
|
|
787
|
+
handle.textContent = '⠿';
|
|
788
|
+
// v3.6.0 Task 16: this tooltip is the reorder affordance's own description
|
|
789
|
+
// of how reordering is done, so it names BOTH doors — the drag below and
|
|
790
|
+
// `wave-ui.js`'s Alt+↑/↓, which reach the same `codec.moveLane`.
|
|
791
|
+
//
|
|
792
|
+
// fix round 1 WITHDRAWS the reason the first cut gave for this line
|
|
793
|
+
// ("leaving it drag-only would leave the keyboard route discoverable
|
|
794
|
+
// nowhere"). That was false six lines above its own head: this is a
|
|
795
|
+
// HOVER tooltip on a `<span>` that deliberately carries no
|
|
796
|
+
// `data-focus-key` and cannot take focus, so a keyboard-only user never
|
|
797
|
+
// reads a character of it. What the line is actually worth runs the
|
|
798
|
+
// other way — the person already reaching for the handle with a mouse
|
|
799
|
+
// learns there is a cheaper route. The three surfaces a KEYBOARD user
|
|
800
|
+
// does read are the canvas's `aria-label` (`wave-draw.js`'s
|
|
801
|
+
// `renderCanvas`), the opening status line, and `handleDrawingKey`'s own
|
|
802
|
+
// docstring (both in `wave-ui.js`); fix round 1 is where those three
|
|
803
|
+
// were made true.
|
|
804
|
+
handle.title = '拖曳這裡可以調整這條 lane 的順序(或選起來按 Alt+↑/Alt+↓ 搬)';
|
|
805
|
+
handle.addEventListener('dragstart', function (ev) {
|
|
806
|
+
laneDragFrom = i;
|
|
807
|
+
// The row index also goes into the DataTransfer, but it is NOT what
|
|
808
|
+
// the drop reads: a drop handler that trusted it would accept a drag
|
|
809
|
+
// that started anywhere on the page, including another window. The
|
|
810
|
+
// payload is set because a drag with no data attached does not start
|
|
811
|
+
// at all in some browsers, and for nothing else.
|
|
812
|
+
if (ev.dataTransfer !== null && ev.dataTransfer !== undefined) {
|
|
813
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
814
|
+
try { ev.dataTransfer.setData('text/plain', String(i)); } catch (e) { /* nothing to do */ }
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
handle.addEventListener('dragend', function () { laneDragFrom = null; });
|
|
818
|
+
row.appendChild(handle);
|
|
819
|
+
|
|
820
|
+
row.addEventListener('dragover', function (ev) {
|
|
821
|
+
if (laneDragFrom === null) return;
|
|
822
|
+
// preventDefault is what MAKES this a drop target; without it the
|
|
823
|
+
// browser never fires `drop` at all and the gesture dies silently.
|
|
824
|
+
ev.preventDefault();
|
|
825
|
+
if (ev.dataTransfer !== null && ev.dataTransfer !== undefined) {
|
|
826
|
+
ev.dataTransfer.dropEffect = 'move';
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
row.addEventListener('drop', function (ev) {
|
|
830
|
+
if (laneDragFrom === null) return;
|
|
831
|
+
ev.preventDefault();
|
|
832
|
+
const from = laneDragFrom;
|
|
833
|
+
laneDragFrom = null;
|
|
834
|
+
if (from === i) return;
|
|
835
|
+
// The lane arrives at this row, so the keyboard goes to the field it
|
|
836
|
+
// arrives in — the same rule every gesture in this dialog follows, and
|
|
837
|
+
// the reason F7/T6h exist.
|
|
838
|
+
actions.focusOverrideSet('lane-name-' + i);
|
|
839
|
+
actions.commit('move-lane', function (doc2) { return codec.moveLane(doc2, from, i); });
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const name = d.createElement('input');
|
|
843
|
+
name.type = 'text';
|
|
844
|
+
name.className = 'ed-wave-lane-name';
|
|
845
|
+
name.setAttribute('data-focus-key', 'lane-name-' + i);
|
|
846
|
+
const lane = layout.lanes[i].lane;
|
|
847
|
+
name.value = typeof lane.name === 'string' ? lane.name : '';
|
|
848
|
+
name.addEventListener('change', function () {
|
|
849
|
+
actions.commit('rename', function (doc2) { return codec.renameLane(doc2, i, name.value); });
|
|
850
|
+
});
|
|
851
|
+
row.appendChild(name);
|
|
852
|
+
|
|
853
|
+
// WaveDrom's own blank row, `{}` — no `wave`, no `name`. It draws as
|
|
854
|
+
// nothing, so without this the rail shows an empty name field on a row
|
|
855
|
+
// the canvas leaves blank and there is no way to tell it from a lane
|
|
856
|
+
// someone simply has not named yet.
|
|
857
|
+
if (lane.wave === undefined && lane.name === undefined) {
|
|
858
|
+
const badge = d.createElement('span');
|
|
859
|
+
badge.className = 'ed-wave-lane-badge';
|
|
860
|
+
badge.textContent = '空白列';
|
|
861
|
+
badge.title = 'WaveJSON 的 {},用來空出間距';
|
|
862
|
+
row.appendChild(badge);
|
|
863
|
+
}
|
|
864
|
+
return row;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Where a lane inserted at display position `index` would actually land,
|
|
869
|
+
* asked of the bridge rather than guessed.
|
|
870
|
+
*
|
|
871
|
+
* This is constraint 4 made visible. `laneInsertPath` puts an insert at a
|
|
872
|
+
* group's FIRST lane inside that group, and an insert at the position just
|
|
873
|
+
* past its LAST lane outside it — a group can be joined at its head and
|
|
874
|
+
* never at its tail. That asymmetry is not something a label can argue
|
|
875
|
+
* away, so the label states it: the 訊號 section's 新增 says which
|
|
876
|
+
* container it will land in before it is pressed (`data-lands-in`, kept
|
|
877
|
+
* current by `wave-ui.js`'s `render()`), and the status line says it again
|
|
878
|
+
* after.
|
|
879
|
+
*
|
|
880
|
+
* v3.6.0 Task 14: exported, because the caller that needs it is now the
|
|
881
|
+
* toolbar in `wave-ui.js` rather than a button this file builds.
|
|
882
|
+
*/
|
|
883
|
+
function landingOf(doc, index) {
|
|
884
|
+
const path = codec.laneInsertPath(doc, index);
|
|
885
|
+
if (path === null) return { path: null, title: null };
|
|
886
|
+
const container = path.slice(0, path.length - 1);
|
|
887
|
+
const held = geometry.valueAt(doc, container);
|
|
888
|
+
const title = Array.isArray(held) && typeof held[0] === 'string' ? held[0] : null;
|
|
889
|
+
return { path: path, title: title };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* The rail's rename field: this dialog's one sub-panel.
|
|
894
|
+
*
|
|
895
|
+
* The field is INSERTED beside its tag and the tag is hidden, rather than
|
|
896
|
+
* the two being swapped, and taking it down again touches only those two
|
|
897
|
+
* nodes. That is not tidiness. Taking it down used to mean a full
|
|
898
|
+
* `render()` — a rebuild of the entire rail — and a rebuild that runs
|
|
899
|
+
* inside a `blur` handler destroys the element the user is in the middle
|
|
900
|
+
* of pressing. Measured before this: open the field, then press any rail
|
|
901
|
+
* button; the mousedown blurred the field, the rail was rebuilt, the
|
|
902
|
+
* mouseup landed on a fresh element so no `click` ever fired, the press
|
|
903
|
+
* did nothing at all, and the keyboard ended on `document.body` inside a
|
|
904
|
+
* modal with no visible cursor.
|
|
905
|
+
*/
|
|
906
|
+
function renameGroup(span) {
|
|
907
|
+
const rows = deps.laneCol.querySelector('.ed-wave-lane-rows');
|
|
908
|
+
const tag = rows === null ? null
|
|
909
|
+
: rows.querySelector('.ed-wave-group[data-group-from="' + span.from + '"]');
|
|
910
|
+
if (tag === null) return;
|
|
911
|
+
|
|
912
|
+
const input = d.createElement('input');
|
|
913
|
+
input.type = 'text';
|
|
914
|
+
input.className = 'ed-wave-group-input';
|
|
915
|
+
input.setAttribute('data-focus-key', 'group-input-' + span.from);
|
|
916
|
+
input.value = span.title;
|
|
917
|
+
input.style.top = tag.style.top;
|
|
918
|
+
input.style.left = tag.style.left;
|
|
919
|
+
|
|
920
|
+
/** The field off, the tag back — and nothing else in the rail touched.
|
|
921
|
+
* A no-op for both once a repaint has taken them away. */
|
|
922
|
+
const takeDown = function () {
|
|
923
|
+
if (input.parentNode !== null) input.parentNode.removeChild(input);
|
|
924
|
+
if (d.contains(tag)) tag.hidden = false;
|
|
925
|
+
};
|
|
926
|
+
/** The tag this field was covering, or — when a repaint has replaced it
|
|
927
|
+
* and moved it (a group's `from` is its first lane's row, so an insert
|
|
928
|
+
* above it renumbers the key) — whatever now carries that key, and
|
|
929
|
+
* failing that the dialog itself. Never nothing: nothing is
|
|
930
|
+
* `document.body`. */
|
|
931
|
+
const focusTag = function () {
|
|
932
|
+
const live = d.contains(tag) ? tag
|
|
933
|
+
: overlay.querySelector('[data-focus-key="group-' + span.from + '"]');
|
|
934
|
+
if (live !== null) live.focus();
|
|
935
|
+
else panel.focus();
|
|
936
|
+
};
|
|
937
|
+
/** Whether the keyboard is IN the field right now. False on the `blur`
|
|
938
|
+
* path — the browser has already moved it by then — which is how the
|
|
939
|
+
* two are told apart without asking which device did anything. */
|
|
940
|
+
const holdsKeyboard = function () { return d.activeElement === input; };
|
|
941
|
+
|
|
942
|
+
const finish = function () {
|
|
943
|
+
// Once. `change` and `blur` both fire for a field committed with Enter,
|
|
944
|
+
// and every teardown below is a blur too. Whoever gets here first owns
|
|
945
|
+
// the gesture; the latch is also how the other two say "not you".
|
|
946
|
+
if (actions.groupRenameGet() === null) return;
|
|
947
|
+
actions.groupRenameSet(null);
|
|
948
|
+
const hadKeyboard = holdsKeyboard();
|
|
949
|
+
const value = input.value;
|
|
950
|
+
takeDown();
|
|
951
|
+
if (value === span.title) {
|
|
952
|
+
// Nothing to write, so nothing to repaint — the tag is already back.
|
|
953
|
+
// The keyboard is only moved when it was in the field: on the mouse
|
|
954
|
+
// path the browser is already carrying it to the control that was
|
|
955
|
+
// pressed, and that control is still there to receive it now that
|
|
956
|
+
// this path no longer rebuilds the rail.
|
|
957
|
+
if (hadKeyboard) focusTag();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
// A real commit, so a real repaint — and the keyboard has to be named
|
|
961
|
+
// if it would otherwise be lost. `focusOverride` is left alone when a
|
|
962
|
+
// caller has already made a more specific promise.
|
|
963
|
+
if (actions.focusOverrideGet() === null &&
|
|
964
|
+
(hadKeyboard || !overlay.contains(d.activeElement))) {
|
|
965
|
+
actions.focusOverrideSet('group-' + span.from);
|
|
966
|
+
}
|
|
967
|
+
actions.commit('rename-group', function (doc2) {
|
|
968
|
+
return replaceAt(doc2, span.path.concat([0]), value);
|
|
969
|
+
});
|
|
970
|
+
};
|
|
971
|
+
// The rail is about to be rebuilt underneath this field by somebody
|
|
972
|
+
// else's repaint — an undo, a redo, any gesture that reaches `render()`
|
|
973
|
+
// while it is open. Both nodes go with that rebuild, so there is nothing
|
|
974
|
+
// to take down; what matters is dropping the latch and naming where the
|
|
975
|
+
// keyboard lands, because `group-input-<n>` will not exist afterwards and
|
|
976
|
+
// focus would be left on a node that is no longer in the document — which
|
|
977
|
+
// is the wedge that lost every subsequent keystroke.
|
|
978
|
+
const retire = function () {
|
|
979
|
+
if (actions.groupRenameGet() === null) return;
|
|
980
|
+
actions.groupRenameSet(null);
|
|
981
|
+
if (actions.focusOverrideGet() === null && holdsKeyboard()) {
|
|
982
|
+
actions.focusOverrideSet('group-' + span.from);
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
// Escape's half of the layering: the field goes, the name does not
|
|
986
|
+
// change, the session stands, and the keyboard goes back to the tag the
|
|
987
|
+
// field was covering — never to `document.body`, which is the state the
|
|
988
|
+
// whole of this task exists to make unreachable. No repaint at all: the
|
|
989
|
+
// tag was never destroyed.
|
|
990
|
+
const cancel = function () {
|
|
991
|
+
if (actions.groupRenameGet() === null) return;
|
|
992
|
+
actions.groupRenameSet(null);
|
|
993
|
+
takeDown();
|
|
994
|
+
focusTag();
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
input.addEventListener('change', finish);
|
|
998
|
+
input.addEventListener('blur', finish);
|
|
999
|
+
tag.hidden = true;
|
|
1000
|
+
rows.insertBefore(input, tag);
|
|
1001
|
+
// Three facts, and only the three that are read: that a rename is
|
|
1002
|
+
// standing (this being non-null), how to take it down on purpose, and how
|
|
1003
|
+
// to let go of it when a repaint takes it down for us. A copy of the span
|
|
1004
|
+
// or of the field itself would be a second record of something the
|
|
1005
|
+
// closure above already holds.
|
|
1006
|
+
actions.groupRenameSet({ cancel: cancel, retire: retire });
|
|
1007
|
+
input.focus();
|
|
1008
|
+
input.select();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* A copy of `doc` with one scalar replaced, for the two fields the codec's
|
|
1013
|
+
* lane operations do not cover (a group's own title).
|
|
1014
|
+
*
|
|
1015
|
+
* Structure-sharing everywhere it does not have to copy, so the store's
|
|
1016
|
+
* lane-identity bookkeeping still sees every untouched lane as the same
|
|
1017
|
+
* object — `remapOrigins` answers a content edit by position and a
|
|
1018
|
+
* structural one by identity, and a wholesale clone here would make an
|
|
1019
|
+
* ordinary rename look like a structural change.
|
|
1020
|
+
*/
|
|
1021
|
+
function replaceAt(node, path, value) {
|
|
1022
|
+
if (path.length === 0) return value;
|
|
1023
|
+
const seg = path[0];
|
|
1024
|
+
const child = replaceAt(node[seg], path.slice(1), value);
|
|
1025
|
+
if (Array.isArray(node)) {
|
|
1026
|
+
const out = node.slice();
|
|
1027
|
+
out[seg] = child;
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
const out = Object.assign({}, node);
|
|
1031
|
+
out[seg] = child;
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function renderPreview(doc) {
|
|
1036
|
+
const engine = wavedrom;
|
|
1037
|
+
if (engine === null || engine === undefined ||
|
|
1038
|
+
typeof engine.RenderWaveForm !== 'function') {
|
|
1039
|
+
deps.previewHost.textContent = '(這一頁沒有載入 WaveDrom,預覽從缺)';
|
|
1040
|
+
deps.previewWrap.setAttribute('data-wave-preview', 'absent');
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
// `notFirstSignal` means "the skin is already on this page, do not
|
|
1045
|
+
// emit it again". True whenever a WaveDrom render has already run here,
|
|
1046
|
+
// which is always in the flow that reaches this editor — the entry
|
|
1047
|
+
// affordance only appears over a RENDERED diagram. The fallback to
|
|
1048
|
+
// `false` is for the page where it somehow has not: emitting the skin
|
|
1049
|
+
// is only wasteful, while omitting one that is not there leaves every
|
|
1050
|
+
// `<use>` unresolvable and the preview blank, which is the one state
|
|
1051
|
+
// this panel may never be in.
|
|
1052
|
+
// `socket` is a bare, global, un-namespaced id that the engine's own
|
|
1053
|
+
// skin `<defs>` defines — so ask for it INSIDE a WaveDrom-rendered SVG's
|
|
1054
|
+
// defs rather than anywhere on the page. A document carrying inline
|
|
1055
|
+
// HTML with `id="socket"` would otherwise convince this that the skin is
|
|
1056
|
+
// present on a page where no diagram had rendered, and the preview would
|
|
1057
|
+
// come up blank: the one state this panel may never be in.
|
|
1058
|
+
const skinOnPage = d.querySelector('svg.WaveDrom defs #socket') !== null;
|
|
1059
|
+
engine.RenderWaveForm(PREVIEW_INDEX, plainCopy(doc, 0),
|
|
1060
|
+
PREVIEW_ID_PREFIX, skinOnPage);
|
|
1061
|
+
deps.previewWrap.setAttribute('data-wave-preview', 'ok');
|
|
1062
|
+
deps.previewWrap.setAttribute('data-wave-preview-skin', skinOnPage ? 'shared' : 'own');
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
deps.previewHost.textContent = 'WaveDrom 畫不出這份文件:' + String(e && e.message);
|
|
1065
|
+
deps.previewWrap.setAttribute('data-wave-preview', 'failed');
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
toolGroup: toolGroup,
|
|
1071
|
+
toolButton: toolButton,
|
|
1072
|
+
metaField: metaField,
|
|
1073
|
+
commitBanner: commitBanner,
|
|
1074
|
+
commitConfigField: commitConfigField,
|
|
1075
|
+
commitLaneField: commitLaneField,
|
|
1076
|
+
renderMeta: renderMeta,
|
|
1077
|
+
renderLaneField: renderLaneField,
|
|
1078
|
+
renderSource: renderSource,
|
|
1079
|
+
renderLanes: renderLanes,
|
|
1080
|
+
// v3.6.0 Task 14: the 訊號 section's 新增/空白列/複製 live in
|
|
1081
|
+
// `wave-ui.js` now, and each of them has to say which container its
|
|
1082
|
+
// insert lands in — before the press (the button's own `data-lands-in`)
|
|
1083
|
+
// and after it (the status line). One answer, from the bridge.
|
|
1084
|
+
landingOf: landingOf,
|
|
1085
|
+
renderEdgeBar: renderEdgeBar,
|
|
1086
|
+
renderPreview: renderPreview,
|
|
1087
|
+
renderUnmodelled: renderUnmodelled,
|
|
1088
|
+
renameGroup: renameGroup,
|
|
1089
|
+
say: say,
|
|
1090
|
+
// v3.6.0 Task 13: the right-hand column and its five section bodies —
|
|
1091
|
+
// see the block above `metaField` for why these are built eagerly
|
|
1092
|
+
// rather than published lazily onto `deps` the way `metaBar`/`edgeBar`/
|
|
1093
|
+
// etc. used to be. `wave-ui.js` appends `side` once into `.ed-wave-body`
|
|
1094
|
+
// and targets `sections.<key>` for every field/notice/preview that used
|
|
1095
|
+
// to go straight into the toolbar or the body row.
|
|
1096
|
+
side: side,
|
|
1097
|
+
sections: sections,
|
|
1098
|
+
expandSide: expandSide,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
module.exports = { createPanels: createPanels };
|