@jxsuite/studio 10.2.0 → 10.4.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/dist/chunks/{2020-bx702wqa.js → 2020-906vx2dq.js} +3 -3
- package/dist/chunks/adapter-hnvad61x.js +4 -0
- package/dist/chunks/add-new-flow-v0pm96mr.js +4 -0
- package/dist/chunks/cf-account-picker-xb8vvt2h.js +4 -0
- package/dist/chunks/cf-settings-mkbcz4z2.js +4 -0
- package/dist/chunks/components-w5zqwva1.js +4 -0
- package/dist/chunks/content-source-g38w0d3f.js +4 -0
- package/dist/chunks/convert-to-repeater-r8sfxjjm.js +4 -0
- package/dist/chunks/editor-wvf5e6j9.js +4 -0
- package/dist/chunks/entry-commands-t13dyw3c.js +4 -0
- package/dist/chunks/extension-sections-vjzw46xb.js +4 -0
- package/dist/chunks/file-ops-1sghzgcr.js +4 -0
- package/dist/chunks/files-zrqtgt41.js +4 -0
- package/dist/chunks/format-host-9gy76erh.js +4 -0
- package/dist/chunks/fs-events-45rhg3me.js +4 -0
- package/dist/chunks/grid-open-t4fq6bbm.js +4 -0
- package/dist/chunks/{grid-view-tghcwrrt.js → grid-view-2f03wcgw.js} +16 -16
- package/dist/chunks/icons-gb967qc8.js +4 -0
- package/dist/chunks/iframe-host-9aq8ze3q.js +4 -0
- package/dist/chunks/image-bake-4afr4y6a.js +4 -0
- package/dist/chunks/image-editor-x7t8jbjn.js +4 -0
- package/dist/chunks/index-f9qr7kzh.js +4 -0
- package/dist/chunks/{index-vr006kbt.js → index-yjrfeks8.js} +3 -3
- package/dist/chunks/{javascript-z1g3s9fd.js → javascript-hy2mws8v.js} +3 -3
- package/dist/chunks/{jsonMode-7szwg8c4.js → jsonMode-vj30te2h.js} +6 -6
- package/dist/chunks/jx-validate-r4ax11e3.js +4 -0
- package/dist/chunks/layers-m49nmwvx.js +4 -0
- package/dist/chunks/layers-panel-zr92pwdp.js +4 -0
- package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
- package/dist/chunks/media-library-j74rt0w4.js +4 -0
- package/dist/chunks/media-open-mypr0k6b.js +4 -0
- package/dist/chunks/media-picker-pemtbsjv.js +4 -0
- package/dist/chunks/{monaco-binding-v42w9c1v.js → monaco-binding-a51py33c.js} +3 -3
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
- package/dist/chunks/{monaco-setup-7bffxd6v.js → monaco-setup-0hw9f835.js} +3 -3
- package/dist/chunks/notify-bwehrp7q.js +4 -0
- package/dist/chunks/parse-hee7ne9b.js +4 -0
- package/dist/chunks/platform-kv0k65cy.js +4 -0
- package/dist/chunks/publish-panel-vg1n880b.js +4 -0
- package/dist/chunks/publish-quick-4fwngwrw.js +4 -0
- package/dist/chunks/publish-workspace-jv4w2bx2.js +4 -0
- package/dist/chunks/quick-search-nh29rt8a.js +4 -0
- package/dist/chunks/references-vqyr5s9x.js +4 -0
- package/dist/chunks/right-panel-3ws9f2w0.js +4 -0
- package/dist/chunks/source-control-h3hcs5gy.js +4 -0
- package/dist/chunks/{studio-j3d2fjqz.js → studio-0939t1q1.js} +4 -4
- package/dist/chunks/studio-0gvvqj3h.js +5 -0
- package/dist/chunks/studio-0khd8xtx.js +4 -0
- package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
- package/dist/chunks/studio-1410q5dw.js +5 -0
- package/dist/chunks/studio-1ctnw3xv.js +5 -0
- package/dist/chunks/studio-1ks5d9y4.js +5 -0
- package/dist/chunks/{studio-awxwjawq.js → studio-1y9mab9m.js} +4 -4
- package/dist/chunks/studio-2e4hry72.js +6 -0
- package/dist/chunks/studio-2kpxm0eh.js +7 -0
- package/dist/chunks/studio-2pk7kc7z.js +5 -0
- package/dist/chunks/studio-2x972v58.js +5 -0
- package/dist/chunks/studio-2zz4qbv9.js +5 -0
- package/dist/chunks/{studio-bvs9kpst.js → studio-3na1r48j.js} +8 -8
- package/dist/chunks/studio-3pz4t5a2.js +5 -0
- package/dist/chunks/studio-3qr08sxq.js +4 -0
- package/dist/chunks/{studio-75kcdbsk.js → studio-45mj4jqe.js} +4 -4
- package/dist/chunks/studio-4bgvvzhv.js +5 -0
- package/dist/chunks/studio-4f7thq0p.js +5 -0
- package/dist/chunks/studio-4fnb5648.js +5 -0
- package/dist/chunks/studio-4zd6an2b.js +5 -0
- package/dist/chunks/{studio-9rbe2xpw.js → studio-5tet77de.js} +3 -3
- package/dist/chunks/studio-61rg2cz5.js +5 -0
- package/dist/chunks/{studio-ybnwymxw.js → studio-7rrdbvex.js} +4 -4
- package/dist/chunks/{studio-sxdb9h3q.js → studio-90zkgne5.js} +7 -7
- package/dist/chunks/studio-9c6cbq2c.js +4 -0
- package/dist/chunks/studio-9dq7rwc7.js +5 -0
- package/dist/chunks/studio-9gr7xs9r.js +6 -0
- package/dist/chunks/studio-a88ej93p.js +7 -0
- package/dist/chunks/{studio-er5f383a.js → studio-abh9kfdh.js} +4 -4
- package/dist/chunks/studio-acer0g8w.js +5 -0
- package/dist/chunks/{studio-qh8berdz.js → studio-amaz545p.js} +4 -4
- package/dist/chunks/studio-az3wvyh2.js +5 -0
- package/dist/chunks/studio-bg8pzprg.js +5 -0
- package/dist/chunks/{studio-399a25za.js → studio-bnc50f9h.js} +4 -4
- package/dist/chunks/studio-brh8d2pr.js +5 -0
- package/dist/chunks/{studio-646nn2ys.js → studio-c2fa783p.js} +5 -5
- package/dist/chunks/studio-cfnn70h1.js +5 -0
- package/dist/chunks/{studio-rzqaz21g.js → studio-cqxj4fcy.js} +4 -4
- package/dist/chunks/studio-cw11h078.js +5 -0
- package/dist/chunks/studio-daksw8kh.js +5 -0
- package/dist/chunks/studio-dn56z7sn.js +5 -0
- package/dist/chunks/studio-egnjpyde.js +9 -0
- package/dist/chunks/studio-ejrypvmq.js +5 -0
- package/dist/chunks/studio-eqcddvc2.js +928 -0
- package/dist/chunks/studio-evq47j2z.js +5 -0
- package/dist/chunks/{studio-r7pf0rkv.js → studio-frc16j8v.js} +7 -7
- package/dist/chunks/studio-g0hxtcga.js +5 -0
- package/dist/chunks/{studio-th7z3fg7.js → studio-g9brbewz.js} +4 -4
- package/dist/chunks/studio-gnazwtv3.js +5 -0
- package/dist/chunks/studio-hfqpfmpk.js +5 -0
- package/dist/chunks/studio-hh394phe.js +5 -0
- package/dist/chunks/{studio-pjtntksy.js → studio-j0zf5x2x.js} +4 -4
- package/dist/chunks/studio-j5fcp6gj.js +5 -0
- package/dist/chunks/studio-jd1qb1st.js +7 -0
- package/dist/chunks/studio-jma2q8r9.js +5 -0
- package/dist/chunks/studio-k0xzrse2.js +64 -0
- package/dist/chunks/studio-k1xqrgm5.js +5 -0
- package/dist/chunks/studio-k2evngrj.js +7 -0
- package/dist/chunks/studio-k3we3kqb.js +5 -0
- package/dist/chunks/studio-knsjyx7n.js +5 -0
- package/dist/chunks/studio-mrjtnm62.js +5 -0
- package/dist/chunks/{studio-sevbc3xv.js → studio-n9qba0gx.js} +4 -4
- package/dist/chunks/{studio-ea74s8c1.js → studio-nvcn2ykc.js} +4 -4
- package/dist/chunks/studio-nwf0c32p.js +5 -0
- package/dist/chunks/studio-nzemfbkk.js +14 -0
- package/dist/chunks/{studio-43y04qck.js → studio-pj216fmn.js} +4 -4
- package/dist/chunks/{studio-4g8yb6jn.js → studio-pm17wdc4.js} +4 -4
- package/dist/chunks/studio-pwctzka3.js +16 -0
- package/dist/chunks/{studio-71m92v9s.js → studio-q80fg1d6.js} +7 -7
- package/dist/chunks/studio-q9hx2qp0.js +5 -0
- package/dist/chunks/studio-qrzmfaf6.js +5 -0
- package/dist/chunks/{studio-86r8hp7r.js → studio-r6mx7vm5.js} +5 -5
- package/dist/chunks/studio-rr00ex23.js +7 -0
- package/dist/chunks/{studio-4jhw38b2.js → studio-s24mhq0z.js} +4 -4
- package/dist/chunks/studio-sce30r7b.js +4 -0
- package/dist/chunks/studio-swedpd82.js +5 -0
- package/dist/chunks/studio-tmfcn347.js +6 -0
- package/dist/chunks/studio-tpxcryf2.js +5 -0
- package/dist/chunks/{studio-wc6za3zs.js → studio-v5wktj6f.js} +4 -4
- package/dist/chunks/studio-v7a7yb5w.js +9 -0
- package/dist/chunks/{studio-kg1abnz0.js → studio-vpevkfxw.js} +4 -4
- package/dist/chunks/{studio-881z1xrx.js → studio-wmjmsw0p.js} +4 -4
- package/dist/chunks/studio-wqfaqw4m.js +5 -0
- package/dist/chunks/{studio-hhp4x8xf.js → studio-xpx91fby.js} +5 -5
- package/dist/chunks/studio-xva3hzkv.js +5 -0
- package/dist/chunks/{studio-6hzdjfv0.js → studio-xx3r3sb0.js} +4 -4
- package/dist/chunks/{studio-cg1bfyhf.js → studio-z3fztxrp.js} +8 -8
- package/dist/chunks/studio-z9453ypn.js +6 -0
- package/dist/chunks/{tsMode-f54j66ym.js → tsMode-jg1dtphj.js} +7 -7
- package/dist/chunks/workspace-6ztqcrsh.js +4 -0
- package/dist/chunks/workspace-home-rse9h8a0.js +4 -0
- package/dist/chunks/{ws-client-e8kj684q.js → ws-client-arz2qsy9.js} +3 -3
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +83 -78
- package/dist/studio.js +84 -84
- package/package.json +11 -11
- package/src/browse/library-pane.ts +44 -29
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +13 -4
- package/src/canvas/asset-resolve.ts +54 -11
- package/src/canvas/canvas-render.ts +25 -0
- package/src/canvas/iframe-entry.ts +5 -1
- package/src/canvas/iframe-host.ts +16 -6
- package/src/canvas/iframe-protocol.ts +9 -1
- package/src/commands/context.ts +5 -0
- package/src/commands/live-context.ts +5 -2
- package/src/editor/shortcuts.ts +3 -0
- package/src/files/asset-versions.ts +108 -2
- package/src/files/file-ops.ts +11 -1
- package/src/files/files.ts +111 -48
- package/src/files/media-paths.ts +12 -4
- package/src/files/media-upload.ts +195 -21
- package/src/files/new-file-name.ts +39 -1
- package/src/media/image-bake.ts +164 -0
- package/src/media/image-editor.ts +846 -0
- package/src/media/image-format.ts +262 -0
- package/src/media/image-recipe.ts +651 -0
- package/src/media/media-commands.ts +65 -1
- package/src/media/media-library-model.ts +1 -1
- package/src/media/media-library.ts +175 -15
- package/src/media/media-open.ts +156 -3
- package/src/media/media-pane.ts +8 -4
- package/src/panels/pane-context.ts +6 -1
- package/src/panels/tab-strip.ts +12 -3
- package/src/studio.ts +13 -2
- package/src/surfaces/image-editor.json +1390 -0
- package/src/surfaces/image-editor.ts +272 -0
- package/src/surfaces/inspector-dock.json +3 -3
- package/src/surfaces/media-library.json +129 -1
- package/src/surfaces/media-library.ts +17 -2
- package/src/types.ts +2 -1
- package/src/workspace/tab-workspace.ts +2 -1
- package/src/workspace/workspace-switch.ts +3 -1
- package/src/workspace/workspace.ts +22 -1
- package/dist/chunks/adapter-pp011m5x.js +0 -4
- package/dist/chunks/add-new-flow-ecqmzz76.js +0 -4
- package/dist/chunks/cf-account-picker-c07znkqt.js +0 -4
- package/dist/chunks/cf-settings-0v2bmqy0.js +0 -4
- package/dist/chunks/components-yn0dn2pq.js +0 -4
- package/dist/chunks/content-source-g0j41yy6.js +0 -4
- package/dist/chunks/convert-to-repeater-sbjk7tgn.js +0 -4
- package/dist/chunks/editor-xz02jeh1.js +0 -4
- package/dist/chunks/entry-commands-kmt02crp.js +0 -4
- package/dist/chunks/extension-sections-0rprf12d.js +0 -4
- package/dist/chunks/file-ops-mexj5qed.js +0 -4
- package/dist/chunks/files-b2tcppdd.js +0 -4
- package/dist/chunks/format-host-btsevv0c.js +0 -4
- package/dist/chunks/fs-events-r0apejva.js +0 -4
- package/dist/chunks/grid-open-n2a8xkht.js +0 -4
- package/dist/chunks/icons-853n6qt3.js +0 -4
- package/dist/chunks/iframe-host-gx0ypyh3.js +0 -4
- package/dist/chunks/index-bs4k9tr6.js +0 -4
- package/dist/chunks/jx-validate-ymf10teh.js +0 -4
- package/dist/chunks/layers-p9nfya38.js +0 -4
- package/dist/chunks/layers-panel-tt1hzhhf.js +0 -4
- package/dist/chunks/lit-html-x18n2sfz.js +0 -4
- package/dist/chunks/media-library-0n45gv8r.js +0 -4
- package/dist/chunks/media-picker-s09at95z.js +0 -4
- package/dist/chunks/monaco-buffer-een2ea0v.js +0 -4
- package/dist/chunks/notify-qwb8radt.js +0 -4
- package/dist/chunks/parse-v77xtwv7.js +0 -4
- package/dist/chunks/platform-8jnez0wr.js +0 -4
- package/dist/chunks/publish-panel-aysnj611.js +0 -4
- package/dist/chunks/publish-quick-kpbqs9mz.js +0 -4
- package/dist/chunks/publish-workspace-nd0a8q99.js +0 -4
- package/dist/chunks/quick-search-vg0jkhk2.js +0 -4
- package/dist/chunks/references-a7v6a0e2.js +0 -4
- package/dist/chunks/right-panel-cvk1vxx5.js +0 -4
- package/dist/chunks/source-control-fsqzc6he.js +0 -4
- package/dist/chunks/studio-05jfwbmf.js +0 -5
- package/dist/chunks/studio-0q5jwddh.js +0 -9
- package/dist/chunks/studio-0w2dr0q8.js +0 -5
- package/dist/chunks/studio-1tzpds97.js +0 -9
- package/dist/chunks/studio-216b27sv.js +0 -4
- package/dist/chunks/studio-2ejhqns4.js +0 -5
- package/dist/chunks/studio-2r90cts3.js +0 -16
- package/dist/chunks/studio-399xknkb.js +0 -6
- package/dist/chunks/studio-4ncmwqw5.js +0 -5
- package/dist/chunks/studio-5atwjgkj.js +0 -7
- package/dist/chunks/studio-5frqk7c0.js +0 -5
- package/dist/chunks/studio-6cwj6xrx.js +0 -5
- package/dist/chunks/studio-78pywnp2.js +0 -6
- package/dist/chunks/studio-886t6ztg.js +0 -5
- package/dist/chunks/studio-8wnvtr4z.js +0 -5
- package/dist/chunks/studio-8z66jk4z.js +0 -5
- package/dist/chunks/studio-b2qpt0xy.js +0 -14
- package/dist/chunks/studio-c71806xz.js +0 -7
- package/dist/chunks/studio-cbwks69x.js +0 -928
- package/dist/chunks/studio-cg710sk9.js +0 -5
- package/dist/chunks/studio-cw0wb9qk.js +0 -7
- package/dist/chunks/studio-cyheys7k.js +0 -5
- package/dist/chunks/studio-ea5m9kg5.js +0 -5
- package/dist/chunks/studio-efdxjwyp.js +0 -64
- package/dist/chunks/studio-egmw2837.js +0 -5
- package/dist/chunks/studio-f746039f.js +0 -5
- package/dist/chunks/studio-fmdymd9v.js +0 -5
- package/dist/chunks/studio-gcm12z00.js +0 -6
- package/dist/chunks/studio-gj98m8gy.js +0 -7
- package/dist/chunks/studio-hasbgjah.js +0 -5
- package/dist/chunks/studio-hz5n7x27.js +0 -5
- package/dist/chunks/studio-jeraxdgv.js +0 -7
- package/dist/chunks/studio-jvgek9xt.js +0 -5
- package/dist/chunks/studio-jws1a87n.js +0 -5
- package/dist/chunks/studio-k2sz13zg.js +0 -5
- package/dist/chunks/studio-k3m4jkdk.js +0 -5
- package/dist/chunks/studio-kg5ya5r2.js +0 -5
- package/dist/chunks/studio-khwbgrtg.js +0 -5
- package/dist/chunks/studio-mx6wzh25.js +0 -5
- package/dist/chunks/studio-mxs3ar9t.js +0 -5
- package/dist/chunks/studio-nm4szey4.js +0 -5
- package/dist/chunks/studio-nqxqf8p2.js +0 -4
- package/dist/chunks/studio-qn7z4krm.js +0 -5
- package/dist/chunks/studio-qyed0ah6.js +0 -5
- package/dist/chunks/studio-r4bcdwhq.js +0 -5
- package/dist/chunks/studio-rtfnhrdg.js +0 -5
- package/dist/chunks/studio-s5tzw84t.js +0 -5
- package/dist/chunks/studio-t51y8v1d.js +0 -5
- package/dist/chunks/studio-t98vq4r6.js +0 -5
- package/dist/chunks/studio-tsxpqvh0.js +0 -6
- package/dist/chunks/studio-v1yascv2.js +0 -5
- package/dist/chunks/studio-wxq6v5sv.js +0 -5
- package/dist/chunks/studio-y60sts62.js +0 -4
- package/dist/chunks/studio-zndrqawb.js +0 -5
- package/dist/chunks/studio-zwaxn5s2.js +0 -5
- package/dist/chunks/workspace-home-x1787wpv.js +0 -4
- package/dist/chunks/workspace-zpe1np43.js +0 -4
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The Image editor surface: one image asset, edited where it is opened (studio.md §9.5).
|
|
4
|
+
*
|
|
5
|
+
* This is the adapter. `media/image-editor.ts` keeps the recipe, its undo stack, the decoded source
|
|
6
|
+
* and the one write door, and hands this module a flat projection: pre-formatted geometry strings,
|
|
7
|
+
* the values the fields show, and the words the `$switch`es read. The document renders that and
|
|
8
|
+
* decides nothing.
|
|
9
|
+
*
|
|
10
|
+
* **One island.** A drag converts screen pixels into the image's own frame, which needs the frame's
|
|
11
|
+
* drawn size, so the frame element is captured as it is created (`onNodeCreated`,
|
|
12
|
+
* studio-ui-guidelines.md §9.4) and the flow asks for its rect. The handles are captured the same
|
|
13
|
+
* way, so moving the roving tab stop to the next handle focuses an element this module was handed
|
|
14
|
+
* rather than one it searched for.
|
|
15
|
+
*
|
|
16
|
+
* **The container is CLEARED, because this is a pane and not a panel** — the reason
|
|
17
|
+
* `surfaces/media-pane.ts` gives: the canvas stage is handed to whichever mode owns it whole.
|
|
18
|
+
*
|
|
19
|
+
* @docs studio/projects/media
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { isNonNullObject, isString } from "@jxsuite/schema/guards";
|
|
23
|
+
import { reactive } from "../reactivity";
|
|
24
|
+
import { mountSurface, registerSurface } from "../ui/surface";
|
|
25
|
+
import { rectOf } from "../utils/geometry";
|
|
26
|
+
import imageEditorDoc from "./image-editor.json";
|
|
27
|
+
import type { SurfaceHandle } from "../ui/surface";
|
|
28
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
29
|
+
import type { JxElement } from "@jxsuite/schema/types";
|
|
30
|
+
|
|
31
|
+
registerSurface("image-editor", imageEditorDoc);
|
|
32
|
+
|
|
33
|
+
/** One crop handle as the document draws it. */
|
|
34
|
+
export interface ImageHandleRow {
|
|
35
|
+
id: string;
|
|
36
|
+
/** Where it sits on screen, as a reader hears it. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** `"0"` for the one handle in the tab order, `"-1"` for the other seven. */
|
|
39
|
+
tabindex: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Everything the document draws, as one value. */
|
|
43
|
+
export interface ImageEditorView {
|
|
44
|
+
region: string;
|
|
45
|
+
/** `loading` while the bytes are read, `refused` with the reason, `ready` with a picture. */
|
|
46
|
+
status: "loading" | "ready" | "refused";
|
|
47
|
+
/** What the loading or refused box says. */
|
|
48
|
+
message: string;
|
|
49
|
+
name: string;
|
|
50
|
+
alt: string;
|
|
51
|
+
/** A same-origin `blob:` URL for the bytes the editor holds. */
|
|
52
|
+
src: string;
|
|
53
|
+
/** The oriented aspect, width over height, as a number string. */
|
|
54
|
+
ratio: string;
|
|
55
|
+
/** The oriented width in CSS pixels, so the preview is never upscaled. */
|
|
56
|
+
maxWidth: string;
|
|
57
|
+
imageWidth: string;
|
|
58
|
+
imageHeight: string;
|
|
59
|
+
imageTransform: string;
|
|
60
|
+
/** The tone pass, the same string the bake sets on its context; `none` when neutral. */
|
|
61
|
+
filter: string;
|
|
62
|
+
cropX: string;
|
|
63
|
+
cropY: string;
|
|
64
|
+
cropW: string;
|
|
65
|
+
cropH: string;
|
|
66
|
+
handles: ImageHandleRow[];
|
|
67
|
+
handleHelpId: string;
|
|
68
|
+
/** The ratio the crop is held to, and the picker's rows. */
|
|
69
|
+
aspect: string;
|
|
70
|
+
aspectOptions: { label: string; value: string }[];
|
|
71
|
+
/** The crop area's cursor: `move`, or `default` when there is nowhere for it to go. */
|
|
72
|
+
moveCursor: string;
|
|
73
|
+
facts: string;
|
|
74
|
+
width: string;
|
|
75
|
+
maxOutputWidth: string;
|
|
76
|
+
fitLabel: string;
|
|
77
|
+
fitHint: string;
|
|
78
|
+
quality: "shown" | "hidden";
|
|
79
|
+
qualityValue: string;
|
|
80
|
+
brightness: string;
|
|
81
|
+
contrast: string;
|
|
82
|
+
saturation: string;
|
|
83
|
+
warning: string;
|
|
84
|
+
/** Disabled flags, each named for the state that disables. */
|
|
85
|
+
busy: boolean;
|
|
86
|
+
undecoded: boolean;
|
|
87
|
+
cannotUndo: boolean;
|
|
88
|
+
cannotRedo: boolean;
|
|
89
|
+
cannotFit: boolean;
|
|
90
|
+
clean: boolean;
|
|
91
|
+
cannotSave: boolean;
|
|
92
|
+
saving: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What a control can ask the editor to do. Every one is a decision the flow owns. */
|
|
96
|
+
export interface ImageEditorActions {
|
|
97
|
+
imageLoaded: (width: number, height: number) => void;
|
|
98
|
+
imageFailed: (src: string) => void;
|
|
99
|
+
rotate: (direction: number) => void;
|
|
100
|
+
flip: (axis: string) => void;
|
|
101
|
+
undo: () => void;
|
|
102
|
+
redo: () => void;
|
|
103
|
+
reset: () => void;
|
|
104
|
+
save: () => void;
|
|
105
|
+
fieldEdited: () => void;
|
|
106
|
+
fitForWeb: () => void;
|
|
107
|
+
setWidth: (value: string) => void;
|
|
108
|
+
setAspect: (value: string) => void;
|
|
109
|
+
setValue: (key: string, value: string) => void;
|
|
110
|
+
handleDown: (
|
|
111
|
+
handle: string,
|
|
112
|
+
pointerId: number,
|
|
113
|
+
clientX: number,
|
|
114
|
+
clientY: number,
|
|
115
|
+
target: Element,
|
|
116
|
+
) => void;
|
|
117
|
+
handleMove: (pointerId: number, clientX: number, clientY: number) => void;
|
|
118
|
+
handleUp: (pointerId: number) => void;
|
|
119
|
+
handleFocused: (handle: string) => void;
|
|
120
|
+
nudge: (handle: string, dx: number, dy: number, large: boolean) => void;
|
|
121
|
+
cycleHandle: (handle: string, step: number) => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ImageEditorSurfaceHandle {
|
|
125
|
+
ready: Promise<HTMLElement>;
|
|
126
|
+
/** Whether what was mounted is still inside the host; true while the mount is in flight. */
|
|
127
|
+
attached: () => boolean;
|
|
128
|
+
update: (view: ImageEditorView) => void;
|
|
129
|
+
/** The drawn frame's rect, or null before it exists. */
|
|
130
|
+
frameRect: () => DOMRect | null;
|
|
131
|
+
/** Move focus to one handle, when it has been drawn. */
|
|
132
|
+
focusHandle: (handle: string) => void;
|
|
133
|
+
/** Commit a number still being typed, so a save writes what is on screen. Keeps the focus. */
|
|
134
|
+
flushFields: () => void;
|
|
135
|
+
/** Put the view's values back in the number fields, whatever was typed into them. */
|
|
136
|
+
syncFields: (view: ImageEditorView) => void;
|
|
137
|
+
dispose: () => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type ImageEditorScope = ImageEditorView & ImageEditorActions;
|
|
141
|
+
|
|
142
|
+
/** Each number field's `part`, which is also the name of the view value it shows. */
|
|
143
|
+
const FIELD_VALUES = {
|
|
144
|
+
brightness: "brightness",
|
|
145
|
+
contrast: "contrast",
|
|
146
|
+
quality: "qualityValue",
|
|
147
|
+
saturation: "saturation",
|
|
148
|
+
width: "width",
|
|
149
|
+
} as const;
|
|
150
|
+
|
|
151
|
+
function isFieldPart(part: string): part is keyof typeof FIELD_VALUES {
|
|
152
|
+
return Object.hasOwn(FIELD_VALUES, part);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The `part` a definition declares, or `""`. Read off the DEFINITION, because a node is announced
|
|
157
|
+
* as it is created, before its attributes are applied.
|
|
158
|
+
*/
|
|
159
|
+
function partOf(def: JxElement | string): string {
|
|
160
|
+
const part = isString(def) ? undefined : def.attributes?.["part"];
|
|
161
|
+
return isString(part) ? part : "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The handle a mapped row stands for, read off its `$map` item. */
|
|
165
|
+
function mappedHandle(state: JxScope | undefined): string | null {
|
|
166
|
+
const map = state?.["$map"];
|
|
167
|
+
const item = isNonNullObject(map) && "item" in map ? map.item : undefined;
|
|
168
|
+
return isNonNullObject(item) && "id" in item && isString(item.id) ? item.id : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Write a projection into the scope. Each key is assigned through the reactive proxy, which
|
|
173
|
+
* notifies only when a value moved, so a drag that changes four crop strings redraws those four.
|
|
174
|
+
*/
|
|
175
|
+
function project(scope: ImageEditorScope, view: ImageEditorView): void {
|
|
176
|
+
Object.assign(scope, view);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Mount the Image editor document into `host`, wired to `actions`.
|
|
181
|
+
*
|
|
182
|
+
* @param {HTMLElement} host - The pane's canvas stage
|
|
183
|
+
* @param {ImageEditorView} view - What to draw right now
|
|
184
|
+
* @param {ImageEditorActions} actions - What each control does
|
|
185
|
+
* @returns {ImageEditorSurfaceHandle}
|
|
186
|
+
*/
|
|
187
|
+
export function mountImageEditorSurface(
|
|
188
|
+
host: HTMLElement,
|
|
189
|
+
view: ImageEditorView,
|
|
190
|
+
actions: ImageEditorActions,
|
|
191
|
+
): ImageEditorSurfaceHandle {
|
|
192
|
+
host.replaceChildren();
|
|
193
|
+
const scope = reactive<ImageEditorScope>({ ...view, ...actions });
|
|
194
|
+
|
|
195
|
+
let frame: Element | null = null;
|
|
196
|
+
const handles = new Map<string, HTMLElement>();
|
|
197
|
+
const fields = new Map<keyof typeof FIELD_VALUES, HTMLElement>();
|
|
198
|
+
let mounted: SurfaceHandle | null = null;
|
|
199
|
+
let disposed = false;
|
|
200
|
+
const ready = mountSurface("image-editor", scope, host, {
|
|
201
|
+
onNodeCreated: (element, _path, def, state) => {
|
|
202
|
+
// `disposed` first: nodes are still created while a mount that was taken down runs out.
|
|
203
|
+
if (disposed || !(element instanceof HTMLElement)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const part = partOf(def);
|
|
207
|
+
if (part === "frame") {
|
|
208
|
+
frame = element;
|
|
209
|
+
} else if (part === "handle") {
|
|
210
|
+
const id = mappedHandle(state);
|
|
211
|
+
if (id !== null) {
|
|
212
|
+
handles.set(id, element);
|
|
213
|
+
}
|
|
214
|
+
} else if (isFieldPart(part)) {
|
|
215
|
+
fields.set(part, element);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
}).then((surface) => {
|
|
219
|
+
if (disposed) {
|
|
220
|
+
surface.dispose();
|
|
221
|
+
} else {
|
|
222
|
+
mounted = surface;
|
|
223
|
+
}
|
|
224
|
+
/* SAFETY: `image-editor.json`'s root definition is a `div` element, so the runtime mounts an
|
|
225
|
+
HTMLElement; a `Text` root only comes from a document whose root is a bare string. */
|
|
226
|
+
return surface.root as HTMLElement;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
attached: () => {
|
|
231
|
+
if (disposed) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const root = mounted?.root;
|
|
235
|
+
return root === undefined ? true : host.contains(root);
|
|
236
|
+
},
|
|
237
|
+
dispose() {
|
|
238
|
+
disposed = true;
|
|
239
|
+
mounted?.dispose();
|
|
240
|
+
mounted = null;
|
|
241
|
+
frame = null;
|
|
242
|
+
handles.clear();
|
|
243
|
+
fields.clear();
|
|
244
|
+
},
|
|
245
|
+
focusHandle: (handle) => {
|
|
246
|
+
const element = handles.get(handle);
|
|
247
|
+
if (element?.isConnected) {
|
|
248
|
+
element.focus();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
frameRect: () => (frame?.isConnected ? rectOf(frame) : null),
|
|
252
|
+
flushFields: () => {
|
|
253
|
+
/* A number input commits on blur, which fires its change event synchronously, and the
|
|
254
|
+
document's handler then takes the value. Focused straight back, so the author's place is
|
|
255
|
+
not lost to a keystroke that only meant "save". */
|
|
256
|
+
const active = document.activeElement;
|
|
257
|
+
if (active instanceof HTMLInputElement && host.contains(active)) {
|
|
258
|
+
active.blur();
|
|
259
|
+
active.focus();
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
ready,
|
|
263
|
+
syncFields: (next) => {
|
|
264
|
+
for (const [part, element] of fields) {
|
|
265
|
+
if (element.isConnected && "value" in element) {
|
|
266
|
+
element.value = next[FIELD_VALUES[part]];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
update: (next) => project(scope, next),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"maxWidth": "none",
|
|
55
55
|
"padding": "var(--jx-space-1) var(--jx-space-2)"
|
|
56
56
|
},
|
|
57
|
-
"& [part=\"panel\"]": {
|
|
58
|
-
"$description": "`#right-panel > .panel-body { flex: 1; min-height: 0; overflow-y: auto }`, now a flex column: Element's sub-switch sits above its leaves and must not scroll away with them. The kit's tab panel pads itself for a dialog; a dock's tab is the whole column and each body pads its own content.",
|
|
57
|
+
"& > [part=\"panel\"]": {
|
|
58
|
+
"$description": "`#right-panel > .panel-body { flex: 1; min-height: 0; overflow-y: auto }`, now a flex column: Element's sub-switch sits above its leaves and must not scroll away with them. The kit's tab panel pads itself for a dialog; a dock's tab is the whole column and each body pads its own content. The child combinator is load-bearing: the leaves mount in this same light DOM, the Style leaf's root and every `jx-color-field`'s picker are also `part=\"panel\"`, and a descendant selector put this `overflow: hidden` on them. The Style root then clipped its own rows, so `panel-body` below saw nothing to scroll and every property past the fold was unreachable.",
|
|
59
59
|
"display": "flex",
|
|
60
60
|
"flexDirection": "column",
|
|
61
61
|
"flex": "1",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"padding": "0",
|
|
64
64
|
"overflow": "hidden"
|
|
65
65
|
},
|
|
66
|
-
"& [part=\"panel\"][hidden]": {
|
|
66
|
+
"& > [part=\"panel\"][hidden]": {
|
|
67
67
|
"$description": "Out of sight, out of the layout and out of the accessibility tree, but not `display: none`: a panel that left the box tree is restyled whole when it comes back, and the Inspector shows its Page panel on every deselect and on every close of the Assistant (§6), which made each of those a 300-element style recalculation. `content-visibility: hidden` skips the subtree while keeping its computed style, so a return restyles only what changed while it was away; `visibility: hidden` keeps it unfocusable and unexposed, as `hidden` means.",
|
|
68
68
|
"display": "flex",
|
|
69
69
|
"flex": "0 0 0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
3
|
"$id": "StudioMediaLibrary",
|
|
4
|
-
"$description": "The Media workspace's library (`specs/studio.md` §22.6): the project's media as filters, a grid and the selected file's detail. ONE document in two modes. In `manage` it is the Media home tab, with Replace, Open in viewer and Delete under the detail; in `pick` it is the full-size picker inside a `jx-dialog` (`media-pick-dialog.json`), and the dialog's own footer answers Cancel or Use <file>. `media/media-library.ts` keeps every decision (which files a filter keeps, the sort, what a count may claim, where an arrow lands, what Replace writes) and hands this document strings and discriminants. **Unused is drawn with no number until the usage sweep has settled**, and Missing alt text and tags are not drawn at all: each needs a store that does not exist, and a filter that cannot be answered is not shipped as one that always answers nothing.",
|
|
4
|
+
"$description": "The Media workspace's library (`specs/studio.md` §22.6): the project's media as filters, a grid and the selected file's detail. ONE document in two modes. In `manage` it is the Media home tab, with Replace, Open in viewer, Rename, Duplicate and Delete under the detail; in `pick` it is the full-size picker inside a `jx-dialog` (`media-pick-dialog.json`), and the dialog's own footer answers Cancel or Use <file>. `media/media-library.ts` keeps every decision (which files a filter keeps, the sort, what a count may claim, where an arrow lands, what Replace writes) and hands this document strings and discriminants. **Unused is drawn with no number until the usage sweep has settled**, and Missing alt text and tags are not drawn at all: each needs a store that does not exist, and a filter that cannot be answered is not shipped as one that always answers nothing.",
|
|
5
5
|
"tagName": "div",
|
|
6
6
|
"attributes": {
|
|
7
7
|
"part": "media-library",
|
|
@@ -1578,6 +1578,134 @@
|
|
|
1578
1578
|
}
|
|
1579
1579
|
]
|
|
1580
1580
|
},
|
|
1581
|
+
{
|
|
1582
|
+
"tagName": "div",
|
|
1583
|
+
"attributes": {
|
|
1584
|
+
"part": "edit-slot",
|
|
1585
|
+
"role": "none"
|
|
1586
|
+
},
|
|
1587
|
+
"style": {
|
|
1588
|
+
"display": "contents"
|
|
1589
|
+
},
|
|
1590
|
+
"$switch": {
|
|
1591
|
+
"$ref": "#/state/editable"
|
|
1592
|
+
},
|
|
1593
|
+
"cases": {
|
|
1594
|
+
"yes": {
|
|
1595
|
+
"tagName": "jx-button",
|
|
1596
|
+
"attributes": {
|
|
1597
|
+
"part": "edit-image"
|
|
1598
|
+
},
|
|
1599
|
+
"$props": {
|
|
1600
|
+
"size": "sm",
|
|
1601
|
+
"variant": "secondary"
|
|
1602
|
+
},
|
|
1603
|
+
"onclick": {
|
|
1604
|
+
"$prototype": "Function",
|
|
1605
|
+
"body": [
|
|
1606
|
+
{
|
|
1607
|
+
"operator": "call",
|
|
1608
|
+
"target": {
|
|
1609
|
+
"$ref": "#/state/editImage"
|
|
1610
|
+
},
|
|
1611
|
+
"value": []
|
|
1612
|
+
}
|
|
1613
|
+
],
|
|
1614
|
+
"description": "Withheld, not disabled, for a file the editor cannot write: a refused format is not offered rather than offered and then refused (studio.md §9.5)."
|
|
1615
|
+
},
|
|
1616
|
+
"children": [
|
|
1617
|
+
{
|
|
1618
|
+
"tagName": "span",
|
|
1619
|
+
"textContent": "Edit image"
|
|
1620
|
+
}
|
|
1621
|
+
]
|
|
1622
|
+
},
|
|
1623
|
+
"no": {
|
|
1624
|
+
"tagName": "span",
|
|
1625
|
+
"attributes": {
|
|
1626
|
+
"hidden": true
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
"tagName": "jx-button",
|
|
1633
|
+
"attributes": {
|
|
1634
|
+
"part": "rename"
|
|
1635
|
+
},
|
|
1636
|
+
"$props": {
|
|
1637
|
+
"size": "sm",
|
|
1638
|
+
"variant": "secondary"
|
|
1639
|
+
},
|
|
1640
|
+
"onclick": {
|
|
1641
|
+
"$prototype": "Function",
|
|
1642
|
+
"body": [
|
|
1643
|
+
{
|
|
1644
|
+
"operator": "call",
|
|
1645
|
+
"target": {
|
|
1646
|
+
"$ref": "#/state/rename"
|
|
1647
|
+
},
|
|
1648
|
+
"value": []
|
|
1649
|
+
}
|
|
1650
|
+
]
|
|
1651
|
+
},
|
|
1652
|
+
"children": [
|
|
1653
|
+
{
|
|
1654
|
+
"tagName": "span",
|
|
1655
|
+
"textContent": "Rename"
|
|
1656
|
+
}
|
|
1657
|
+
]
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
"tagName": "div",
|
|
1661
|
+
"attributes": {
|
|
1662
|
+
"part": "duplicate-slot",
|
|
1663
|
+
"role": "none"
|
|
1664
|
+
},
|
|
1665
|
+
"style": {
|
|
1666
|
+
"display": "contents"
|
|
1667
|
+
},
|
|
1668
|
+
"$switch": {
|
|
1669
|
+
"$ref": "#/state/duplicable"
|
|
1670
|
+
},
|
|
1671
|
+
"cases": {
|
|
1672
|
+
"yes": {
|
|
1673
|
+
"tagName": "jx-button",
|
|
1674
|
+
"attributes": {
|
|
1675
|
+
"part": "duplicate"
|
|
1676
|
+
},
|
|
1677
|
+
"$props": {
|
|
1678
|
+
"size": "sm",
|
|
1679
|
+
"variant": "secondary"
|
|
1680
|
+
},
|
|
1681
|
+
"onclick": {
|
|
1682
|
+
"$prototype": "Function",
|
|
1683
|
+
"body": [
|
|
1684
|
+
{
|
|
1685
|
+
"operator": "call",
|
|
1686
|
+
"target": {
|
|
1687
|
+
"$ref": "#/state/duplicate"
|
|
1688
|
+
},
|
|
1689
|
+
"value": []
|
|
1690
|
+
}
|
|
1691
|
+
],
|
|
1692
|
+
"description": "Withheld, not disabled, on a backend that cannot read a file's raw bytes: a copy read as text would be a broken file (studio.md §9.3)."
|
|
1693
|
+
},
|
|
1694
|
+
"children": [
|
|
1695
|
+
{
|
|
1696
|
+
"tagName": "span",
|
|
1697
|
+
"textContent": "Duplicate"
|
|
1698
|
+
}
|
|
1699
|
+
]
|
|
1700
|
+
},
|
|
1701
|
+
"no": {
|
|
1702
|
+
"tagName": "span",
|
|
1703
|
+
"attributes": {
|
|
1704
|
+
"hidden": true
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1581
1709
|
{
|
|
1582
1710
|
"tagName": "jx-button",
|
|
1583
1711
|
"attributes": {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This is the adapter. `media/media-library.ts` is the flow: which files a filter keeps, in what
|
|
6
6
|
* order, what each count may claim, which tile is selected, where an arrow lands and what Replace,
|
|
7
|
-
* Delete and an upload write. It hands this module one projection, and this
|
|
8
|
-
* document a scope and reports the nodes the flow needs to hold.
|
|
7
|
+
* Rename, Duplicate, Delete and an upload write. It hands this module one projection, and this
|
|
8
|
+
* module hands the document a scope and reports the nodes the flow needs to hold.
|
|
9
9
|
*
|
|
10
10
|
* **Four islands, all through `onNodeCreated`.** The flow needs four nodes the document creates:
|
|
11
11
|
* each tile (watched for viewport entry, so a thumbnail is requested only when it is looked at, and
|
|
@@ -97,6 +97,13 @@ export interface MediaLibraryView {
|
|
|
97
97
|
previewSpecimen: string;
|
|
98
98
|
/** The Replace input's `accept`: the selected file's own format only. */
|
|
99
99
|
replaceAccept: string;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the selected file opens in the Image editor (studio.md §9.5): withheld on exactly the
|
|
102
|
+
* conditions `media.edit` gates on, so the palette and this pane cannot give two answers.
|
|
103
|
+
*/
|
|
104
|
+
editable: "yes" | "no";
|
|
105
|
+
/** Whether the host can copy the selected file: `media.duplicate` gates on the same capability. */
|
|
106
|
+
duplicable: "yes" | "no";
|
|
100
107
|
usageState: "hidden" | "shown";
|
|
101
108
|
usageHeadline: string;
|
|
102
109
|
usageFiles: MediaUsageRow[];
|
|
@@ -126,7 +133,13 @@ export interface MediaLibraryActions {
|
|
|
126
133
|
openDocument: (path: string) => void;
|
|
127
134
|
replace: (input: HTMLInputElement) => void;
|
|
128
135
|
remove: () => void;
|
|
136
|
+
/** Rename the selected file, through the Rename dialog. */
|
|
137
|
+
rename: () => void;
|
|
138
|
+
/** Copy the selected file to a new name beside it, and select the copy. */
|
|
139
|
+
duplicate: () => void;
|
|
129
140
|
openViewer: () => void;
|
|
141
|
+
/** Open the selected image in its Image editor. */
|
|
142
|
+
editImage: () => void;
|
|
130
143
|
reset: () => void;
|
|
131
144
|
/** A tile node was created for `file`. */
|
|
132
145
|
tileHost: (element: HTMLElement, file: string) => void;
|
|
@@ -174,6 +187,8 @@ export function emptyMediaLibraryView(): MediaLibraryView {
|
|
|
174
187
|
query: "",
|
|
175
188
|
region: "",
|
|
176
189
|
replaceAccept: "",
|
|
190
|
+
editable: "no",
|
|
191
|
+
duplicable: "no",
|
|
177
192
|
selectedFile: "",
|
|
178
193
|
sort: "",
|
|
179
194
|
sortOptions: [],
|
package/src/types.ts
CHANGED
|
@@ -406,7 +406,8 @@ export interface StudioPlatform {
|
|
|
406
406
|
* tainting stops being a thing that can happen rather than a thing that is worked around.
|
|
407
407
|
*
|
|
408
408
|
* Optional, and feature-detected like every optional member: a backend without it simply does not
|
|
409
|
-
* offer the image editor, which is the honest degradation
|
|
409
|
+
* offer the image editor or a file's Duplicate, which is the honest degradation
|
|
410
|
+
* `specs/desktop.md` §3.1 asks for.
|
|
410
411
|
*/
|
|
411
412
|
readFileBytes?: (path: string) => Promise<ArrayBuffer>;
|
|
412
413
|
writeFile: (path: string, content: string) => Promise<void>;
|
|
@@ -47,7 +47,8 @@ export function workspaceOfTab(tab: Tab): WorkspaceId {
|
|
|
47
47
|
if (mode === "git-diff") {
|
|
48
48
|
return "publish";
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// The viewer and its editing sibling (`media/image-editor.ts`, §9.5): one file, one workspace.
|
|
51
|
+
if (mode === "media" || mode === "image") {
|
|
51
52
|
return "media";
|
|
52
53
|
}
|
|
53
54
|
if (CONTENT_MODES.has(mode)) {
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
setCloseLanding,
|
|
47
47
|
setEmptyPaneFallback,
|
|
48
48
|
setTabScope,
|
|
49
|
+
tabHoldsEditBesideDocument,
|
|
49
50
|
workspace,
|
|
50
51
|
} from "./workspace";
|
|
51
52
|
import { homeWorkspaceOfTabId, isHomeWorkspace, workspaceHomeTabId } from "./workspace-ids";
|
|
@@ -119,7 +120,8 @@ export function viewsReplacedBy(
|
|
|
119
120
|
homeWorkspaceOfTabId(other) !== id &&
|
|
120
121
|
workspaceOfTab(tab) === id &&
|
|
121
122
|
!tab.pinned &&
|
|
122
|
-
!tab.doc.dirty
|
|
123
|
+
!tab.doc.dirty &&
|
|
124
|
+
!tabHoldsEditBesideDocument(tab)
|
|
123
125
|
);
|
|
124
126
|
});
|
|
125
127
|
}
|
|
@@ -1063,12 +1063,33 @@ export function promoteTab(tabId: string) {
|
|
|
1063
1063
|
*/
|
|
1064
1064
|
export function promoteDirtyPreviewTabs() {
|
|
1065
1065
|
for (const [id, tab] of workspace.tabs) {
|
|
1066
|
-
if (tab.preview && tab.doc.dirty) {
|
|
1066
|
+
if (tab.preview && (tab.doc.dirty || tabHoldsEditBesideDocument(tab))) {
|
|
1067
1067
|
promoteTab(id);
|
|
1068
1068
|
}
|
|
1069
1069
|
}
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
|
+
/** The answer to {@link tabHoldsEditBesideDocument}, injected by the module that holds the edit. */
|
|
1073
|
+
let editBesideDocument: ((tab: Tab) => boolean) | null = null;
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Register who answers {@link tabHoldsEditBesideDocument}. Injected rather than imported, because
|
|
1077
|
+
* the Image editor (`media/image-editor.ts`, studio.md §9.5) reaches the upload and collab layers,
|
|
1078
|
+
* every one of which imports this module.
|
|
1079
|
+
*/
|
|
1080
|
+
export function setEditBesideDocument(answer: (tab: Tab) => boolean): void {
|
|
1081
|
+
editBesideDocument = answer;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Whether this tab holds unsaved work that is NOT in its document: a media tab's document is a
|
|
1086
|
+
* stub, and an image edit is a recipe held beside it, so `tab.doc.dirty` cannot say. Every gate
|
|
1087
|
+
* that discards a tab without asking has to ask this too, or a pending crop goes in silence.
|
|
1088
|
+
*/
|
|
1089
|
+
export function tabHoldsEditBesideDocument(tab: Tab): boolean {
|
|
1090
|
+
return editBesideDocument?.(tab) ?? false;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1072
1093
|
/**
|
|
1073
1094
|
* Pin or unpin a tab, moving it to the boundary of the pinned prefix so the head of the strip is
|
|
1074
1095
|
* always exactly the pinned set.
|