flexdesk 0.17.0 → 0.17.1

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/editor.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @flexdesk/editor — A Monaco-backed multi-file editor: the loader, the editor factory, the
3
+ * tab bar, search, a split-pane container and an undo manager.
4
+ *
5
+ * Monaco is a PEER dependency — 14 MB this bundle deliberately does NOT
6
+ * contain, and whose location it refuses to guess. Call setMonacoBasePath()
7
+ * and setDefaultLanguage().
8
+ *
9
+ * GENERATED by scripts/gen_barrels.py. Do not hand-edit.
10
+ *
11
+ * 7 modules.
12
+ */
13
+
14
+ export * from './src/editor/editor_pane.js';
15
+ export * from './src/editor/monaco_editor_factory.js';
16
+ export * from './src/editor/monaco_loader.js';
17
+ export * from './src/editor/notebook_search.js';
18
+ export * from './src/editor/notebook_tab_bar.js';
19
+ export * from './src/editor/notebook_undo_manager.js';
20
+ export * from './src/editor/split_pane_container.js';
package/flow.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @flexdesk/flow — flow editors: the shared kit (a step-type catalogue, a
3
+ * settings panel generated from a JSON-Schema subset, a widget registry,
4
+ * reference chips, the step and value pickers, findings, undo over whole
5
+ * snapshots, keys) and the three editors built on it — the canvas, the outline
6
+ * and the lanes.
7
+ *
8
+ * Every editor is domain-neutral: it never fetches, never saves and never
9
+ * names a consumer's vocabulary. The step types, the reference grammar, every
10
+ * finding and every byte of I/O are the consumer's, passed in.
11
+ *
12
+ * HAND-WRITTEN, not generated. The other barrels say "GENERATED by
13
+ * scripts/gen_barrels.py", and that script is not in this repository, so this
14
+ * one is kept by hand. Each line below is one owner's module (36 §2): the kit
15
+ * is laid down first, and each editor fills its own index.js.
16
+ */
17
+
18
+ export * from './src/flow/kit/index.js';
19
+ export * from './src/flow/canvas_editor/index.js';
20
+ export * from './src/flow/outline/index.js';
21
+ export * from './src/flow/lanes/index.js';
package/host.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @flexdesk/host — The host port. The library never touches pywebview, fetch or localStorage
3
+ * directly — it asks a Host, and the application supplies one.
4
+ *
5
+ * GENERATED by scripts/gen_barrels.py. Do not hand-edit.
6
+ *
7
+ * 5 modules.
8
+ */
9
+
10
+ export * from './src/host/host.js';
11
+ export * from './src/host/pywebview_host.js';
12
+ export * from './src/host/photino_host.js';
13
+ export * from './src/host/external_links.js';
14
+ export * from './src/host/file_picker.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,6 +18,15 @@
18
18
  "files": [
19
19
  "dist",
20
20
  "src",
21
+ "core.js",
22
+ "host.js",
23
+ "wm.js",
24
+ "widgets.js",
25
+ "charts.js",
26
+ "tiles.js",
27
+ "editor.js",
28
+ "canvas.js",
29
+ "flow.js",
21
30
  "css",
22
31
  "README.md",
23
32
  "CHANGELOG.md",
@@ -2888,7 +2888,7 @@ export function createCanvasEditor(host, options = {}) {
2888
2888
  for (const c of g.connections) {
2889
2889
  const source = idOf.get(c.source);
2890
2890
  const target = idOf.get(c.target);
2891
- if (source && target) connect(graph, catalogue, { ...c, source, target }, { strings: S });
2891
+ if (source && target) connect(graph, catalogue, { ...c, source, target }, { strings: S, dataPorts: dataConnect });
2892
2892
  }
2893
2893
  const noteIds = [];
2894
2894
  for (const from of noting ? g.notes || [] : []) {
@@ -6,10 +6,11 @@
6
6
  * it likes (DataTable's `editors` option); the plain text box below is the
7
7
  * default. A factory is called with
8
8
  *
9
- * { value, row, rowIdx, colIdx, column: {index, header, type},
10
- * initialText, commit(value), cancel() }
9
+ * { ...args, value, row, rowIdx, colIdx, column: {index, header, type},
10
+ * initialText, commit(value, move?), cancel() }
11
11
  *
12
- * and returns either an element, or `{ el, getValue?, focus?, destroy? }`.
12
+ * and returns either an element, or
13
+ * `{ el, getValue?, parse?, multiline?, focus?, destroy? }`.
13
14
  * `initialText` is the character typed to open the editor (null when it was
14
15
  * opened by F2, Enter or a double-click): the editor starts with it in place
15
16
  * of the value, as a spreadsheet does.
@@ -29,6 +30,20 @@
29
30
  * pressed in the editor stops there: the table's own keys (the arrows, Ctrl+A,
30
31
  * Ctrl+C) are not the editor's.
31
32
  *
33
+ * ══ A VALUE THAT DOES NOT PARSE (STORY-0087) ═══════════════════════════
34
+ *
35
+ * An editor with a `parse()` — `() => {ok: true, value} | {ok: false, error}`
36
+ * — commits what `parse()` reads, not `getValue()`. A refusal on Enter or Tab
37
+ * keeps the editor open, and its cell carries the message
38
+ * (`twm-dt-cell--error`, `title`, `data-error`) until the next keystroke in
39
+ * it; nothing is committed. A refusal when the focus leaves, or on `commit()`
40
+ * from outside, closes the editor and hands `onRefuse` the message and the
41
+ * text (`getValue()`); still nothing is committed. A `multiline` editor
42
+ * keeps Enter for a new line: Ctrl/Cmd+Enter commits it.
43
+ *
44
+ * `commit(value, move?)` takes an optional move ('down', 'up', 'right',
45
+ * 'left'), for an editor that commits on a key of its own.
46
+ *
32
47
  * ══ EXACTLY ONCE ═════════════════════════════════════════════════════════
33
48
  *
34
49
  * A session closes once. Escape's own repaint takes the focused input out of
@@ -73,14 +88,29 @@ function normalise(made) {
73
88
  let getValue = typeof own.getValue === 'function' ? own.getValue : null;
74
89
  // A form field's value is the editor's; any other element says for itself.
75
90
  if (!getValue && el.matches?.('input, select, textarea')) getValue = () => el.value;
91
+ const parse = typeof own.parse === 'function' ? own.parse : null;
92
+ // What a commit reads: the parse, else the value as it is.
93
+ let read = null;
94
+ if (parse) {
95
+ read = () => {
96
+ let r;
97
+ try { r = parse(); } catch (err) { r = { ok: false, error: err?.message || String(err) }; }
98
+ return r && r.ok === false ? { ok: false, error: String(r.error ?? 'Not valid') }
99
+ : { ok: true, value: r ? r.value : undefined };
100
+ };
101
+ } else if (getValue) read = () => ({ ok: true, value: getValue() });
76
102
  return {
77
103
  el,
78
104
  getValue,
105
+ read,
106
+ multiline: !!own.multiline,
79
107
  focus: typeof own.focus === 'function' ? own.focus : null,
80
108
  destroy: typeof own.destroy === 'function' ? own.destroy : null,
81
109
  };
82
110
  }
83
111
 
112
+ const MOVES = new Set(['down', 'up', 'right', 'left']);
113
+
84
114
  export class CellEditorHost {
85
115
  /**
86
116
  * @param {object} o
@@ -88,19 +118,24 @@ export class CellEditorHost {
88
118
  * cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
89
119
  * or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
90
120
  * @param {(session, reason) => void} o.onCancel
121
+ * @param {(session, error, text, reason) => void} [o.onRefuse] a value
122
+ * that did not parse, when the editor closed with it (the focus left,
123
+ * `commit()` from outside); without it such a close is a cancel.
91
124
  */
92
- constructor({ onCommit, onCancel }) {
125
+ constructor({ onCommit, onCancel, onRefuse = null }) {
93
126
  this._onCommit = onCommit;
94
127
  this._onCancel = onCancel;
128
+ this._onRefuse = onRefuse;
95
129
  /** The open session, or null. */
96
130
  this.session = null;
97
131
  }
98
132
 
99
133
  /**
100
- * Open an editor in `td`. Returns whether one opened (a factory that
134
+ * Open an editor in `td`; `args` go to the factory beside the cell's own
135
+ * (DataTable's `locale`, `utc`, `messages`). Returns whether one opened (a factory that
101
136
  * throws or returns nothing opens none, and the cell is untouched).
102
137
  */
103
- open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, ...extra }) {
138
+ open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, args = {}, ...extra }) {
104
139
  if (this.session) return false;
105
140
  const s = {
106
141
  ...extra, td, value, row, rowIdx, colIdx, column,
@@ -109,8 +144,8 @@ export class CellEditorHost {
109
144
  let made = null;
110
145
  try {
111
146
  made = factory({
112
- value, row, rowIdx, colIdx, column, initialText,
113
- commit: (v) => this._finish(s, v, null, 'editor'),
147
+ ...args, value, row, rowIdx, colIdx, column, initialText,
148
+ commit: (v, move = null) => this._finish(s, v, MOVES.has(move) ? move : null, 'editor'),
114
149
  cancel: () => this._cancel(s, 'editor'),
115
150
  });
116
151
  } catch (err) {
@@ -122,6 +157,10 @@ export class CellEditorHost {
122
157
  s.editor = editor;
123
158
  s.saved = [...td.childNodes];
124
159
  s.savedTitle = td.getAttribute('title');
160
+ s.savedError = {
161
+ on: td.classList.contains('twm-dt-cell--error'),
162
+ message: td.getAttribute('data-error'),
163
+ };
125
164
  this.session = s;
126
165
  td.classList.add('twm-dt-cell--editing');
127
166
  td.replaceChildren(editor.el);
@@ -136,26 +175,29 @@ export class CellEditorHost {
136
175
  if (ev.key === 'Escape') {
137
176
  ev.preventDefault();
138
177
  this._cancel(s, 'key');
139
- } else if (ev.key === 'Enter' && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
178
+ } else if (ev.key === 'Enter' && !ev.altKey && editor.read
179
+ && (editor.multiline ? (ev.ctrlKey || ev.metaKey) : !(ev.ctrlKey || ev.metaKey))) {
140
180
  ev.preventDefault();
141
- this._finish(s, editor.getValue(), ev.shiftKey ? 'up' : 'down', 'key');
142
- } else if (ev.key === 'Tab' && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
181
+ this._tryKey(s, ev.shiftKey ? 'up' : 'down');
182
+ } else if (ev.key === 'Tab' && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.read) {
143
183
  ev.preventDefault();
144
- this._finish(s, editor.getValue(), ev.shiftKey ? 'left' : 'right', 'key');
184
+ this._tryKey(s, ev.shiftKey ? 'left' : 'right');
145
185
  }
146
186
  });
187
+ // A refusal's message stands until the text changes.
188
+ on('input', () => this._unmark(s));
147
189
  // A press inside the editor is the editor's, not a click on the cell.
148
190
  for (const type of ['click', 'dblclick', 'mousedown', 'contextmenu']) {
149
191
  on(type, (ev) => ev.stopPropagation());
150
192
  }
151
193
  on('focusout', (ev) => {
152
- if (s.closed || !editor.getValue) return;
194
+ if (s.closed || !editor.read) return;
153
195
  const to = ev.relatedTarget;
154
196
  if (to && editor.el.contains(to)) return;
155
197
  // The window losing focus (another app) is not a click elsewhere.
156
198
  const doc = editor.el.ownerDocument;
157
199
  if (!to && doc && typeof doc.hasFocus === 'function' && !doc.hasFocus()) return;
158
- this._finish(s, editor.getValue(), null, 'blur');
200
+ this._settle(s, null, 'blur');
159
201
  });
160
202
 
161
203
  try {
@@ -169,8 +211,49 @@ export class CellEditorHost {
169
211
  commit(move = null, reason = 'editor') {
170
212
  const s = this.session;
171
213
  if (!s) return false;
172
- if (!s.editor.getValue) { this._cancel(s, reason); return false; }
173
- return this._finish(s, s.editor.getValue(), move, reason);
214
+ if (!s.editor.read) { this._cancel(s, reason); return false; }
215
+ return this._settle(s, move, reason);
216
+ }
217
+
218
+ /** Enter or Tab: commit what parses; a refusal keeps the editor open,
219
+ * the message on its cell. */
220
+ _tryKey(s, move) {
221
+ const r = s.editor.read();
222
+ if (r.ok) return this._finish(s, r.value, move, 'key');
223
+ s.td.classList.add('twm-dt-cell--error');
224
+ s.td.setAttribute('data-error', r.error);
225
+ s.td.setAttribute('title', r.error);
226
+ s.marked = true;
227
+ return false;
228
+ }
229
+
230
+ _unmark(s) {
231
+ if (!s.marked) return;
232
+ s.marked = false;
233
+ this._restoreMark(s);
234
+ }
235
+
236
+ _restoreMark(s) {
237
+ const { on, message } = s.savedError;
238
+ s.td.classList.toggle('twm-dt-cell--error', on);
239
+ if (message == null) s.td.removeAttribute('data-error');
240
+ else s.td.setAttribute('data-error', message);
241
+ if (s.savedTitle == null) s.td.removeAttribute('title');
242
+ else s.td.setAttribute('title', s.savedTitle);
243
+ }
244
+
245
+ /** The editor closing with what it holds (the focus left, `commit()`):
246
+ * committed when it parses, else handed to `onRefuse`. */
247
+ _settle(s, move, reason) {
248
+ const r = s.editor.read();
249
+ if (r.ok) return this._finish(s, r.value, move, reason);
250
+ if (s.closed) return false;
251
+ let text = '';
252
+ try { text = s.editor.getValue ? s.editor.getValue() : ''; } catch (_) { /* gone */ }
253
+ this._close(s);
254
+ if (this._onRefuse) this._onRefuse(s, r.error, text, reason);
255
+ else this._onCancel?.(s, reason);
256
+ return true;
174
257
  }
175
258
 
176
259
  /** Cancel the open session: the cell is drawn as it was. */
@@ -191,8 +274,7 @@ export class CellEditorHost {
191
274
  }
192
275
  s.td.classList.remove('twm-dt-cell--editing');
193
276
  s.td.replaceChildren(...s.saved);
194
- if (s.savedTitle == null) s.td.removeAttribute('title');
195
- else s.td.setAttribute('title', s.savedTitle);
277
+ this._restoreMark(s);
196
278
  }
197
279
 
198
280
  _finish(s, value, move, reason) {