flexdesk 0.4.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.4.0",
3
+ "version": "0.4.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": {
@@ -86,6 +86,11 @@ import { openForm } from '../ui/components/modal.js';
86
86
  * window onto a tile puts it back in the
87
87
  * tree. Default off — it changes what a
88
88
  * drag to an edge does.
89
+ * @param {boolean} [cfg.backToOpenList] C32. Whether Backspace on a record
90
+ * tab closes it and returns to a list of
91
+ * its section that is already open, rather
92
+ * than rewriting it into a second copy of
93
+ * that list. Default off.
89
94
  * @param {'top'|'bottom'} [cfg.tabLayout] C22. Where a multi-tab leaf draws its
90
95
  * tabs. `'bottom'` is the framework's own
91
96
  * spreadsheet strip under the tile body
@@ -122,6 +127,7 @@ export async function createShell({
122
127
  panels = null,
123
128
  snapPromotion = false,
124
129
  promoteInPlace = false,
130
+ backToOpenList = false,
125
131
  tabLayout = null,
126
132
  chrome = {},
127
133
  // An embedder that moved its sections out of the top bar — into an icon
@@ -163,6 +169,7 @@ export async function createShell({
163
169
  panelDefaults: panels,
164
170
  snapPromotion,
165
171
  promoteInPlace,
172
+ backToOpenList,
166
173
  tabLayout,
167
174
  // `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
168
175
  // spreads it into every content factory, which is how the breadcrumb
package/src/tiling/wm.js CHANGED
@@ -34,7 +34,7 @@ const PANEL_TITLES = {
34
34
  export class WindowManager {
35
35
  constructor({ rootEl, api, ctx, onChange, eventBus, host, taxonomy, events, content,
36
36
  panelDefaults = null, snapPromotion = false,
37
- promoteInPlace = false, tabLayout = null }) {
37
+ promoteInPlace = false, tabLayout = null, backToOpenList = false }) {
38
38
  if (!taxonomy) throw new Error('WindowManager: a taxonomy is required');
39
39
  if (!content || typeof content.mount !== 'function') {
40
40
  throw new Error('WindowManager: a content registry is required '
@@ -83,6 +83,12 @@ export class WindowManager {
83
83
  * changes where a promoted window can be dragged, and an existing
84
84
  * embedder upgrading must not find its windows suddenly clipped. */
85
85
  this.promoteInPlace = !!promoteInPlace;
86
+ /** C32. Whether Backspace on an ENTITY tab closes it and returns to a list
87
+ * that is already open, instead of turning the tab into a second copy of
88
+ * that list. See `_backToOpenList`. Default off: it changes what the key
89
+ * does, and an existing embedder upgrading must not find tabs closing
90
+ * where they used to navigate. */
91
+ this.backToOpenList = !!backToOpenList;
86
92
  this._snapCtl = null;
87
93
  this.desktops = new DesktopManager({
88
94
  seed: this._rootLeaf, panelDefaults: this.panelDefaults });
@@ -285,7 +291,10 @@ export class WindowManager {
285
291
  this._notifyChange('navigate-back');
286
292
  return;
287
293
  }
288
- // (2) Taxonomy fallback — the user's path is gone, so walk the
294
+ // (2) C32. An entity tab with nowhere left to go back to, beside a list
295
+ // of its own section that is already open: close it and land there.
296
+ if (this.backToOpenList && this._backToOpenList(id)) return;
297
+ // (3) Taxonomy fallback — the user's path is gone, so walk the
289
298
  // canonical parent instead. An ENTITY-level parent wins (the
290
299
  // instance one level up, e.g. the parent an id names); failing
291
300
  // that, the KIND-level parent (this page's section).
@@ -347,6 +356,68 @@ export class WindowManager {
347
356
  /** Navigate inside the current tab — preserves every other tab in
348
357
  * the leaf. Used by Backspace + breadcrumb segments + anything
349
358
  * else that should walk WITHIN the tile rather than reset it. */
359
+ /**
360
+ * C32. BACK FROM AN OPENED RECORD RETURNS TO THE LIST, IT DOES NOT MAKE ONE.
361
+ *
362
+ * The gesture this is for: a list tab, a record opened from it into a NEW
363
+ * tab, and Backspace in that record. With nothing left in the record tab's
364
+ * own history, step (3) of `navigateBack` walks the taxonomy up and REWRITES
365
+ * the record tab into its parent — so the tile ends up showing the list
366
+ * twice, once in the tab you started from and once in the tab you just left.
367
+ * The user's model is a browser's: you came from the list, the list is still
368
+ * open, so going back means closing this and being on the list again.
369
+ *
370
+ * So: when the active tab is an ENTITY — its props carry an `id`, the same
371
+ * test `swapToPage` uses for "the caller asked for a specific entity" — look
372
+ * for an open tab of the SAME section that is NOT one. That is the list or
373
+ * landing page the record belongs to. Close the record and activate it.
374
+ *
375
+ * NEAREST TO THE LEFT FIRST. New tabs are appended at the end, so the tab a
376
+ * record was opened from sits to its left; when several lists of the section
377
+ * are open, the nearest one on that side is the one it most plausibly came
378
+ * from. The right side is searched only after the left has nothing.
379
+ *
380
+ * DECLINES, and leaves step (3) to walk up in place, whenever there is no
381
+ * such tab: a record that is the tile's only tab, or one whose section has no
382
+ * list open. That keeps the one guarantee the whole rule exists for — Back
383
+ * never leaves two copies of a list — without ever closing a record into a
384
+ * tile that has nothing of its own to show.
385
+ *
386
+ * Matching is by SECTION (`topNavFor`) and not by kind, because the list a
387
+ * record was opened from is often not the record's taxonomy parent: a root
388
+ * landing page that renders the section, or a list that a flattened taxonomy
389
+ * files beside the record rather than above it.
390
+ *
391
+ * @returns {boolean} whether it closed a tab
392
+ */
393
+ _backToOpenList(leafId) {
394
+ const tree = this._tree();
395
+ const leaf = tree.get(leafId);
396
+ if (!leaf || leaf.kind !== 'leaf') return false;
397
+ const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
398
+ if (tabs.length < 2) return false;
399
+ const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
400
+ const active = tabs[activeIdx];
401
+ if (!_isEntityTab(active)) return false;
402
+ const section = this.taxonomy.topNavFor(active.kind);
403
+ if (!section) return false;
404
+
405
+ const isList = (t) => t && !_isEntityTab(t) && this.taxonomy.topNavFor(t.kind) === section;
406
+ let target = -1;
407
+ for (let i = activeIdx - 1; i >= 0 && target < 0; i--) if (isList(tabs[i])) target = i;
408
+ for (let i = activeIdx + 1; i < tabs.length && target < 0; i++) if (isList(tabs[i])) target = i;
409
+ if (target < 0) return false;
410
+
411
+ tree.removeLeafTab(leafId, activeIdx);
412
+ // Removing the active tab shifts everything after it one to the left.
413
+ tree.setActiveLeafTab(leafId, target > activeIdx ? target - 1 : target);
414
+ tree.focus(leafId);
415
+ this.renderer.render();
416
+ this._persist();
417
+ this._notifyChange('navigate-back-close');
418
+ return true;
419
+ }
420
+
350
421
  navigateActiveTab(leafId, kind, props = {}) {
351
422
  const tree = this._tree();
352
423
  const leaf = tree.get(leafId);
@@ -3641,3 +3712,8 @@ function _swapSiblings(tree, aId, bId) {
3641
3712
  return true;
3642
3713
  }
3643
3714
 
3715
+ /** A tab showing ONE record rather than a list: its props name an `id`. The same
3716
+ * test `swapToPage` applies to "the caller asked for a specific entity". */
3717
+ function _isEntityTab(tab) {
3718
+ return !!tab && tab.props != null && tab.props.id != null && tab.props.id !== '';
3719
+ }