@helping-ai-workflow/md2doc 3.12.0 → 3.13.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/bin/md2doc.js +6 -0
- package/lib/editor/client.js +356 -180
- package/lib/editor/server.js +5 -0
- package/lib/editor/wave-canvas.js +301 -0
- package/lib/editor/wave-codec.js +417 -11
- package/lib/editor/wave-draw.js +209 -1202
- package/lib/editor/wave-geometry.js +145 -10
- package/lib/editor/wave-panels.js +907 -918
- package/lib/editor/wave-store.js +486 -91
- package/lib/editor/wave-ui.js +3594 -2232
- package/lib/md2doc.js +342 -460
- package/lib/theme/runtime.js +27 -3
- package/lib/wave-overflow.js +107 -0
- package/package.json +3 -3
- package/scripts/release-check.js +60 -0
|
@@ -1,331 +1,811 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* wave-panels — the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* wave-panels — the pieces of the waveform editor's shell that are built from
|
|
5
|
+
* plain DOM: toolbar buttons, the small popovers the toolbar opens (menus and
|
|
6
|
+
* the grouped brush grid), and the notice card under the toolbar that says
|
|
7
|
+
* which parts of the document the editor cannot place.
|
|
7
8
|
*
|
|
8
9
|
* Plain CommonJS, like `wave-codec.js` / `wave-geometry.js` / `wave-store.js`
|
|
9
|
-
* / `wave-draw.js` next to it
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and `lib/editor/client.js`'s `openWaveEditor`), the same way it already
|
|
15
|
-
* takes `opts.draw`.
|
|
10
|
+
* / `wave-draw.js` next to it: `wave-ui.js` is injected into the edit page as
|
|
11
|
+
* a bare `<script>` with no `require()` of its own, so it takes this module in
|
|
12
|
+
* as `opts.panels` (see `lib/editor/server.js`'s shim and
|
|
13
|
+
* `lib/editor/client.js`'s `openWaveEditor`), the same way it takes
|
|
14
|
+
* `opts.draw`.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
16
|
+
* Wave redesign Task 6b: the right-hand column (its five collapsible panels,
|
|
17
|
+
* the lane list beside the canvas and the preview) is gone. Spec section 4.9
|
|
18
|
+
* gives each of its parts a new home: a lane's period / phase and the group
|
|
19
|
+
* rename come back in Task 8's name column (`laneMenu`, `renameField`,
|
|
20
|
+
* `periodPhasePopover`), the edge inspector as the floating toolbar beside
|
|
21
|
+
* an edge's label in Task 9 (`edgeToolbar`, `noteToolbar`, `inlineInput`),
|
|
22
|
+
* and the settings fields as Task 10's ⚙ popover (`settingsPopover`).
|
|
23
|
+
* Task 11 adds the shortcut card (`shortcutCard`, spec section 4.8).
|
|
21
24
|
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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.
|
|
25
|
+
* `deps`:
|
|
26
|
+
* d, geometry, codec the document and the two modules, handed in
|
|
27
|
+
* overlay the dialog root (the notice publishes its counts there)
|
|
28
|
+
* actions.commit(name, fn) run one gesture (constraint 5 in wave-ui.js)
|
|
95
29
|
*/
|
|
96
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The edge toolbar's two button groups (spec section 4.5): 線型 straight /
|
|
33
|
+
* curve / elbow by 箭頭 none / end / both, nine shapes. `SHAPE_ROWS` also
|
|
34
|
+
* carries the half-curve and elbow variants 更多 offers, each with the
|
|
35
|
+
* variant it becomes under each head (null: the variant has no such form),
|
|
36
|
+
* so changing the head of `-|-` gives `-|->`, not `-|>`.
|
|
37
|
+
*/
|
|
38
|
+
const NINE = {
|
|
39
|
+
straight: { none: '-', end: '->', both: '<->' },
|
|
40
|
+
curve: { none: '~', end: '~>', both: '<~>' },
|
|
41
|
+
elbow: { none: '-|', end: '-|>', both: '<-|>' },
|
|
42
|
+
};
|
|
43
|
+
const SHAPE_ROWS = [
|
|
44
|
+
['straight', ['-', '->', '<->']],
|
|
45
|
+
['curve', ['~', '~>', '<~>']],
|
|
46
|
+
['elbow', ['-|', '-|>', '<-|>']],
|
|
47
|
+
['curve', ['-~', '-~>', '<-~>']],
|
|
48
|
+
['curve', ['~-', '~->', null]],
|
|
49
|
+
['elbow', ['-|-', '-|->', '<-|->']],
|
|
50
|
+
['elbow', ['|-', '|->', null]],
|
|
51
|
+
];
|
|
52
|
+
const HEADS = ['none', 'end', 'both'];
|
|
53
|
+
|
|
54
|
+
/** `shape` -> `{line, head}`; both null for `+` (a tee at each end, no line
|
|
55
|
+
* family and no arrow) and for anything unknown. */
|
|
56
|
+
function shapeParts(shape) {
|
|
57
|
+
for (const row of SHAPE_ROWS) {
|
|
58
|
+
const at = row[1].indexOf(shape);
|
|
59
|
+
if (at !== -1) return { line: row[0], head: HEADS[at] };
|
|
60
|
+
}
|
|
61
|
+
return { line: null, head: null };
|
|
62
|
+
}
|
|
63
|
+
/** The shape `shape` becomes when one of its parts changes: `{line}` picks
|
|
64
|
+
* from the nine and keeps the head; `{head}` keeps a 更多 variant when it
|
|
65
|
+
* has that head, else falls back to the nine. A part that `shape` does not
|
|
66
|
+
* have (`+`) reads as straight, no arrow. */
|
|
67
|
+
function shapeWith(shape, change) {
|
|
68
|
+
const parts = shapeParts(shape);
|
|
69
|
+
const line = parts.line === null ? 'straight' : parts.line;
|
|
70
|
+
const head = parts.head === null ? 'none' : parts.head;
|
|
71
|
+
if (change.line !== undefined) return NINE[change.line][head];
|
|
72
|
+
for (const row of SHAPE_ROWS) {
|
|
73
|
+
if (row[1].indexOf(shape) === -1) continue;
|
|
74
|
+
const v = row[1][HEADS.indexOf(change.head)];
|
|
75
|
+
if (v !== null && v !== undefined) return v;
|
|
76
|
+
}
|
|
77
|
+
return NINE[line][change.head];
|
|
78
|
+
}
|
|
79
|
+
const NINE_SHAPES = [].concat.apply([], Object.keys(NINE).map(function (k) {
|
|
80
|
+
return HEADS.map(function (h) { return NINE[k][h]; });
|
|
81
|
+
}));
|
|
82
|
+
|
|
97
83
|
function createPanels(deps) {
|
|
98
84
|
const d = deps.d;
|
|
99
85
|
const geometry = deps.geometry;
|
|
100
86
|
const codec = deps.codec;
|
|
101
|
-
const SIZES = deps.SIZES;
|
|
102
87
|
const actions = deps.actions;
|
|
103
88
|
const overlay = deps.overlay;
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
/** 更多's list: every shape the engine draws that is not one of the nine. */
|
|
90
|
+
const MORE_SHAPES = codec.EDGE_SHAPES.filter(function (s) { return NINE_SHAPES.indexOf(s) === -1; });
|
|
91
|
+
|
|
92
|
+
/** One toolbar button. `parent` (last arg, required) is where it goes. */
|
|
93
|
+
function toolButton(cls, label, hint, fn, parent) {
|
|
94
|
+
const b = d.createElement('button');
|
|
95
|
+
b.type = 'button';
|
|
96
|
+
b.className = cls;
|
|
97
|
+
b.setAttribute('data-focus-key', cls);
|
|
98
|
+
b.textContent = label;
|
|
99
|
+
b.title = hint;
|
|
100
|
+
b.addEventListener('click', fn);
|
|
101
|
+
parent.appendChild(b);
|
|
102
|
+
return b;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A small popover a toolbar button opens: `div.ed-wave-pop` plus the given
|
|
107
|
+
* class. `wave-ui.js` places it under its button and owns opening and
|
|
108
|
+
* closing (only one is open at a time).
|
|
109
|
+
*/
|
|
110
|
+
function popover(cls) {
|
|
111
|
+
const el = d.createElement('div');
|
|
112
|
+
el.className = 'ed-wave-pop ' + cls;
|
|
113
|
+
return el;
|
|
114
|
+
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
|
-
* A
|
|
117
|
+
* A menu popover. `items` are `{ key, label, title, run, shortcut?, danger? }`;
|
|
118
|
+
* `key` becomes both the item's class and its `data-focus-key`, like
|
|
119
|
+
* `toolButton`. `shortcut` is the key the same action has elsewhere, shown
|
|
120
|
+
* on the item's right; `danger` marks an item that removes something (red).
|
|
121
|
+
*/
|
|
122
|
+
function menu(cls, items) {
|
|
123
|
+
const el = popover('ed-wave-menu ' + cls);
|
|
124
|
+
el.setAttribute('role', 'menu');
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
const b = d.createElement('button');
|
|
127
|
+
b.type = 'button';
|
|
128
|
+
b.className = 'ed-wave-menu-item ' + item.key + (item.danger ? ' is-danger' : '');
|
|
129
|
+
b.setAttribute('data-focus-key', item.key);
|
|
130
|
+
b.setAttribute('role', 'menuitem');
|
|
131
|
+
const label = d.createElement('span');
|
|
132
|
+
label.className = 'ed-wave-menu-label';
|
|
133
|
+
label.textContent = item.label;
|
|
134
|
+
b.appendChild(label);
|
|
135
|
+
if (item.shortcut) {
|
|
136
|
+
const k = d.createElement('span');
|
|
137
|
+
k.className = 'ed-wave-menu-key';
|
|
138
|
+
k.textContent = item.shortcut;
|
|
139
|
+
b.appendChild(k);
|
|
140
|
+
}
|
|
141
|
+
if (item.title) b.title = item.title;
|
|
142
|
+
b.addEventListener('click', item.run);
|
|
143
|
+
el.appendChild(b);
|
|
144
|
+
}
|
|
145
|
+
return el;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The ⠿ menu of one row in the name column (spec section 4.4). A signal's
|
|
150
|
+
* row: 改名 F2, 建立副本 Ctrl+D, 在下方新增訊號, 在下方新增空白列,
|
|
151
|
+
* 上移 Alt+↑, 下移 Alt+↓, 和下一條組成群組, 週期與相位…, 刪除 Del (red).
|
|
152
|
+
* A group's row (`isGroup`): 改名, 解散群組, 上移, 下移.
|
|
111
153
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
154
|
+
* `actions` holds what each item does, keyed by the item's suffix
|
|
155
|
+
* (`rename`, `copy`, `addBelow`, `blankBelow`, `up`, `down`, `group`,
|
|
156
|
+
* `period`, `remove`; for a group `rename`, `ungroup`, `up`, `down`). An
|
|
157
|
+
* action that is missing leaves its item out: a blank row (`{}`) has no
|
|
158
|
+
* name, so wave-ui hands it no `rename`. `anchor` is the grip that opens
|
|
159
|
+
* the menu; wave-ui places the menu under it, as it does every toolbar
|
|
160
|
+
* popover.
|
|
118
161
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
function laneMenu(anchor, opts) {
|
|
163
|
+
const a = opts.actions;
|
|
164
|
+
const spec = opts.isGroup ? [
|
|
165
|
+
['rename', 'ed-wave-lane-rename', '改名', '', '改這個群組的名稱'],
|
|
166
|
+
['ungroup', 'ed-wave-lane-ungroup', '解散群組', '', '拿掉這個群組,裡面的訊號留在原位'],
|
|
167
|
+
['up', 'ed-wave-lane-up', '上移', '', '整個群組往上搬一格'],
|
|
168
|
+
['down', 'ed-wave-lane-down', '下移', '', '整個群組往下搬一格'],
|
|
169
|
+
] : [
|
|
170
|
+
['rename', 'ed-wave-lane-rename', '改名', 'F2', '在名稱上直接改名'],
|
|
171
|
+
['copy', 'ed-wave-lane-copy', '建立副本', 'Ctrl+D', '在下面放一條一樣的訊號'],
|
|
172
|
+
['addBelow', 'ed-wave-lane-add-below', '在下方新增訊號', '', '在這一條下面加一條新訊號'],
|
|
173
|
+
['blankBelow', 'ed-wave-lane-blank-below', '在下方新增空白列', '', '在這一條下面插入一個空白列(WaveJSON 的 {})'],
|
|
174
|
+
['up', 'ed-wave-lane-up', '上移', 'Alt+↑', '往上搬一格'],
|
|
175
|
+
['down', 'ed-wave-lane-down', '下移', 'Alt+↓', '往下搬一格'],
|
|
176
|
+
['group', 'ed-wave-lane-group', '和下一條組成群組', '', '把這一條和下一條包成一個群組,接著替它取名'],
|
|
177
|
+
['period', 'ed-wave-lane-period', '週期與相位…', '', '這一條的 period(每個字元佔幾拍)與 phase(往左移幾拍)'],
|
|
178
|
+
['remove', 'ed-wave-lane-delete', '刪除', 'Del', '刪掉這一條(Ctrl+Z 可以拿回來)'],
|
|
179
|
+
];
|
|
180
|
+
const items = [];
|
|
181
|
+
for (const s of spec) {
|
|
182
|
+
if (typeof a[s[0]] !== 'function') continue;
|
|
183
|
+
items.push({ key: s[1], label: s[2], shortcut: s[3], title: s[4], run: a[s[0]], danger: s[0] === 'remove' });
|
|
125
184
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
185
|
+
const el = menu('ed-wave-lane-menu', items);
|
|
186
|
+
if (anchor && typeof anchor.getAttribute === 'function') {
|
|
187
|
+
el.setAttribute('data-for', anchor.getAttribute('data-focus-key') || '');
|
|
188
|
+
}
|
|
189
|
+
return el;
|
|
129
190
|
}
|
|
130
191
|
|
|
131
192
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
193
|
+
* The in-place name field (spec section 4.4): a signal's name or a group's
|
|
194
|
+
* title, typed where it is drawn. wave-ui places it and decides what a
|
|
195
|
+
* commit writes; this owns the keys.
|
|
134
196
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
197
|
+
* `opts`: `value`, `focusKey`, `onCommit(value)`, `onCancel()`.
|
|
198
|
+
* Enter commits and Esc cancels; `change` and `blur` commit too — but
|
|
199
|
+
* never while an input method is composing (spec section 7.2): the Enter
|
|
200
|
+
* that picks a candidate, the Esc that drops a composition and the Tab
|
|
201
|
+
* some IMEs page with belong to the IME. wave-ui's dialog-level key
|
|
202
|
+
* handler already stands aside for those keys; the field's own handlers
|
|
203
|
+
* have to as well, and a `change` fired as a composition is committed is
|
|
204
|
+
* not a commit either.
|
|
142
205
|
*/
|
|
143
|
-
function
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
206
|
+
function renameField(opts) {
|
|
207
|
+
const input = d.createElement('input');
|
|
208
|
+
input.type = 'text';
|
|
209
|
+
input.className = 'ed-wave-rename';
|
|
210
|
+
input.setAttribute('data-focus-key', opts.focusKey);
|
|
211
|
+
input.setAttribute('aria-label', '名稱');
|
|
212
|
+
input.value = opts.value;
|
|
213
|
+
let composing = false;
|
|
214
|
+
input.addEventListener('compositionstart', function () { composing = true; });
|
|
215
|
+
input.addEventListener('compositionend', function () { composing = false; });
|
|
216
|
+
input.addEventListener('keydown', function (ev) {
|
|
217
|
+
if (composing || ev.isComposing === true || ev.keyCode === 229) return;
|
|
218
|
+
if (ev.key === 'Enter') {
|
|
219
|
+
ev.preventDefault();
|
|
220
|
+
ev.stopPropagation();
|
|
221
|
+
opts.onCommit(input.value);
|
|
222
|
+
} else if (ev.key === 'Escape') {
|
|
223
|
+
ev.preventDefault();
|
|
224
|
+
ev.stopPropagation();
|
|
225
|
+
opts.onCancel();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
input.addEventListener('change', function () { if (!composing) opts.onCommit(input.value); });
|
|
229
|
+
input.addEventListener('blur', function () { opts.onCommit(input.value); });
|
|
230
|
+
return input;
|
|
151
231
|
}
|
|
152
232
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
233
|
+
/**
|
|
234
|
+
* 週期與相位… (spec section 4.4): one lane's `period` and `phase`, the two
|
|
235
|
+
* fields the side rail's Lane section used to hold. Each is written when
|
|
236
|
+
* it is committed (Enter, or leaving it) through `commitLaneField`, so a
|
|
237
|
+
* numeral goes in as a number and a blank field removes the key; the
|
|
238
|
+
* panel stays open for the other field. Composition is respected the same
|
|
239
|
+
* way as in the name field.
|
|
240
|
+
*
|
|
241
|
+
* `el.flush()` writes every field whose text is not written yet — wave-ui
|
|
242
|
+
* calls it when Esc or a press outside closes the panel (Ruling R22, which
|
|
243
|
+
* the final review extended from ⚙ to every popover with text fields). A
|
|
244
|
+
* focused field taken out of the page fires `change` in Chromium and not
|
|
245
|
+
* in WebKit, so without it the typed text survived the close in one engine
|
|
246
|
+
* only. A field mid-composition is skipped, exactly as its Enter is.
|
|
247
|
+
*
|
|
248
|
+
* `lane` is the lane object (read for the current values), `at` its
|
|
249
|
+
* display position.
|
|
250
|
+
*/
|
|
251
|
+
function periodPhasePopover(lane, at) {
|
|
252
|
+
const el = popover('ed-wave-period-pop');
|
|
253
|
+
el.setAttribute('role', 'group');
|
|
254
|
+
el.setAttribute('aria-label', '週期與相位');
|
|
255
|
+
const laneObj = lane !== null && typeof lane === 'object' ? lane : {};
|
|
256
|
+
const commits = [];
|
|
257
|
+
for (const key of ['period', 'phase']) {
|
|
258
|
+
const row = d.createElement('label');
|
|
259
|
+
row.className = 'ed-wave-period-row';
|
|
260
|
+
const cap = d.createElement('span');
|
|
261
|
+
cap.className = 'ed-wave-period-cap';
|
|
262
|
+
cap.textContent = key === 'period' ? '週期 period' : '相位 phase';
|
|
263
|
+
row.appendChild(cap);
|
|
264
|
+
const input = d.createElement('input');
|
|
265
|
+
input.type = 'text';
|
|
266
|
+
input.className = 'ed-wave-period-input';
|
|
267
|
+
input.setAttribute('data-focus-key', 'ed-wave-lane-' + key + '-input');
|
|
268
|
+
input.setAttribute('inputmode', 'decimal');
|
|
269
|
+
const now = laneObj[key];
|
|
270
|
+
input.value = now === undefined || now === null ? '' : String(now);
|
|
271
|
+
input.title = key === 'period'
|
|
272
|
+
? '每個波形字元佔幾拍(空白=1)' : '整條往左移幾拍,可以是小數(空白=0)';
|
|
273
|
+
let composing = false;
|
|
274
|
+
let written = input.value;
|
|
275
|
+
const commitNow = function () {
|
|
276
|
+
if (composing || input.value === written) return;
|
|
277
|
+
written = input.value;
|
|
278
|
+
commitLaneField(at, key, input.value);
|
|
279
|
+
};
|
|
280
|
+
input.addEventListener('compositionstart', function () { composing = true; });
|
|
281
|
+
input.addEventListener('compositionend', function () { composing = false; });
|
|
282
|
+
input.addEventListener('keydown', function (ev) {
|
|
283
|
+
if (composing || ev.isComposing === true || ev.keyCode === 229) return;
|
|
284
|
+
if (ev.key !== 'Enter') return;
|
|
285
|
+
ev.preventDefault();
|
|
286
|
+
ev.stopPropagation();
|
|
287
|
+
commitNow();
|
|
288
|
+
});
|
|
289
|
+
input.addEventListener('change', commitNow);
|
|
290
|
+
commits.push(commitNow);
|
|
291
|
+
row.appendChild(input);
|
|
292
|
+
el.appendChild(row);
|
|
293
|
+
}
|
|
294
|
+
el.flush = function () { for (const c of commits) c(); };
|
|
295
|
+
return el;
|
|
166
296
|
}
|
|
167
297
|
|
|
168
298
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
299
|
+
* ⚙ 整張圖 (spec section 4.6): the document-wide settings the side rail's
|
|
300
|
+
* 文件 section used to hold. Top level: 標題 (`head.text`, a multi-line
|
|
301
|
+
* box), 每拍寬度 (`config.hscale` as 1× 2× 3× 4× — the engine rounds it to
|
|
302
|
+
* an integer, so only integers are offered), and, when a data label is
|
|
303
|
+
* wider than its segment, a card saying how many with 「改成每拍 K×」. Under
|
|
304
|
+
* 進階 (folded when the popover opens): `head.tick` / `tock` / `every`,
|
|
305
|
+
* `foot.text`, `foot.tick` / `tock` / `every`.
|
|
306
|
+
*
|
|
307
|
+
* `opts`:
|
|
308
|
+
* doc the document now (read for the current values)
|
|
309
|
+
* overflow `[{needScale}, ...]` — the labels wider than their segment
|
|
310
|
+
* scale the smallest scale that fits them all (K), when there are any
|
|
311
|
+
* actions `setHeadText(text)`, `setScale(k, fromCard)`, `setBanner(which, key, value)`
|
|
172
312
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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.
|
|
313
|
+
* A text field is written when it is committed: Enter (Shift+Enter is a new
|
|
314
|
+
* line in 標題), leaving it, or `flush()` — which wave-ui calls when the
|
|
315
|
+
* popover closes, because a field taken out of the page never fires
|
|
316
|
+
* `change` and 「改了立即生效」 must not lose what was typed. A field whose
|
|
317
|
+
* text has not changed since it was last written writes nothing. While an
|
|
318
|
+
* input method is composing, Enter belongs to it (spec section 7.2).
|
|
182
319
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
320
|
+
* Clearing a field writes `''`, which `codec.setBannerField` turns into a
|
|
321
|
+
* DELETED key. A numeral in tick / tock / every is written as a number:
|
|
322
|
+
* the pinned engine adds the hbounds offset to them with `+`
|
|
323
|
+
* (parse-config.js `source.head.tick + lane.xmin_cfg/2`), so a STRING
|
|
324
|
+
* `"5"` becomes `"50"` and the ruler starts at 50.
|
|
325
|
+
*
|
|
326
|
+
* Returns the popover element. `el.refresh({doc, overflow, scale})`
|
|
327
|
+
* brings it in line with a repaint in place — a field holding unwritten
|
|
328
|
+
* text keeps it — and `el.flush()` writes every field with unwritten text.
|
|
188
329
|
*/
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
330
|
+
function settingsPopover(anchor, opts) {
|
|
331
|
+
const actions = opts.actions;
|
|
332
|
+
const el = popover('ed-wave-settings-pop');
|
|
333
|
+
el.setAttribute('role', 'group');
|
|
334
|
+
el.setAttribute('aria-label', '整張圖');
|
|
335
|
+
if (anchor && typeof anchor.getAttribute === 'function') {
|
|
336
|
+
el.setAttribute('data-for', anchor.getAttribute('data-focus-key') || '');
|
|
337
|
+
}
|
|
338
|
+
const fields = [];
|
|
339
|
+
|
|
340
|
+
function caption(parent, text) {
|
|
341
|
+
const c = d.createElement('div');
|
|
342
|
+
c.className = 'ed-wave-set-cap';
|
|
343
|
+
c.textContent = text;
|
|
344
|
+
parent.appendChild(c);
|
|
345
|
+
return c;
|
|
346
|
+
}
|
|
347
|
+
/** One text field bound to `read(doc)` and written by `write(text)`. */
|
|
348
|
+
function textField(parent, key, label, read, write, multi, spoken) {
|
|
349
|
+
const row = d.createElement('label');
|
|
350
|
+
row.className = 'ed-wave-set-row';
|
|
351
|
+
const cap = d.createElement('span');
|
|
352
|
+
cap.className = 'ed-wave-set-label';
|
|
353
|
+
cap.textContent = label;
|
|
354
|
+
row.appendChild(cap);
|
|
355
|
+
const input = d.createElement(multi ? 'textarea' : 'input');
|
|
356
|
+
if (multi) input.rows = 2;
|
|
357
|
+
else input.type = 'text';
|
|
358
|
+
input.className = 'ed-wave-set-input';
|
|
359
|
+
input.setAttribute('data-focus-key', key);
|
|
360
|
+
input.setAttribute('aria-label', spoken || label);
|
|
361
|
+
row.appendChild(input);
|
|
362
|
+
parent.appendChild(row);
|
|
363
|
+
const f = { input: input, read: read, written: '' };
|
|
364
|
+
f.commit = function () {
|
|
365
|
+
if (input.value === f.written) return;
|
|
366
|
+
f.written = input.value;
|
|
367
|
+
write(input.value);
|
|
368
|
+
};
|
|
369
|
+
let composing = false;
|
|
370
|
+
input.addEventListener('compositionstart', function () { composing = true; });
|
|
371
|
+
input.addEventListener('compositionend', function () { composing = false; });
|
|
372
|
+
input.addEventListener('keydown', function (ev) {
|
|
373
|
+
if (composing || ev.isComposing === true || ev.keyCode === 229) return;
|
|
374
|
+
if (ev.key !== 'Enter' || ev.shiftKey) return;
|
|
375
|
+
ev.preventDefault();
|
|
376
|
+
ev.stopPropagation();
|
|
377
|
+
f.commit();
|
|
378
|
+
});
|
|
379
|
+
input.addEventListener('change', function () { if (!composing) f.commit(); });
|
|
380
|
+
fields.push(f);
|
|
381
|
+
return f;
|
|
382
|
+
}
|
|
383
|
+
function bannerValue(key, text) {
|
|
384
|
+
return key !== 'text' && /^-?\d+(\.\d+)?$/.test(text.trim()) ? Number(text) : text;
|
|
385
|
+
}
|
|
386
|
+
function bannerField(parent, which, key, label) {
|
|
387
|
+
// Spoken with its band: two fields on screen are both captioned 標題,
|
|
388
|
+
// and three pairs tick / tock / every.
|
|
389
|
+
const spoken = (which === 'head' ? '上方' : '下方') + (key === 'text' ? '標題' : '刻度 ' + key);
|
|
390
|
+
textField(parent, 'ed-wave-set-' + which + '-' + key, label,
|
|
391
|
+
function (doc) {
|
|
392
|
+
const b = doc[which];
|
|
393
|
+
return b !== null && typeof b === 'object' ? tickTockText(b[key]) : '';
|
|
394
|
+
},
|
|
395
|
+
function (text) { actions.setBanner(which, key, bannerValue(key, text)); }, false, spoken);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
textField(el, 'ed-wave-set-head', '標題',
|
|
399
|
+
function (doc) { return doc.head && typeof doc.head === 'object' ? tickTockText(doc.head.text) : ''; },
|
|
400
|
+
function (text) { actions.setHeadText(text); }, true);
|
|
401
|
+
|
|
402
|
+
caption(el, '每拍寬度');
|
|
403
|
+
const scales = d.createElement('div');
|
|
404
|
+
scales.className = 'ed-wave-set-scales';
|
|
405
|
+
scales.setAttribute('role', 'group');
|
|
406
|
+
scales.setAttribute('aria-label', '每拍寬度');
|
|
407
|
+
const scaleBtns = [];
|
|
408
|
+
for (let k = 1; k <= 4; k++) {
|
|
409
|
+
const b = toolButton('ed-wave-set-scale', k + '×', '每拍寬度 ' + k + ' 倍(config.hscale)', function () {
|
|
410
|
+
actions.setScale(k);
|
|
411
|
+
}, scales);
|
|
412
|
+
b.setAttribute('data-focus-key', 'ed-wave-set-scale-' + k);
|
|
413
|
+
b.setAttribute('aria-pressed', 'false');
|
|
414
|
+
scaleBtns.push(b);
|
|
415
|
+
}
|
|
416
|
+
el.appendChild(scales);
|
|
417
|
+
|
|
418
|
+
// The overflow card (spec section 4.6, and section 6 for K > 4).
|
|
419
|
+
const card = d.createElement('div');
|
|
420
|
+
card.className = 'ed-wave-set-overflow';
|
|
421
|
+
card.hidden = true;
|
|
422
|
+
const cardText = d.createElement('span');
|
|
423
|
+
cardText.className = 'ed-wave-set-overflow-text';
|
|
424
|
+
card.appendChild(cardText);
|
|
425
|
+
const fix = toolButton('ed-wave-set-fix', '', '', function () {
|
|
426
|
+
const k = Number(fix.getAttribute('data-scale'));
|
|
427
|
+
// `true`: from the card, whose button goes away with what it fixes.
|
|
428
|
+
if (k > 0) actions.setScale(k, true);
|
|
429
|
+
}, card);
|
|
430
|
+
el.appendChild(card);
|
|
431
|
+
|
|
432
|
+
const advBtn = toolButton('ed-wave-set-advanced', '進階', '上方刻度、下方標題與下方刻度', function () {
|
|
433
|
+
const open = advBtn.getAttribute('aria-expanded') !== 'true';
|
|
434
|
+
advBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
435
|
+
adv.hidden = !open;
|
|
436
|
+
}, el);
|
|
437
|
+
advBtn.setAttribute('aria-expanded', 'false');
|
|
438
|
+
const adv = d.createElement('div');
|
|
439
|
+
adv.className = 'ed-wave-set-adv';
|
|
440
|
+
adv.hidden = true;
|
|
441
|
+
caption(adv, '上方刻度');
|
|
442
|
+
bannerField(adv, 'head', 'tick', 'tick');
|
|
443
|
+
bannerField(adv, 'head', 'tock', 'tock');
|
|
444
|
+
bannerField(adv, 'head', 'every', 'every');
|
|
445
|
+
caption(adv, '下方');
|
|
446
|
+
bannerField(adv, 'foot', 'text', '標題');
|
|
447
|
+
bannerField(adv, 'foot', 'tick', 'tick');
|
|
448
|
+
bannerField(adv, 'foot', 'tock', 'tock');
|
|
449
|
+
bannerField(adv, 'foot', 'every', 'every');
|
|
450
|
+
el.appendChild(adv);
|
|
451
|
+
|
|
452
|
+
function refresh(state) {
|
|
453
|
+
const doc = state.doc;
|
|
454
|
+
for (const f of fields) {
|
|
455
|
+
const now = f.read(doc);
|
|
456
|
+
// Text typed and not yet written stays; everything else follows the
|
|
457
|
+
// document (an undo, the other fields' writes).
|
|
458
|
+
if (f.input.value !== f.written) { f.written = now; continue; }
|
|
459
|
+
f.written = now;
|
|
460
|
+
if (f.input.value !== now) f.input.value = now;
|
|
461
|
+
}
|
|
462
|
+
const cfg = doc.config !== null && typeof doc.config === 'object' ? doc.config : {};
|
|
463
|
+
const hscale = typeof cfg.hscale === 'number' ? cfg.hscale : (cfg.hscale === undefined ? 1 : null);
|
|
464
|
+
scaleBtns.forEach(function (b, i) { b.setAttribute('aria-pressed', hscale === i + 1 ? 'true' : 'false'); });
|
|
465
|
+
const list = state.overflow || [];
|
|
466
|
+
const k = state.scale;
|
|
467
|
+
card.hidden = list.length === 0;
|
|
468
|
+
if (list.length === 0) {
|
|
469
|
+
fix.removeAttribute('data-scale');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
cardText.textContent = list.length + ' 個資料標籤比它那一段寬' +
|
|
473
|
+
(k > 4 ? ';需要每拍 ' + k + '×,圖會比內文寬並被縮小' : '');
|
|
474
|
+
fix.textContent = '改成每拍 ' + k + '×';
|
|
475
|
+
fix.title = '把每拍寬度改成 ' + k + ' 倍,讓每個標籤都放得下';
|
|
476
|
+
fix.setAttribute('data-scale', String(k));
|
|
477
|
+
}
|
|
478
|
+
for (const f of fields) {
|
|
479
|
+
f.written = f.read(opts.doc);
|
|
480
|
+
f.input.value = f.written;
|
|
481
|
+
}
|
|
482
|
+
refresh({ doc: opts.doc, overflow: opts.overflow, scale: opts.scale });
|
|
483
|
+
el.refresh = refresh;
|
|
484
|
+
el.flush = function () { for (const f of fields) f.commit(); };
|
|
485
|
+
return el;
|
|
225
486
|
}
|
|
226
487
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
488
|
+
/**
|
|
489
|
+
* 匯出 ⌄ (spec section 4.6): 複製為圖片, 下載 PNG, 下載 SVG, 複製 WaveJSON,
|
|
490
|
+
* and the line that says every export is light on white. `acts` holds
|
|
491
|
+
* what each does (`copyImage`, `downloadPng`, `downloadSvg`, `copyJson`);
|
|
492
|
+
* wave-ui closes the menu and runs it in the same click, which 複製為圖片
|
|
493
|
+
* needs (the clipboard item has to be made inside the press).
|
|
494
|
+
*/
|
|
495
|
+
function exportMenu(anchor, acts) {
|
|
496
|
+
const el = menu('ed-wave-export-pop', [
|
|
497
|
+
{ key: 'ed-wave-export-copyimg', label: '複製為圖片', title: '把這張波形的 PNG(2 倍大小)放進剪貼簿', run: acts.copyImage },
|
|
498
|
+
{ key: 'ed-wave-export-png', label: '下載 PNG', title: '存成 PNG 檔(白底,2 倍大小)', run: acts.downloadPng },
|
|
499
|
+
{ key: 'ed-wave-export-svg', label: '下載 SVG', title: '存成 SVG 檔(自帶 WaveDrom 的 skin,離開這一頁也畫得出來)', run: acts.downloadSvg },
|
|
500
|
+
{ key: 'ed-wave-export-json', label: '複製 WaveJSON', title: '把這張波形的 WaveJSON 複製到剪貼簿', run: acts.copyJson },
|
|
501
|
+
]);
|
|
502
|
+
const note = d.createElement('div');
|
|
503
|
+
note.className = 'ed-wave-export-note';
|
|
504
|
+
note.textContent = '匯出一律是淺色、白底';
|
|
505
|
+
el.appendChild(note);
|
|
506
|
+
if (anchor && typeof anchor.getAttribute === 'function') {
|
|
507
|
+
el.setAttribute('data-for', anchor.getAttribute('data-focus-key') || '');
|
|
508
|
+
}
|
|
509
|
+
return el;
|
|
510
|
+
}
|
|
245
511
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
512
|
+
/**
|
|
513
|
+
* The grid 更多 opens (spec section 4.2): one titled row per group.
|
|
514
|
+
* `groups` is `[[title, [ch, ...]], ...]`; `makeBrush(ch)` builds the same
|
|
515
|
+
* button the toolbar's resident brushes use, so a brush looks and acts the
|
|
516
|
+
* same wherever it is picked from.
|
|
517
|
+
*/
|
|
518
|
+
function brushGrid(groups, makeBrush) {
|
|
519
|
+
const el = popover('ed-wave-brush-grid');
|
|
520
|
+
for (const g of groups) {
|
|
521
|
+
const row = d.createElement('div');
|
|
522
|
+
row.className = 'ed-wave-brush-group';
|
|
523
|
+
const title = d.createElement('span');
|
|
524
|
+
title.className = 'ed-wave-brush-group-title';
|
|
525
|
+
title.textContent = g[0];
|
|
526
|
+
row.appendChild(title);
|
|
527
|
+
const list = d.createElement('div');
|
|
528
|
+
list.className = 'ed-wave-brushes';
|
|
529
|
+
for (const ch of g[1]) list.appendChild(makeBrush(ch));
|
|
530
|
+
row.appendChild(list);
|
|
531
|
+
el.appendChild(row);
|
|
532
|
+
}
|
|
533
|
+
return el;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The shortcut card (spec section 4.8, Task 11): a titled card with one
|
|
538
|
+
* section per group. `groups` is `[[title, [[[key, ...], what], ...]], ...]`
|
|
539
|
+
* (wave-ui.js `shortcutGroups`); each key is a `kbd`. `onClose` runs
|
|
540
|
+
* from its ✕. wave-ui.js places it in the middle of the editor and owns
|
|
541
|
+
* opening and closing it, like every other popover.
|
|
542
|
+
*/
|
|
543
|
+
function shortcutCard(groups, onClose) {
|
|
544
|
+
const el = popover('ed-wave-shortcuts');
|
|
545
|
+
el.setAttribute('role', 'dialog');
|
|
546
|
+
el.setAttribute('aria-label', '全部快捷鍵');
|
|
547
|
+
el.setAttribute('tabindex', '-1');
|
|
548
|
+
const head = d.createElement('div');
|
|
549
|
+
head.className = 'ed-wave-shortcuts-head';
|
|
550
|
+
const title = d.createElement('span');
|
|
551
|
+
title.className = 'ed-wave-shortcuts-title';
|
|
552
|
+
title.textContent = '全部快捷鍵';
|
|
553
|
+
head.appendChild(title);
|
|
554
|
+
const x = toolButton('ed-wave-shortcuts-close', '✕', '關閉(Esc)', onClose, head);
|
|
555
|
+
x.setAttribute('aria-label', '關閉快捷鍵');
|
|
556
|
+
el.appendChild(head);
|
|
557
|
+
const body = d.createElement('div');
|
|
558
|
+
body.className = 'ed-wave-shortcuts-body';
|
|
559
|
+
for (const g of groups) {
|
|
560
|
+
const sec = d.createElement('section');
|
|
561
|
+
sec.className = 'ed-wave-shortcut-group';
|
|
562
|
+
const h = d.createElement('div');
|
|
563
|
+
h.className = 'ed-wave-shortcut-title';
|
|
564
|
+
h.textContent = g[0];
|
|
565
|
+
sec.appendChild(h);
|
|
566
|
+
for (const row of g[1]) {
|
|
567
|
+
const r = d.createElement('div');
|
|
568
|
+
r.className = 'ed-wave-shortcut-row';
|
|
569
|
+
const keys = d.createElement('span');
|
|
570
|
+
keys.className = 'ed-wave-shortcut-keys';
|
|
571
|
+
for (const k of row[0]) {
|
|
572
|
+
const kbd = d.createElement('kbd');
|
|
573
|
+
kbd.textContent = k;
|
|
574
|
+
keys.appendChild(kbd);
|
|
575
|
+
}
|
|
576
|
+
r.appendChild(keys);
|
|
577
|
+
const what = d.createElement('span');
|
|
578
|
+
what.className = 'ed-wave-shortcut-what';
|
|
579
|
+
what.textContent = row[1];
|
|
580
|
+
r.appendChild(what);
|
|
581
|
+
sec.appendChild(r);
|
|
582
|
+
}
|
|
583
|
+
body.appendChild(sec);
|
|
584
|
+
}
|
|
585
|
+
el.appendChild(body);
|
|
586
|
+
return el;
|
|
587
|
+
}
|
|
264
588
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
589
|
+
/**
|
|
590
|
+
* The floating toolbar over a selected run of cycles (spec section 4.3):
|
|
591
|
+
* 電位 ⌄, 複製, 刪除 N 拍, 前插, 後插, 關聯線 (A), 標註 (T), in that order.
|
|
592
|
+
* The last two are W5's, folded in here; like the rest, what they do
|
|
593
|
+
* arrives from wave-ui.js in `actions`.
|
|
594
|
+
*
|
|
595
|
+
* `anchor` is `{left, top, width}`: where the toolbar goes, in its
|
|
596
|
+
* container's coordinates. wave-ui.js measures the toolbar once it is on
|
|
597
|
+
* screen and moves it (above the run, or below it when there is no room),
|
|
598
|
+
* so this only sets the first position.
|
|
599
|
+
*
|
|
600
|
+
* A press with the pointer does not take the keyboard off the canvas
|
|
601
|
+
* (`mousedown` is cancelled), so the arrows, Del and Ctrl+V keep working
|
|
602
|
+
* on the drawing after a button is clicked. Tab still reaches every
|
|
603
|
+
* button, and a button pressed from the keyboard keeps it.
|
|
604
|
+
*/
|
|
605
|
+
function rangeToolbar(anchor, opts) {
|
|
606
|
+
const count = opts.count;
|
|
607
|
+
const actions = opts.actions;
|
|
608
|
+
const el = d.createElement('div');
|
|
609
|
+
el.className = 'ed-wave-range';
|
|
610
|
+
el.setAttribute('role', 'toolbar');
|
|
611
|
+
el.setAttribute('aria-label', '選取的 ' + count + ' 拍');
|
|
612
|
+
el.style.left = anchor.left + 'px';
|
|
613
|
+
el.style.top = anchor.top + 'px';
|
|
614
|
+
const items = [
|
|
615
|
+
['ed-wave-range-level', '電位 ⌄', '把這 ' + count + ' 拍改成同一個電位', actions.level],
|
|
616
|
+
['ed-wave-range-copy', '複製', '複製這 ' + count + ' 拍(在畫布上按 Ctrl+V 貼到選取的位置前面)', actions.copy],
|
|
617
|
+
['ed-wave-range-delete', '刪除 ' + count + ' 拍', '刪掉這 ' + count + ' 拍(每一條訊號同時變窄;Del 也可以)', actions.remove],
|
|
618
|
+
['ed-wave-range-before', '前插', '在這一段前面插入一拍', actions.insertBefore],
|
|
619
|
+
['ed-wave-range-after', '後插', '在這一段後面插入一拍', actions.insertAfter],
|
|
620
|
+
['ed-wave-range-edge', '關聯線 A', '從這一拍拉一條關聯線(A)', actions.edge],
|
|
621
|
+
['ed-wave-range-note', '標註 T', '在這一拍加一個標註(T)', actions.note],
|
|
622
|
+
];
|
|
623
|
+
for (const it of items) {
|
|
624
|
+
const run = it[3];
|
|
625
|
+
const b = toolButton(it[0], it[1], it[2], function () { run(b); }, el);
|
|
626
|
+
b.addEventListener('mousedown', function (ev) { ev.preventDefault(); });
|
|
627
|
+
if (it[0] === 'ed-wave-range-level') {
|
|
628
|
+
b.setAttribute('aria-haspopup', 'true');
|
|
629
|
+
b.setAttribute('aria-expanded', 'false');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return el;
|
|
276
633
|
}
|
|
277
634
|
|
|
278
635
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
636
|
+
* The floating toolbar beside a selected edge's label (spec section 4.5):
|
|
637
|
+
* 線型 (直 曲 折) and 箭頭 (無 單向 雙向) as two button groups, 更多 (the
|
|
638
|
+
* other eleven shapes, listed inside the toolbar when it is opened),
|
|
639
|
+
* 改標籤 and 刪除. `opts.shape` is the edge's shape now, which decides the
|
|
640
|
+
* pressed line and head (`shapeParts`); every press goes to wave-ui.js
|
|
641
|
+
* through `opts.actions`: `setLine(line)`, `setHead(head)`, `more(shape)`,
|
|
642
|
+
* `editLabel()`, `remove()`, and `resized()` (optional) after 更多 opens
|
|
643
|
+
* or folds, so wave-ui.js can move the taller or shorter toolbar.
|
|
644
|
+
*
|
|
645
|
+
* Like the range toolbar, a press with the pointer does not take the
|
|
646
|
+
* keyboard off the canvas, so Del and Enter keep meaning the selected
|
|
647
|
+
* edge; Tab still reaches every button.
|
|
287
648
|
*/
|
|
288
|
-
function
|
|
289
|
-
|
|
649
|
+
function edgeToolbar(anchor, opts) {
|
|
650
|
+
const actions = opts.actions;
|
|
651
|
+
const parts = shapeParts(opts.shape);
|
|
652
|
+
const el = barShell(anchor, '關聯線');
|
|
653
|
+
const group = function (label) {
|
|
654
|
+
const g = d.createElement('div');
|
|
655
|
+
g.className = 'ed-wave-edge-group';
|
|
656
|
+
g.setAttribute('role', 'group');
|
|
657
|
+
g.setAttribute('aria-label', label);
|
|
658
|
+
el.appendChild(g);
|
|
659
|
+
return g;
|
|
660
|
+
};
|
|
661
|
+
const lines = group('線型');
|
|
662
|
+
for (const [line, text, title] of [['straight', '直', '直線'], ['curve', '曲', '曲線'], ['elbow', '折', '折線']]) {
|
|
663
|
+
const b = barButton('ed-wave-edge-line-' + line, text, title, function () { actions.setLine(line); }, lines);
|
|
664
|
+
b.setAttribute('aria-pressed', parts.line === line ? 'true' : 'false');
|
|
665
|
+
}
|
|
666
|
+
const heads = group('箭頭');
|
|
667
|
+
for (const [head, text, title] of [['none', '無', '沒有箭頭'], ['end', '單向', '終點一個箭頭'], ['both', '雙向', '兩端都有箭頭']]) {
|
|
668
|
+
const b = barButton('ed-wave-edge-head-' + head, text, title, function () { actions.setHead(head); }, heads);
|
|
669
|
+
b.setAttribute('aria-pressed', parts.head === head ? 'true' : 'false');
|
|
670
|
+
}
|
|
671
|
+
const list = d.createElement('div');
|
|
672
|
+
list.className = 'ed-wave-edge-more-list';
|
|
673
|
+
list.setAttribute('role', 'group');
|
|
674
|
+
list.setAttribute('aria-label', '其他形狀');
|
|
675
|
+
list.hidden = true;
|
|
676
|
+
const moreBtn = barButton('ed-wave-edge-more', '更多 ⌄', '其他 11 種形狀(半曲、中折、先直後橫、+)', function () {
|
|
677
|
+
list.hidden = !list.hidden;
|
|
678
|
+
moreBtn.setAttribute('aria-expanded', list.hidden ? 'false' : 'true');
|
|
679
|
+
if (typeof actions.resized === 'function') actions.resized();
|
|
680
|
+
}, el);
|
|
681
|
+
moreBtn.setAttribute('aria-expanded', 'false');
|
|
682
|
+
barButton('ed-wave-edge-label', '改標籤', '在線上改標籤(Enter 或雙擊標籤也可以)', function () { actions.editLabel(); }, el);
|
|
683
|
+
barButton('ed-wave-edge-delete', '刪除', '刪掉這條關聯線(Del 也可以)', function () { actions.remove(); }, el)
|
|
684
|
+
.classList.add('is-danger');
|
|
685
|
+
MORE_SHAPES.forEach(function (shape, i) {
|
|
686
|
+
const b = barButton('ed-wave-edge-shape-' + i, shape, '形狀 ' + shape, function () { actions.more(shape); }, list);
|
|
687
|
+
b.setAttribute('data-shape', shape);
|
|
688
|
+
b.setAttribute('aria-pressed', opts.shape === shape ? 'true' : 'false');
|
|
689
|
+
});
|
|
690
|
+
el.appendChild(list);
|
|
691
|
+
return el;
|
|
290
692
|
}
|
|
291
693
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
function
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
694
|
+
/**
|
|
695
|
+
* The same floating toolbar for a selected point note (spec section 4.5):
|
|
696
|
+
* 改文字, 拉成關聯線, 刪除 — `opts.actions.editText()`, `toEdge()`,
|
|
697
|
+
* `remove()`.
|
|
698
|
+
*/
|
|
699
|
+
function noteToolbar(anchor, opts) {
|
|
700
|
+
const actions = opts.actions;
|
|
701
|
+
const el = barShell(anchor, '標註');
|
|
702
|
+
barButton('ed-wave-note-text', '改文字', '在標註上改文字(Enter 或雙擊也可以)', function () { actions.editText(); }, el);
|
|
703
|
+
barButton('ed-wave-note-to-edge', '拉成關聯線', '從這個標註的位置拉一條關聯線,文字變成線的標籤', function () { actions.toEdge(); }, el);
|
|
704
|
+
barButton('ed-wave-note-delete', '刪除', '刪掉這個標註(Del 也可以)', function () { actions.remove(); }, el)
|
|
705
|
+
.classList.add('is-danger');
|
|
706
|
+
return el;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function barShell(anchor, label) {
|
|
710
|
+
const el = d.createElement('div');
|
|
711
|
+
el.className = 'ed-wave-edge-bar';
|
|
712
|
+
el.setAttribute('role', 'toolbar');
|
|
713
|
+
el.setAttribute('aria-label', label);
|
|
714
|
+
el.style.left = anchor.left + 'px';
|
|
715
|
+
el.style.top = anchor.top + 'px';
|
|
716
|
+
return el;
|
|
717
|
+
}
|
|
718
|
+
function barButton(key, text, title, run, parent) {
|
|
719
|
+
const b = toolButton(key, text, title, run, parent);
|
|
720
|
+
b.addEventListener('mousedown', function (ev) { ev.preventDefault(); });
|
|
721
|
+
return b;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* One line of text typed where it is drawn (spec section 4.5): an edge's
|
|
726
|
+
* label or a point note's text, `input.ed-wave-edge-input[data-kind]`.
|
|
727
|
+
* wave-ui.js places it and decides what a commit writes — and what
|
|
728
|
+
* Escape means, because for a label field opened on a brand-new edge
|
|
729
|
+
* Escape takes the edge back off (the dialog's own Escape layering). This
|
|
730
|
+
* owns Enter and the composition: Enter commits, but never while an input
|
|
731
|
+
* method is composing (spec section 7.2), and neither does the `change` or
|
|
732
|
+
* `blur` a committed composition can fire.
|
|
733
|
+
*
|
|
734
|
+
* `opts`: `kind` ('label' | 'note'), `value`, `focusKey`, `label` (the
|
|
735
|
+
* field's accessible name), `onCommit(value)`.
|
|
736
|
+
*/
|
|
737
|
+
function inlineInput(opts) {
|
|
303
738
|
const input = d.createElement('input');
|
|
304
739
|
input.type = 'text';
|
|
305
|
-
input.className =
|
|
306
|
-
input.setAttribute('data-
|
|
307
|
-
input.setAttribute('data-
|
|
308
|
-
input.
|
|
309
|
-
|
|
310
|
-
|
|
740
|
+
input.className = 'ed-wave-edge-input';
|
|
741
|
+
input.setAttribute('data-kind', opts.kind);
|
|
742
|
+
input.setAttribute('data-focus-key', opts.focusKey);
|
|
743
|
+
input.setAttribute('aria-label', opts.label);
|
|
744
|
+
input.value = opts.value;
|
|
745
|
+
let composing = false;
|
|
746
|
+
input.addEventListener('compositionstart', function () { composing = true; });
|
|
747
|
+
input.addEventListener('compositionend', function () { composing = false; });
|
|
748
|
+
input.addEventListener('keydown', function (ev) {
|
|
749
|
+
if (composing || ev.isComposing === true || ev.keyCode === 229) return;
|
|
750
|
+
if (ev.key !== 'Enter') return;
|
|
751
|
+
ev.preventDefault();
|
|
752
|
+
ev.stopPropagation();
|
|
753
|
+
opts.onCommit(input.value);
|
|
754
|
+
});
|
|
755
|
+
input.addEventListener('change', function () { if (!composing) opts.onCommit(input.value); });
|
|
756
|
+
input.addEventListener('blur', function () { if (!composing) opts.onCommit(input.value); });
|
|
311
757
|
return input;
|
|
312
758
|
}
|
|
313
759
|
|
|
760
|
+
/**
|
|
761
|
+
* The notice card under the toolbar (spec section 4.9): neutral, and it can
|
|
762
|
+
* be folded away. `show(sentences)` fills it; no sentences hides it. The
|
|
763
|
+
* folded state lives on the card for the rest of the session, so a repaint
|
|
764
|
+
* does not unfold what the user folded.
|
|
765
|
+
*/
|
|
766
|
+
function noticeCard() {
|
|
767
|
+
const el = d.createElement('div');
|
|
768
|
+
el.className = 'ed-wave-notice';
|
|
769
|
+
el.hidden = true;
|
|
770
|
+
const text = d.createElement('div');
|
|
771
|
+
text.className = 'ed-wave-notice-text';
|
|
772
|
+
el.appendChild(text);
|
|
773
|
+
const fold = d.createElement('button');
|
|
774
|
+
fold.type = 'button';
|
|
775
|
+
fold.className = 'ed-wave-notice-fold';
|
|
776
|
+
fold.setAttribute('data-focus-key', 'ed-wave-notice-fold');
|
|
777
|
+
fold.textContent = '收起';
|
|
778
|
+
fold.addEventListener('click', function () {
|
|
779
|
+
const folded = el.hasAttribute('data-folded');
|
|
780
|
+
if (folded) el.removeAttribute('data-folded');
|
|
781
|
+
else el.setAttribute('data-folded', '');
|
|
782
|
+
fold.textContent = folded ? '收起' : '展開';
|
|
783
|
+
fold.setAttribute('aria-expanded', folded ? 'true' : 'false');
|
|
784
|
+
});
|
|
785
|
+
fold.setAttribute('aria-expanded', 'true');
|
|
786
|
+
el.appendChild(fold);
|
|
787
|
+
function show(sentences) {
|
|
788
|
+
el.hidden = sentences.length === 0;
|
|
789
|
+
text.textContent = sentences.join(' ');
|
|
790
|
+
}
|
|
791
|
+
return { el: el, show: show };
|
|
792
|
+
}
|
|
793
|
+
|
|
314
794
|
/**
|
|
315
795
|
* A numeral typed into `hscale`/`period`/`phase` is committed as a real
|
|
316
|
-
* JS `number`, not as the string the `<input>` holds
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* `
|
|
796
|
+
* JS `number`, not as the string the `<input>` holds. (The banner fields
|
|
797
|
+
* do the same for tick / tock / every since Task 10 — see `bannerValue`
|
|
798
|
+
* in `settingsPopover` for the engine's `+` that turns a string `"5"`
|
|
799
|
+
* into `"50"`; their `.text` stays a string.) For these three:
|
|
320
800
|
* `wave-geometry.hscaleOf`/`layoutOf` gate on `typeof … === 'number'`, so
|
|
321
|
-
* a STRING `"2"` would silently read as "unset"
|
|
322
|
-
*
|
|
801
|
+
* a STRING `"2"` would silently read as "unset" to the editor's geometry —
|
|
802
|
+
* its default of 1/0 — while the engine that draws the canvas still
|
|
323
803
|
* honours it (its `tonumber()` coerces a string with `>`). That is the
|
|
324
804
|
* exact same-idea-two-definitions divergence this batch has already paid
|
|
325
805
|
* for three times elsewhere (see `md2doc`'s CLAUDE.md). Blank clears the
|
|
326
806
|
* field (the `undefined` sentinel `setConfigField`/`setLaneField` both
|
|
327
807
|
* read); anything that is not a plain, trimmed number is left as the raw
|
|
328
|
-
* string — both
|
|
808
|
+
* string — both the geometry's `typeof` gate and the engine's own
|
|
329
809
|
* `tonumber()` already fall back to the SAME default (1 for hscale/period,
|
|
330
810
|
* 0 for phase) for a non-numeric value, so there is nothing to reconcile
|
|
331
811
|
* for that case, only for a well-formed one.
|
|
@@ -336,7 +816,7 @@ function createPanels(deps) {
|
|
|
336
816
|
}
|
|
337
817
|
|
|
338
818
|
function commitConfigField(key, value) {
|
|
339
|
-
actions.commit('config.' + key, function (doc) {
|
|
819
|
+
return actions.commit('config.' + key, function (doc) {
|
|
340
820
|
return codec.setConfigField(doc, key, parseNumericField(value));
|
|
341
821
|
});
|
|
342
822
|
}
|
|
@@ -347,6 +827,65 @@ function createPanels(deps) {
|
|
|
347
827
|
});
|
|
348
828
|
}
|
|
349
829
|
|
|
830
|
+
/**
|
|
831
|
+
* A copy of `node` with the value at `path` replaced, sharing every subtree
|
|
832
|
+
* it does not have to copy — so the store's lane-identity bookkeeping
|
|
833
|
+
* (`remapOrigins` matches lanes by identity across a structural edit) still
|
|
834
|
+
* sees each untouched lane as the same object. The codec's lane operations
|
|
835
|
+
* cover lanes; a group's own title and its place among its siblings are
|
|
836
|
+
* the two writes they do not, and both go through this.
|
|
837
|
+
*/
|
|
838
|
+
function replaceAt(node, path, value) {
|
|
839
|
+
if (path.length === 0) return value;
|
|
840
|
+
const seg = path[0];
|
|
841
|
+
const child = replaceAt(node[seg], path.slice(1), value);
|
|
842
|
+
const out = Array.isArray(node) ? node.slice() : Object.assign({}, node);
|
|
843
|
+
out[seg] = child;
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** The group at `path` gets the title `value` (spec section 4.4, 改名). */
|
|
848
|
+
function commitGroupTitle(path, value) {
|
|
849
|
+
return actions.commit('rename-group', function (doc) {
|
|
850
|
+
const group = geometry.valueAt(doc, path);
|
|
851
|
+
if (!Array.isArray(group) || typeof group[0] !== 'string' || group[0] === value) return doc;
|
|
852
|
+
return replaceAt(doc, path.concat([0]), value);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Where the group at `path` lands when it trades places with its neighbour
|
|
858
|
+
* one step `dir` (-1 up, 1 down) in the array that holds it — a lane, a
|
|
859
|
+
* blank row or another group — or null when there is none that way. The
|
|
860
|
+
* title at index 0 of a titled parent group is not a neighbour.
|
|
861
|
+
*/
|
|
862
|
+
function groupStepPath(doc, path, dir) {
|
|
863
|
+
const parentPath = path.slice(0, -1);
|
|
864
|
+
const parent = geometry.valueAt(doc, parentPath);
|
|
865
|
+
if (!Array.isArray(parent)) return null;
|
|
866
|
+
const first = parentPath.length > 1 && typeof parent[0] === 'string' ? 1 : 0;
|
|
867
|
+
const at = path[path.length - 1];
|
|
868
|
+
const to = at + dir;
|
|
869
|
+
if (to < first || to >= parent.length) return null;
|
|
870
|
+
return parentPath.concat([to]);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** 上移 / 下移 for a whole group: swap it with that neighbour. */
|
|
874
|
+
function commitGroupMove(path, dir) {
|
|
875
|
+
return actions.commit('move-group', function (doc) {
|
|
876
|
+
const dest = groupStepPath(doc, path, dir);
|
|
877
|
+
if (dest === null) return doc;
|
|
878
|
+
const parentPath = path.slice(0, -1);
|
|
879
|
+
const next = geometry.valueAt(doc, parentPath).slice();
|
|
880
|
+
const a = path[path.length - 1];
|
|
881
|
+
const b = dest[dest.length - 1];
|
|
882
|
+
const held = next[a];
|
|
883
|
+
next[a] = next[b];
|
|
884
|
+
next[b] = held;
|
|
885
|
+
return replaceAt(doc, parentPath, next);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
350
889
|
// v3.5.0 Task 13: widened from `(which, value)` — always writing `.text`
|
|
351
890
|
// — to `(which, key, value)` so the same function also carries the other
|
|
352
891
|
// five ruler fields.
|
|
@@ -374,35 +913,7 @@ function createPanels(deps) {
|
|
|
374
913
|
// `setBannerField` is a plain, exported, pure function — pinned in
|
|
375
914
|
// `test/wave-codec.test.js`.
|
|
376
915
|
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;
|
|
916
|
+
return actions.commit(which + '.' + key, function (doc) { return codec.setBannerField(doc, which, key, value); });
|
|
406
917
|
}
|
|
407
918
|
|
|
408
919
|
/**
|
|
@@ -415,9 +926,10 @@ function createPanels(deps) {
|
|
|
415
926
|
* own `val.trim().split(/\s+/)` turns a plain string back into) rather
|
|
416
927
|
* than blanked out — blanking a field that actually holds `tick: 0`
|
|
417
928
|
* would show a confident wrong state, the same failure `renderUnmodelled`
|
|
418
|
-
* exists to avoid elsewhere in this file. Editing the field
|
|
419
|
-
*
|
|
420
|
-
*
|
|
929
|
+
* exists to avoid elsewhere in this file. Editing the field commits a
|
|
930
|
+
* number for a plain numeral and the typed string otherwise (Task 10's
|
|
931
|
+
* `settingsPopover`): the engine adds the hbounds offset to tick / tock
|
|
932
|
+
* with `+`, so the STRING `"5"` would start the ruler at 50.
|
|
421
933
|
*/
|
|
422
934
|
function tickTockText(v) {
|
|
423
935
|
if (typeof v === 'string') return v;
|
|
@@ -426,142 +938,56 @@ function createPanels(deps) {
|
|
|
426
938
|
return '';
|
|
427
939
|
}
|
|
428
940
|
|
|
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
941
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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.
|
|
942
|
+
* Where a lane inserted at display position `index` would actually land,
|
|
943
|
+
* asked of the bridge rather than guessed.
|
|
944
|
+
*
|
|
945
|
+
* This is constraint 4 made visible. `laneInsertPath` puts an insert at a
|
|
946
|
+
* group's FIRST lane inside that group, and an insert at the position just
|
|
947
|
+
* past its LAST lane outside it — a group can be joined at its head and
|
|
948
|
+
* never at its tail. That asymmetry is not something a label can argue
|
|
949
|
+
* away, so the label states it: the 訊號 menu's items say which container
|
|
950
|
+
* they will land in before they are pressed (`data-lands-in` and the
|
|
951
|
+
* title, computed when `wave-ui.js` opens the menu), and the notice says
|
|
952
|
+
* it again after.
|
|
485
953
|
*/
|
|
486
|
-
function
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
954
|
+
function landingOf(doc, index) {
|
|
955
|
+
const path = codec.laneInsertPath(doc, index);
|
|
956
|
+
if (path === null) return { path: null, title: null };
|
|
957
|
+
const container = path.slice(0, path.length - 1);
|
|
958
|
+
const held = geometry.valueAt(doc, container);
|
|
959
|
+
const title = Array.isArray(held) && typeof held[0] === 'string' ? held[0] : null;
|
|
960
|
+
return { path: path, title: title };
|
|
491
961
|
}
|
|
492
962
|
|
|
493
963
|
/**
|
|
494
|
-
* Say
|
|
495
|
-
*
|
|
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.
|
|
964
|
+
* Say which parts of this document the editor cannot place, in the notice
|
|
965
|
+
* card (spec section 4.9), and what to do about each: the next step is the
|
|
966
|
+
* block's own MD source, reached by closing the editor.
|
|
506
967
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
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.
|
|
968
|
+
* Edges are asked of `geometry.edgeLayoutDetail` — the walk that places
|
|
969
|
+
* them — rather than re-derived here. Skips are bucketed by whatever
|
|
970
|
+
* `reason` comes back, not by a list of the reasons known today, so a new
|
|
971
|
+
* reason shows up as its own (generically worded) bucket instead of
|
|
972
|
+
* escaping the notice. A point note (`<letter> <text>`) is not a skip:
|
|
973
|
+
* the engine draws it (spec section 5-3).
|
|
519
974
|
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
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.
|
|
975
|
+
* `found` is where a document property the canvas does not show would be
|
|
976
|
+
* listed. It is empty today (period, phase and hscale are modelled) and is
|
|
977
|
+
* still published as `data-wave-unmodelled`, so a test can tell "nothing to
|
|
978
|
+
* report" from "never rendered".
|
|
546
979
|
*/
|
|
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".
|
|
980
|
+
function renderUnmodelled(doc, layout, card) {
|
|
553
981
|
const found = [];
|
|
554
|
-
|
|
555
982
|
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
983
|
const buckets = new Map();
|
|
560
984
|
for (const skip of detail.skipped) {
|
|
561
985
|
if (!buckets.has(skip.reason)) buckets.set(skip.reason, []);
|
|
562
986
|
buckets.get(skip.reason).push(skip);
|
|
563
987
|
}
|
|
564
|
-
|
|
988
|
+
function which(list) {
|
|
989
|
+
return '第 ' + list.map(function (s) { return s.index + 1; }).join('、') + ' 條關聯線';
|
|
990
|
+
}
|
|
565
991
|
function letters(list) {
|
|
566
992
|
const out = [];
|
|
567
993
|
for (const skip of list) {
|
|
@@ -571,532 +997,95 @@ function createPanels(deps) {
|
|
|
571
997
|
}
|
|
572
998
|
return out;
|
|
573
999
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
1000
|
+
// Only the letters no lane carries: a dangling edge's other end is real.
|
|
1001
|
+
const nodes = codec.nodesOf(doc);
|
|
1002
|
+
function missing(list) {
|
|
1003
|
+
return letters(list).filter(function (l) { return nodes[l] === undefined; });
|
|
1004
|
+
}
|
|
1005
|
+
const NEXT = '關閉後用區塊 ⠿ →「MD 原始碼」修改。';
|
|
1006
|
+
// `dangling` and `unparseable` are not drawn by the engine either;
|
|
1007
|
+
// `out-of-range` is drawn by the engine but the editor cannot place its
|
|
1008
|
+
// ends, so it cannot be selected or dragged here.
|
|
579
1009
|
const REASON_TEXT = {
|
|
580
1010
|
dangling: function (list) {
|
|
581
|
-
return
|
|
582
|
-
letters(list).join('、') + '),左邊畫不出它們。';
|
|
1011
|
+
return which(list) + '用到不存在的錨點字母(' + missing(list).join('、') + '),畫不出來;' + NEXT;
|
|
583
1012
|
},
|
|
584
1013
|
unparseable: function (list) {
|
|
585
|
-
return
|
|
586
|
-
' 條關聯線的寫法連引擎也認不得,左邊畫不出它們(跟右邊的 WaveDrom 預覽一致)。';
|
|
1014
|
+
return which(list) + '的寫法認不得,畫不出來;' + NEXT;
|
|
587
1015
|
},
|
|
588
1016
|
'out-of-range': function (list) {
|
|
589
|
-
return
|
|
590
|
-
|
|
591
|
-
'——但右邊的 WaveDrom 預覽仍然會畫,兩邊會不一致,以預覽為準。';
|
|
1017
|
+
return which(list) + '的錨點(' + letters(list).join('、') +
|
|
1018
|
+
')落在那條訊號目前的長度之外,在這裡選不到也拖不動;' + NEXT;
|
|
592
1019
|
},
|
|
593
1020
|
};
|
|
594
|
-
|
|
595
1021
|
const sentences = [];
|
|
596
1022
|
if (found.length > 0) {
|
|
597
|
-
sentences.push('這份文件用了 ' + found.join('、') +
|
|
598
|
-
',左邊的手繪波形不表現它們(寬度與相位會不一樣)——以右邊的 WaveDrom 預覽為準。');
|
|
1023
|
+
sentences.push('這份文件用了 ' + found.join('、') + ',編輯器沒有把它們畫出來;' + NEXT);
|
|
599
1024
|
}
|
|
600
1025
|
for (const [reason, list] of buckets) {
|
|
601
1026
|
const text = REASON_TEXT[reason];
|
|
602
|
-
sentences.push(text ? text(list) :
|
|
603
|
-
'這個區塊有 ' + list.length + ' 條關聯線因為「' + reason + '」畫不出來,' +
|
|
604
|
-
'以右邊的 WaveDrom 預覽為準。');
|
|
1027
|
+
sentences.push(text ? text(list) : which(list) + '因為「' + reason + '」畫不出來;' + NEXT);
|
|
605
1028
|
}
|
|
606
1029
|
|
|
607
1030
|
const danglingCount = buckets.has('dangling') ? buckets.get('dangling').length : 0;
|
|
608
1031
|
overlay.setAttribute('data-wave-unmodelled', found.join(' '));
|
|
609
1032
|
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
1033
|
overlay.setAttribute('data-wave-skipped-edges', String(detail.skipped.length));
|
|
613
|
-
deps.unmodelled.hidden = sentences.length === 0;
|
|
614
|
-
deps.unmodelled.textContent = sentences.join(' ');
|
|
615
1034
|
|
|
616
|
-
//
|
|
617
|
-
//
|
|
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.
|
|
1035
|
+
// This editor edits `signal:` and nothing else, so a `reg:` or `assign:`
|
|
1036
|
+
// block opens with no lanes. Say so rather than look broken.
|
|
624
1037
|
const kinds = [];
|
|
625
1038
|
for (const key of ['reg', 'assign']) {
|
|
626
1039
|
if (doc[key] !== undefined) kinds.push(key + ':');
|
|
627
1040
|
}
|
|
628
1041
|
const noLanes = codec.lanePaths(doc).length === 0;
|
|
629
1042
|
overlay.setAttribute('data-wave-nolanes', noLanes ? '1' : '0');
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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');
|
|
1043
|
+
if (noLanes) {
|
|
1044
|
+
sentences.unshift((kinds.length === 0
|
|
1045
|
+
? '這個區塊裡沒有 signal:,所以沒有可以編輯的訊號;'
|
|
1046
|
+
: '這個區塊用的是 ' + kinds.join(' / ') + ',這個編輯器只編輯 signal: 的訊號;') +
|
|
1047
|
+
'要改這個區塊,' + NEXT);
|
|
1066
1048
|
}
|
|
1049
|
+
card.show(sentences);
|
|
1067
1050
|
}
|
|
1068
1051
|
|
|
1069
1052
|
return {
|
|
1070
|
-
toolGroup: toolGroup,
|
|
1071
1053
|
toolButton: toolButton,
|
|
1072
|
-
|
|
1054
|
+
popover: popover,
|
|
1055
|
+
menu: menu,
|
|
1056
|
+
// Task 8 (spec section 4.4): the name column's ⠿ menu, its in-place
|
|
1057
|
+
// name field and 週期與相位…, and the two group writes.
|
|
1058
|
+
laneMenu: laneMenu,
|
|
1059
|
+
renameField: renameField,
|
|
1060
|
+
periodPhasePopover: periodPhasePopover,
|
|
1061
|
+
commitGroupTitle: commitGroupTitle,
|
|
1062
|
+
groupStepPath: groupStepPath,
|
|
1063
|
+
commitGroupMove: commitGroupMove,
|
|
1064
|
+
brushGrid: brushGrid,
|
|
1065
|
+
shortcutCard: shortcutCard,
|
|
1066
|
+
// Task 10 (spec section 4.6): ⚙ 整張圖 and 匯出 ⌄.
|
|
1067
|
+
settingsPopover: settingsPopover,
|
|
1068
|
+
exportMenu: exportMenu,
|
|
1069
|
+
rangeToolbar: rangeToolbar,
|
|
1070
|
+
// Task 9 (spec section 4.5): the edge and note toolbars, their shape
|
|
1071
|
+
// arithmetic, and the inline label / note field.
|
|
1072
|
+
edgeToolbar: edgeToolbar,
|
|
1073
|
+
noteToolbar: noteToolbar,
|
|
1074
|
+
inlineInput: inlineInput,
|
|
1075
|
+
shapeParts: shapeParts,
|
|
1076
|
+
shapeWith: shapeWith,
|
|
1077
|
+
noticeCard: noticeCard,
|
|
1078
|
+
renderUnmodelled: renderUnmodelled,
|
|
1079
|
+
// Where an insert lands, asked of the bridge (constraint 4 in
|
|
1080
|
+
// wave-ui.js); the 訊號 menu says it before and after.
|
|
1081
|
+
landingOf: landingOf,
|
|
1082
|
+
// The settings and lane fields' write side (spec section 4.9): the
|
|
1083
|
+
// lane fields are Task 8's 週期與相位…, the settings Task 10's.
|
|
1073
1084
|
commitBanner: commitBanner,
|
|
1074
1085
|
commitConfigField: commitConfigField,
|
|
1075
1086
|
commitLaneField: commitLaneField,
|
|
1076
|
-
|
|
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,
|
|
1087
|
+
tickTockText: tickTockText,
|
|
1099
1088
|
};
|
|
1100
1089
|
}
|
|
1101
1090
|
|
|
1102
|
-
module.exports = { createPanels: createPanels };
|
|
1091
|
+
module.exports = { createPanels: createPanels, shapeParts: shapeParts, shapeWith: shapeWith, NINE: NINE };
|