@heycys/gallery 0.1.1 → 0.1.2
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/THIRD-PARTY-NOTICES.md +1 -1
- package/browser/editor/components/columns/page.mjs +4 -4
- package/browser/editor/components/embed/index.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +7 -6
- package/browser/editor/components/flowchart/page.mjs +1 -1
- package/browser/editor/components/icons.mjs +8 -0
- package/browser/editor/components/image/crop.mjs +6 -9
- package/browser/editor/components/installed/index.mjs +11 -4
- package/browser/editor/components/installed/page.mjs +27 -16
- package/browser/editor/components/installed/served.mjs +6 -5
- package/browser/editor/components/instances.mjs +18 -0
- package/browser/editor/components/list/page.mjs +3 -6
- package/browser/editor/components/registry.mjs +13 -4
- package/browser/editor/components/table/index.mjs +59 -2
- package/browser/editor/components/table/page.mjs +1 -1
- package/browser/editor/page/annotate/card.mjs +3 -6
- package/browser/editor/page/autosave.mjs +310 -37
- package/browser/editor/page/board/client.mjs +57 -0
- package/browser/editor/page/board/hint.mjs +145 -0
- package/browser/editor/page/board/history.mjs +119 -0
- package/browser/editor/page/board/index.mjs +970 -0
- package/browser/editor/page/board/settings.mjs +209 -0
- package/browser/editor/page/caret.mjs +1 -1
- package/browser/editor/page/changes/bar.mjs +6 -14
- package/browser/editor/page/changes/index.mjs +2 -2
- package/browser/editor/page/clipboard/import/html.mjs +29 -6
- package/browser/editor/page/clipboard/import/inline.mjs +32 -0
- package/browser/editor/page/clipboard/import/markdown.mjs +58 -13
- package/browser/editor/page/clipboard/import/read.mjs +6 -1
- package/browser/editor/page/clipboard/import/to-blocks.mjs +44 -12
- package/browser/editor/page/clipboard/import/yuque.mjs +622 -0
- package/browser/editor/page/clipboard/index.mjs +124 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +11 -11
- package/browser/editor/page/clipboard/own/chart-keys.mjs +41 -28
- package/browser/editor/page/clipboard/own/selection.mjs +1 -1
- package/browser/editor/page/columns/index.mjs +48 -46
- package/browser/editor/page/commit.mjs +80 -22
- package/browser/editor/page/conflicts.mjs +7 -7
- package/browser/editor/page/core.mjs +31 -15
- package/browser/editor/page/custom-page.mjs +106 -0
- package/browser/editor/page/editor.mjs +51 -14
- package/browser/editor/page/emoji-picker/styles.mjs +7 -12
- package/browser/editor/page/emoji-picker/upload.mjs +2 -2
- package/browser/editor/page/handle/index.mjs +5 -8
- package/browser/editor/page/history.mjs +24 -4
- package/browser/editor/page/links/index.mjs +5 -3
- package/browser/editor/page/links/pages.mjs +27 -9
- package/browser/editor/page/look.mjs +54 -3
- package/browser/editor/page/margin.mjs +18 -3
- package/browser/editor/page/media-menu/index.mjs +6 -7
- package/browser/editor/page/menu.mjs +18 -7
- package/browser/editor/page/notices.mjs +2 -2
- package/browser/editor/page/page-objects.mjs +13 -2
- package/browser/editor/page/panel.mjs +4 -9
- package/browser/editor/page/present-links.mjs +11 -2
- package/browser/editor/page/resize/index.mjs +42 -29
- package/browser/editor/page/served.mjs +14 -0
- package/browser/editor/page/session.mjs +48 -4
- package/browser/editor/page/slash-menu/index.mjs +3 -3
- package/browser/editor/page/table/cells.mjs +102 -46
- package/browser/editor/page/table/icons.mjs +2 -0
- package/browser/editor/page/table/index.mjs +29 -10
- package/browser/editor/page/table/pictures.mjs +299 -0
- package/browser/editor/page/table/views.mjs +150 -0
- package/browser/editor/page/text-select/index.mjs +26 -12
- package/browser/editor/page/todo-writes.mjs +75 -0
- package/browser/editor/page/typing/index.mjs +53 -3
- package/browser/editor/page/ui.mjs +13 -0
- package/browser/shared/block-packs.mjs +404 -0
- package/browser/shared/board-places.mjs +224 -0
- package/browser/shared/card-size.mjs +59 -0
- package/browser/shared/colors.mjs +3 -2
- package/browser/shared/component-catalog.mjs +323 -46
- package/browser/shared/component-source.mjs +37 -0
- package/browser/shared/components/index.mjs +12 -3
- package/browser/shared/components/installed.mjs +35 -3
- package/browser/shared/components/page.mjs +18 -8
- package/browser/shared/components/state.mjs +5 -1
- package/browser/shared/document-look.mjs +10 -5
- package/browser/shared/drafts.mjs +432 -0
- package/browser/shared/extension-sidebar.mjs +126 -0
- package/browser/shared/extensions.mjs +79 -0
- package/browser/shared/file-table.mjs +101 -0
- package/browser/shared/i18n/en.mjs +307 -27
- package/browser/shared/json-layout.mjs +47 -0
- package/browser/shared/net-cache.mjs +133 -0
- package/browser/shared/net-keys.mjs +32 -0
- package/browser/shared/net-policy.mjs +177 -0
- package/browser/shared/outline-shape.mjs +128 -0
- package/browser/shared/outline.mjs +331 -67
- package/browser/shared/pack-manifest.mjs +77 -0
- package/browser/shared/page-address.mjs +6 -3
- package/browser/shared/page-cards.mjs +308 -0
- package/browser/shared/page-links.mjs +69 -6
- package/browser/shared/pages.mjs +388 -0
- package/browser/shared/paths.mjs +6 -0
- package/browser/shared/preview-server.mjs +3 -7
- package/browser/shared/session.mjs +8 -1
- package/browser/shared/shipped-blocks.mjs +299 -0
- package/browser/shared/table-chart.mjs +33 -1
- package/browser/shared/todos.mjs +83 -0
- package/browser/shared/user-directory.mjs +24 -0
- package/browser/shared/versions.mjs +14 -0
- package/browser/shared/web-links.mjs +134 -0
- package/browser/shared/workspace-columns.mjs +120 -0
- package/browser/shared/workspace-name.mjs +154 -0
- package/browser/shared/workspace-query.mjs +125 -0
- package/browser/shared/workspace-registry.mjs +272 -0
- package/browser/shared/workspace.mjs +171 -20
- package/dist/chunk-2NRBY6AJ.mjs +2 -0
- package/dist/chunk-32ZUEFZ7.mjs +2 -0
- package/dist/chunk-35NQ4PLQ.mjs +2 -0
- package/dist/chunk-3KJHLHUO.mjs +2 -0
- package/dist/chunk-3NJACR4W.mjs +2 -0
- package/dist/chunk-4AV456RQ.mjs +2 -0
- package/dist/chunk-4OZWXZE5.mjs +5 -0
- package/dist/chunk-5XYHDBJ5.mjs +2 -0
- package/dist/chunk-5ZJA7CXT.mjs +2 -0
- package/dist/chunk-6A53MZYZ.mjs +2 -0
- package/dist/chunk-6EXH3ORZ.mjs +166 -0
- package/dist/chunk-6EZ7FNIK.mjs +3 -0
- package/dist/{chunk-V5JTW2LH.mjs → chunk-6QBHU4DF.mjs} +1 -1
- package/dist/{chunk-Y6RLRPG3.mjs → chunk-6VTU6KEO.mjs} +1 -1
- package/dist/chunk-B4P2CM2J.mjs +3 -0
- package/dist/{chunk-FDLXYYG4.mjs → chunk-BPL64HGA.mjs} +1 -1
- package/dist/chunk-BVEFGE4B.mjs +2 -0
- package/dist/{chunk-LS4F7JUM.mjs → chunk-BZ5RAF7L.mjs} +1 -1
- package/dist/chunk-CRJEG7MT.mjs +2 -0
- package/dist/{chunk-LCTHTZIL.mjs → chunk-D4CKRD3V.mjs} +91 -91
- package/dist/chunk-D4EURAX4.mjs +4 -0
- package/dist/chunk-DQCE7TJY.mjs +5 -0
- package/dist/chunk-DR73Y2UJ.mjs +3 -0
- package/dist/{chunk-TMYZB6AV.mjs → chunk-EKNHM3P4.mjs} +1 -1
- package/dist/{chunk-GB5SOYQJ.mjs → chunk-ERLAIBOA.mjs} +2 -2
- package/dist/chunk-EURLTOTZ.mjs +4 -0
- package/dist/chunk-FJYN7RTH.mjs +4 -0
- package/dist/chunk-FWNRPMCJ.mjs +12 -0
- package/dist/chunk-FWW4CSAZ.mjs +2 -0
- package/dist/{chunk-2B5E4CDJ.mjs → chunk-GPIWQ4X4.mjs} +1 -1
- package/dist/chunk-GY3FAUVM.mjs +2 -0
- package/dist/chunk-HLYV7RIH.mjs +2 -0
- package/dist/chunk-HMYTM3GT.mjs +15 -0
- package/dist/chunk-IA4PSIMD.mjs +2 -0
- package/dist/chunk-JEUK3ZO7.mjs +2 -0
- package/dist/chunk-KTKHRC6H.mjs +2 -0
- package/dist/chunk-L7UP52JO.mjs +2 -0
- package/dist/{chunk-SYGN7GLU.mjs → chunk-MHW7ZKOU.mjs} +1 -1
- package/dist/chunk-N76HWBA3.mjs +10 -0
- package/dist/chunk-N7AZZLGT.mjs +548 -0
- package/dist/chunk-NAMN3RAF.mjs +2 -0
- package/dist/{chunk-5ZKJ3H4P.mjs → chunk-NFMCP2HI.mjs} +3 -3
- package/dist/chunk-NG375ILQ.mjs +2 -0
- package/dist/chunk-NVRNLK25.mjs +6 -0
- package/dist/chunk-NWFBE2YA.mjs +2 -0
- package/dist/chunk-NWR2LT5B.mjs +2 -0
- package/dist/chunk-OJKMINIU.mjs +20 -0
- package/dist/{chunk-4FS7Z7WR.mjs → chunk-OUUP3WKT.mjs} +1 -1
- package/dist/{chunk-BGGDRNYQ.mjs → chunk-OW5NWPOJ.mjs} +1 -1
- package/dist/chunk-PAREWQGB.mjs +4 -0
- package/dist/chunk-POQSE4C2.mjs +2 -0
- package/dist/chunk-PUOQ65NC.mjs +2 -0
- package/dist/{chunk-XNROSH2T.mjs → chunk-PVER7LSC.mjs} +2 -2
- package/dist/chunk-PZDLJJTJ.mjs +3 -0
- package/dist/chunk-Q47PLKIJ.mjs +15 -0
- package/dist/chunk-Q4UEPUWF.mjs +2 -0
- package/dist/chunk-QKXMPUXC.mjs +2 -0
- package/dist/chunk-QOUEFUCH.mjs +16 -0
- package/dist/chunk-RFDZXFJF.mjs +13 -0
- package/dist/chunk-RKPWLGZ3.mjs +4 -0
- package/dist/chunk-RMECZA2K.mjs +2 -0
- package/dist/{chunk-7DTINW6U.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
- package/dist/chunk-SX4LJYT6.mjs +8 -0
- package/dist/chunk-SXHBYZST.mjs +16 -0
- package/dist/chunk-TANZRINB.mjs +2 -0
- package/dist/chunk-TEZAWDSQ.mjs +2 -0
- package/dist/chunk-UBBRTJ2R.mjs +24 -0
- package/dist/chunk-UFNYFFKO.mjs +20 -0
- package/dist/chunk-UGWYKHBL.mjs +170 -0
- package/dist/chunk-UKSHDZZ6.mjs +2 -0
- package/dist/chunk-UNOYVIYR.mjs +2 -0
- package/dist/{chunk-E7LWZRXY.mjs → chunk-V6RIYQZZ.mjs} +1 -1
- package/dist/chunk-VJPFVNNW.mjs +15 -0
- package/dist/chunk-VS3ZMVGX.mjs +5 -0
- package/dist/chunk-VTKNEYT3.mjs +6 -0
- package/dist/chunk-VXACTLO3.mjs +3 -0
- package/dist/chunk-YI43RG4P.mjs +2 -0
- package/dist/chunk-YQGT4AB2.mjs +6 -0
- package/dist/chunk-ZB4H7ZN4.mjs +2 -0
- package/dist/{chunk-IVDLMMBD.mjs → chunk-ZHEHPMBK.mjs} +1 -1
- package/dist/chunk-ZTHICSSF.mjs +2 -0
- package/dist/chunk-ZUNVAXLH.mjs +2 -0
- package/dist/chunk-ZWPMCK7P.mjs +2 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/{chunk-U6KNPJPR.mjs → update-fetch.mjs} +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +95 -0
- package/kit/board.js +250 -0
- package/kit/charts.js +5 -71
- package/kit/code.js +14 -14
- package/kit/report.css +255 -17
- package/kit/runtime.js +1600 -125
- package/kit/state.js +10 -1
- package/kit/tables.js +128 -43
- package/kit/ui.js +77 -35
- package/package.json +2 -1
- package/packs/gallery/card/component.json +14 -0
- package/packs/gallery/card/example.html +10 -0
- package/packs/gallery/card/preview.svg +1 -0
- package/packs/gallery/card/style.css +6 -0
- package/packs/gallery/card/template.html +6 -0
- package/packs/gallery/card/writing.md +14 -0
- package/packs/gallery/chart-card/component.json +15 -0
- package/packs/gallery/chart-card/example.html +14 -0
- package/packs/gallery/chart-card/kit.js +130 -0
- package/packs/gallery/chart-card/preview.svg +1 -0
- package/packs/gallery/chart-card/style.css +42 -0
- package/packs/gallery/chart-card/template.html +13 -0
- package/packs/gallery/chart-card/writing.md +20 -0
- package/packs/gallery/chart-view/component.json +20 -0
- package/packs/gallery/chart-view/example.html +13 -0
- package/packs/gallery/chart-view/kit.js +355 -0
- package/packs/gallery/chart-view/preview.svg +1 -0
- package/packs/gallery/chart-view/style.css +94 -0
- package/packs/gallery/chart-view/template.html +3 -0
- package/packs/gallery/chart-view/writing.md +22 -0
- package/packs/gallery/countdown/component.json +18 -0
- package/packs/gallery/countdown/example.html +3 -0
- package/packs/gallery/countdown/kit.js +156 -0
- package/packs/gallery/countdown/preview.svg +1 -0
- package/packs/gallery/countdown/style.css +130 -0
- package/packs/gallery/countdown/template.html +3 -0
- package/packs/gallery/countdown/writing.md +22 -0
- package/packs/gallery/habit/component.json +14 -0
- package/packs/gallery/habit/example.html +3 -0
- package/packs/gallery/habit/kit.js +295 -0
- package/packs/gallery/habit/preview.svg +1 -0
- package/packs/gallery/habit/style.css +246 -0
- package/packs/gallery/habit/template.html +3 -0
- package/packs/gallery/habit/writing.md +21 -0
- package/packs/gallery/heatmap/component.json +18 -0
- package/packs/gallery/heatmap/example.html +3 -0
- package/packs/gallery/heatmap/kit.js +351 -0
- package/packs/gallery/heatmap/preview.svg +1 -0
- package/packs/gallery/heatmap/style.css +201 -0
- package/packs/gallery/heatmap/template.html +3 -0
- package/packs/gallery/heatmap/writing.md +21 -0
- package/packs/gallery/kanban/component.json +18 -0
- package/packs/gallery/kanban/example.html +12 -0
- package/packs/gallery/kanban/kit.js +540 -0
- package/packs/gallery/kanban/preview.svg +1 -0
- package/packs/gallery/kanban/style.css +212 -0
- package/packs/gallery/kanban/template.html +3 -0
- package/packs/gallery/kanban/writing.md +20 -0
- package/packs/gallery/metric/component.json +22 -0
- package/packs/gallery/metric/example.html +15 -0
- package/packs/gallery/metric/kit.js +248 -0
- package/packs/gallery/metric/preview.svg +1 -0
- package/packs/gallery/metric/style.css +92 -0
- package/packs/gallery/metric/template.html +3 -0
- package/packs/gallery/metric/writing.md +23 -0
- package/packs/gallery/note/component.json +14 -0
- package/packs/gallery/note/example.html +3 -0
- package/packs/gallery/note/kit.js +102 -0
- package/packs/gallery/note/preview.svg +1 -0
- package/packs/gallery/note/style.css +76 -0
- package/packs/gallery/note/template.html +3 -0
- package/packs/gallery/note/writing.md +18 -0
- package/packs/gallery/pack.json +8 -0
- package/packs/gallery/recent/component.json +13 -0
- package/packs/gallery/recent/example.html +1 -0
- package/packs/gallery/recent/kit.js +238 -0
- package/packs/gallery/recent/preview.svg +1 -0
- package/packs/gallery/recent/style.css +158 -0
- package/packs/gallery/recent/template.html +1 -0
- package/packs/gallery/recent/writing.md +15 -0
- package/packs/gallery/static/component.json +13 -0
- package/packs/gallery/static/example.html +11 -0
- package/packs/gallery/static/preview.svg +1 -0
- package/packs/gallery/static/style.css +138 -0
- package/packs/gallery/static/template.html +1 -0
- package/packs/gallery/static/writing.md +18 -0
- package/packs/gallery/stats/component.json +13 -0
- package/packs/gallery/stats/example.html +1 -0
- package/packs/gallery/stats/kit.js +166 -0
- package/packs/gallery/stats/preview.svg +1 -0
- package/packs/gallery/stats/style.css +151 -0
- package/packs/gallery/stats/template.html +1 -0
- package/packs/gallery/stats/writing.md +11 -0
- package/packs/gallery/today/component.json +12 -0
- package/packs/gallery/today/example.html +1 -0
- package/packs/gallery/today/kit.js +185 -0
- package/packs/gallery/today/preview.svg +1 -0
- package/packs/gallery/today/style.css +120 -0
- package/packs/gallery/today/template.html +1 -0
- package/packs/gallery/today/writing.md +14 -0
- package/packs/gallery/todos/component.json +19 -0
- package/packs/gallery/todos/example.html +3 -0
- package/packs/gallery/todos/kit.js +342 -0
- package/packs/gallery/todos/preview.svg +1 -0
- package/packs/gallery/todos/style.css +267 -0
- package/packs/gallery/todos/template.html +3 -0
- package/packs/gallery/todos/writing.md +30 -0
- package/packs/gallery/weather/component.json +19 -0
- package/packs/gallery/weather/example.html +3 -0
- package/packs/gallery/weather/kit.js +312 -0
- package/packs/gallery/weather/preview.svg +1 -0
- package/packs/gallery/weather/style.css +258 -0
- package/packs/gallery/weather/template.html +3 -0
- package/packs/gallery/weather/writing.md +26 -0
- package/packs/gallery-soft/pack.json +8 -0
- package/release.json +12 -10
- package/sdk/doc-kit.d.ts +396 -5
- package/sdk/doc-state.d.ts +2 -0
- package/skill/SKILL.md +56 -23
- package/skill/references/block-script.md +140 -0
- package/skill/references/building-blocks.md +257 -0
- package/skill/references/extensions.md +59 -0
- package/skill/references/pages-and-data.md +221 -0
- package/skill/references/writing-html.md +6 -4
- package/skill/sdk/doc-kit.d.ts +396 -5
- package/skill/sdk/doc-state.d.ts +2 -0
- package/web/assets/index-BK2e_07w.js +13 -0
- package/web/assets/index-CWFI83mF.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-3HIRPVLU.mjs +0 -2
- package/dist/chunk-5MHMDLZD.mjs +0 -4
- package/dist/chunk-64K7IPLO.mjs +0 -255
- package/dist/chunk-6QXGZM36.mjs +0 -8
- package/dist/chunk-6RGJOMJP.mjs +0 -16
- package/dist/chunk-AJ73XJF3.mjs +0 -2
- package/dist/chunk-AUZGSZJH.mjs +0 -13
- package/dist/chunk-BFE45AKL.mjs +0 -2
- package/dist/chunk-BXO2ZY5P.mjs +0 -2
- package/dist/chunk-CEH4MRCC.mjs +0 -2
- package/dist/chunk-CY2FNNAB.mjs +0 -16
- package/dist/chunk-DFUDV4LX.mjs +0 -2
- package/dist/chunk-DKXG4XDG.mjs +0 -2
- package/dist/chunk-EZQ34M3F.mjs +0 -124
- package/dist/chunk-EZY3WC6Z.mjs +0 -15
- package/dist/chunk-FUKQNOQS.mjs +0 -9
- package/dist/chunk-JPOSLGOC.mjs +0 -6
- package/dist/chunk-L3QKH4OK.mjs +0 -26
- package/dist/chunk-LAE5NQ4B.mjs +0 -2
- package/dist/chunk-LEF6DRCK.mjs +0 -59
- package/dist/chunk-NO5NJCHO.mjs +0 -2
- package/dist/chunk-NQPVAU63.mjs +0 -2
- package/dist/chunk-NSKUTQ5J.mjs +0 -5
- package/dist/chunk-O35FIXRB.mjs +0 -2
- package/dist/chunk-P2CAZPDP.mjs +0 -3
- package/dist/chunk-PL6GHLGD.mjs +0 -15
- package/dist/chunk-QJIR2BLP.mjs +0 -6
- package/dist/chunk-QUPLVPB2.mjs +0 -7
- package/dist/chunk-R7BNENLY.mjs +0 -4
- package/dist/chunk-S6O4UHLC.mjs +0 -2
- package/dist/chunk-SGJWQKEI.mjs +0 -2
- package/dist/chunk-TX6JH7TR.mjs +0 -2
- package/dist/chunk-TYK6VNAB.mjs +0 -10
- package/dist/chunk-UALRNVSE.mjs +0 -4
- package/dist/chunk-V3HXVQEM.mjs +0 -2
- package/dist/chunk-V5VXUJKG.mjs +0 -2
- package/dist/chunk-VBXMADMP.mjs +0 -2
- package/dist/chunk-VNV55RUA.mjs +0 -2
- package/dist/chunk-VPD2PS35.mjs +0 -2
- package/dist/chunk-W4VKN3GF.mjs +0 -7
- package/dist/chunk-W5BXXPDJ.mjs +0 -3
- package/dist/chunk-XUNHRCOG.mjs +0 -6
- package/skill/references/building-components.md +0 -367
- package/web/assets/index-Bh74DfsV.css +0 -1
- package/web/assets/index-UW0RyFFt.js +0 -13
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import { GAP, GRID, cellAt, fitHeight, gridFor, moved, pushDown, sizeFor, spanOf } from "../../../shared/board-places.mjs";
|
|
2
|
+
import { cellsOf } from "../../../shared/card-size.mjs";
|
|
3
|
+
import { language, t } from "../../../shared/i18n/index.mjs";
|
|
4
|
+
import { BLOCK_ICON, svg } from "../../components/icons.mjs";
|
|
5
|
+
import { instanceAround, instanceOf, packageOf } from "../../components/instances.mjs";
|
|
6
|
+
import { edgeScroll } from "../edge-scroll.mjs";
|
|
7
|
+
import { gestureLock } from "../gesture.mjs";
|
|
8
|
+
import { ICONS, createMenu } from "../menu.mjs";
|
|
9
|
+
import { say } from "../notices.mjs";
|
|
10
|
+
import { createTodoWrites } from "../todo-writes.mjs";
|
|
11
|
+
import { tooltip } from "../tooltip.mjs";
|
|
12
|
+
import { addStyle, element, mount } from "../ui.mjs";
|
|
13
|
+
import { createClient } from "./client.mjs";
|
|
14
|
+
import { createHistory } from "./history.mjs";
|
|
15
|
+
import { showHint, showNews } from "./hint.mjs";
|
|
16
|
+
import { closeSettings, formOpen, openSettings } from "./settings.mjs";
|
|
17
|
+
|
|
18
|
+
// A custom page's board edited (docs/render.md "A custom page"; the plan's §5.3, the spec §6 and §8):
|
|
19
|
+
// what the reader does to the cards of its boards, each change written through the page service
|
|
20
|
+
// (src/editor/page-service.mjs: ./client.mjs) and undone by the page's own undo (./history.mjs). The
|
|
21
|
+
// kit's board script (src/render/kit/board/) lays the cards out; this moves them, as the editor's
|
|
22
|
+
// handle, menus and notices do on a document's page, in the editor's look (look.mjs: grays for every
|
|
23
|
+
// line, guide and handle, never a bright blue; nothing that snaps hard).
|
|
24
|
+
//
|
|
25
|
+
// - The grip, the card's one handle (no ⋯ beside it). The pointer on a card shows, INSIDE it at its
|
|
26
|
+
// top-right corner (a GRIP.size px box, GRIP.inset px from the card's top and right edges, which no
|
|
27
|
+
// block puts a control or text in: the corner is the grip's), the grip (six dots, their quiet gray,
|
|
28
|
+
// darker under the pointer, grey behind only while pressed or while its menu is open). It shows while
|
|
29
|
+
// the pointer is over the card, while its menu is open and while the keyboard focus is in the card
|
|
30
|
+
// (or on the grip), never outside the card. A press that moves past THRESHOLD px drags the card;
|
|
31
|
+
// one that moves less, or a key (Enter, Space) on the grip, opens the card's menu: what is dragged
|
|
32
|
+
// and selected in a card is its block's (DocKit.drag never moves a card). A bare card (its block's
|
|
33
|
+
// card.bare) shows where it stands by a dashed line of the guides' gray.
|
|
34
|
+
// - Moving. The card follows the pointer, and where it would go, the whole cells nearest to it
|
|
35
|
+
// (shared/board-places.mjs cellAt), shows as a quiet spot (a fill of its own, no line; no guide
|
|
36
|
+
// lines stand where its edge meets another card's); the cards it would meet are drawn pushed down
|
|
37
|
+
// under it (pushDown, the service's own rule: DocBoard.draft), never up. Escape, or the pointer taken
|
|
38
|
+
// away, puts all back; a drop sends `place`, and the board stands as the service answers. The rows
|
|
39
|
+
// above a board's first card are not drawn (DocBoard.skipped): the pointer's row is the drawn one
|
|
40
|
+
// plus the rows skipped as the drag began, so that a card let go where it was writes nothing and one
|
|
41
|
+
// held above the first row goes above it (drawn at the top, the others under it).
|
|
42
|
+
// - Resizing. A card whose block says card.resize has a grip at its bottom-right: it follows the
|
|
43
|
+
// pointer, the spot showing the size it snaps to, the nearest of card.sizes, else whole cells
|
|
44
|
+
// between card.min and card.max (sizeFor), never past the row's end; one of fit content only
|
|
45
|
+
// gets wider (its height is its content's); one whose manifest frees it (`any`) reaches every
|
|
46
|
+
// whole size. The menu's 大小 › gives the listed sizes (the common ones), in a card of rows of
|
|
47
|
+
// words alone, only as wide as they are.
|
|
48
|
+
// - The card's menu (the grip's click): 设置… (a block with settings: ./settings.mjs, written by the
|
|
49
|
+
// service's `settings`), 大小 ›, 删除 (`remove`).
|
|
50
|
+
// - 添加自定义块: in a workbench (`hosted`) the workbench's top bar has its button, which asks by the
|
|
51
|
+
// session's `add-card` command (./index.mjs `addCard`; custom-page.mjs): the library opens in the
|
|
52
|
+
// page, under the button's place (the rect the workbench sends, in the frame's own coordinates) or
|
|
53
|
+
// centred, and adds to the page's FIRST board; a page open alone in a tab has the button of its own
|
|
54
|
+
// at the top right of each board, above it. The library (the service's GET library: the blocks
|
|
55
|
+
// whose places hold page and are not turned off, with their previews, Gallery's own not yet in the
|
|
56
|
+
// workspace too, which the service puts in): the card put at the first free spot by its block's
|
|
57
|
+
// default size (`add`); the page is then shown again (the host's hidden-frame swap), which brings
|
|
58
|
+
// the block's script and style.
|
|
59
|
+
// - A block's own write (context.edit, the kit's cancellable doc-component-edit): taken here, its
|
|
60
|
+
// drafts put on the page and sent as `edit`, one step of undo.
|
|
61
|
+
// - Undo and redo (Ctrl+Z, Ctrl+Shift+Z or Ctrl+Y, never in a field): the reverse op of each step,
|
|
62
|
+
// a move's and a resize's putting back up the cards they pushed (`arrange`).
|
|
63
|
+
// - A to-do of a document checked by a block (context.workspace.setTodo: ../todo-writes.mjs, the plan's
|
|
64
|
+
// 5.1), which writes that document, not this page: one step of undo each that changed it, its undo the
|
|
65
|
+
// check the other way (the item named by the id the write gave it), never the document's undo.
|
|
66
|
+
// - Each op, and each undo and redo, is a task of the client's: sent once those before it are done
|
|
67
|
+
// (answered, their cards' bases and their undo steps taken in), so that what it sends goes with the
|
|
68
|
+
// bases they left, never with those it had when it was asked.
|
|
69
|
+
// - A narrow board (fewer than twelve columns: a room narrower than twelve of the fixed cells, the kit's
|
|
70
|
+
// gridFor, its cards drawn one after another, flow) moves nothing: its grip opens the menu and does not
|
|
71
|
+
// drag (its tooltip says the window wants to be wider), no resize, no 大小 ›. The file holds twelve-column
|
|
72
|
+
// places, and a gesture in the flow has no one twelve-column place that leaves the flow as it was
|
|
73
|
+
// dragged, nor shows what the twelve columns then get; folding the sidebar or widening the window gives
|
|
74
|
+
// the board its twelve, the cards where the file says.
|
|
75
|
+
// - The card of a block that does not run on a page (the server holds its instance in a template:
|
|
76
|
+
// a block of documents alone, one turned off) says so in its frame.
|
|
77
|
+
// - 恢复默认卡片 (the home page's, which the workbench's sidebar asks of it: custom-page.mjs): the
|
|
78
|
+
// service's restore-defaults, the default cards the home page misses put in at free spots, one step
|
|
79
|
+
// of undo that takes them all out again, and the page shown again with their blocks; the workbench
|
|
80
|
+
// says what came back, its 撤销 the page's undo of that step while it is still the last one.
|
|
81
|
+
// - The home page's first-open note (./hint.mjs), while the map says `hint`.
|
|
82
|
+
// - Shown again: the page's own writes are no one else's (the live script's "page <version>" of a
|
|
83
|
+
// version its ops wrote changes nothing: __galleryPageChanged); someone else's (an Agent's write of
|
|
84
|
+
// page.html, an op answered on a version the page did not know) shows the page again once nothing
|
|
85
|
+
// of the page waits: no op unanswered, no drag, no form, no field of a card typed in and not left
|
|
86
|
+
// (DocKit.input commits as it is left: plan 4.5).
|
|
87
|
+
|
|
88
|
+
const BOARD = ".gallery-board";
|
|
89
|
+
const CARD = ".gallery-card";
|
|
90
|
+
const THRESHOLD = 4; // px a press moves before it is a drag
|
|
91
|
+
const GRIP = { size: 28, inset: 6 }; // the grip's box, and its room from the card's top and right edges (inside the card)
|
|
92
|
+
const FIELDS = "input, textarea, select, [contenteditable]:not([contenteditable=false])";
|
|
93
|
+
|
|
94
|
+
const DOTS = '<svg viewBox="0 0 10 16" aria-hidden="true"><circle cx="2.5" cy="2" r="1.25"/><circle cx="7.5" cy="2" r="1.25"/><circle cx="2.5" cy="8" r="1.25"/><circle cx="7.5" cy="8" r="1.25"/><circle cx="2.5" cy="14" r="1.25"/><circle cx="7.5" cy="14" r="1.25"/></svg>';
|
|
95
|
+
const CORNER = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M13 6.5 6.5 13M13 10.5 10.5 13"/></svg>';
|
|
96
|
+
const GEAR = svg('<circle cx="10" cy="10" r="2.25"/><path d="M10 3.25v1.5M10 15.25v1.5M3.25 10h1.5M15.25 10h1.5M5.2 5.2l1.06 1.06M13.74 13.74l1.06 1.06M5.2 14.8l1.06-1.06M13.74 6.26l1.06-1.06"/>');
|
|
97
|
+
const SIZE = svg('<rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5"/><path d="M10 3.25v13.5M3.25 10H10"/>');
|
|
98
|
+
const BLOCK = BLOCK_ICON;
|
|
99
|
+
|
|
100
|
+
const STYLE = `
|
|
101
|
+
/* As wide as the page and clipped there: a grip left where a wider window put it (hidden, or not yet put again) never widens the page. */
|
|
102
|
+
.ge-board-layer { position: absolute; left: 0; top: 0; width: 100%; height: 0; overflow-x: clip; z-index: 30; }
|
|
103
|
+
/* The grip: one box inside the card at its top-right corner, an icon button, so no fill under the pointer (the dots go darker), grey only while pressed or while its menu is open. */
|
|
104
|
+
.ge-board-grip { position: absolute; display: flex; align-items: center; justify-content: center; box-sizing: border-box; width: ${GRIP.size}px; height: ${GRIP.size}px; border-radius: 6px; outline: none; color: var(--ge-ico3); cursor: grab;
|
|
105
|
+
user-select: none; touch-action: none; opacity: 0; pointer-events: none; transition: opacity .1s ease-out; }
|
|
106
|
+
.ge-board-grip[data-shown] { opacity: 1; pointer-events: auto; }
|
|
107
|
+
.ge-board-grip[data-narrow] { cursor: pointer; }
|
|
108
|
+
.ge-board-grip svg { display: block; width: 10px; height: 16px; fill: currentColor; pointer-events: none; }
|
|
109
|
+
.ge-board-grip:hover { color: var(--ge-ico); }
|
|
110
|
+
.ge-board-grip[data-pressed] { background: var(--ge-hover); color: var(--ge-ico); }
|
|
111
|
+
.ge-board-grip:focus-visible { box-shadow: 0 0 0 2px var(--ge-halo); }
|
|
112
|
+
.ge-board-resize { position: absolute; display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 4px; color: var(--ge-ico3); cursor: nwse-resize; opacity: 0; pointer-events: none; touch-action: none; transition: opacity .1s ease-out; }
|
|
113
|
+
.ge-board-resize[data-shown] { opacity: 1; pointer-events: auto; }
|
|
114
|
+
.ge-board-resize:hover, .ge-board-resize[data-pressed] { background: var(--ge-hover); }
|
|
115
|
+
.ge-board-resize svg { display: block; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; pointer-events: none; }
|
|
116
|
+
.ge-board-add { position: absolute; display: flex; align-items: center; gap: 4px; box-sizing: border-box; height: 28px; padding: 0 8px 0 6px; border: 0; border-radius: 6px; background: none; color: var(--ge-tx2);
|
|
117
|
+
font: 14px/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif; white-space: nowrap; cursor: pointer; user-select: none; }
|
|
118
|
+
.ge-board-add:hover { background: var(--ge-hover); color: var(--ge-tx); }
|
|
119
|
+
.ge-board-add:active, .ge-board-add[data-pressed] { background: var(--ge-press); color: var(--ge-tx); }
|
|
120
|
+
.ge-board-add svg { display: block; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }
|
|
121
|
+
.gallery-board > .ge-board-spot { box-sizing: border-box; grid-column: calc(var(--gallery-x, 0) + 1) / span var(--gallery-w, 4); grid-row: calc(var(--gallery-y, 0) + 1) / span var(--gallery-h, 2);
|
|
122
|
+
border-radius: var(--gallery-card-radius, 12px); background: light-dark(rgb(55 53 47 / 7%), rgb(255 255 255 / 8%)); pointer-events: none; }
|
|
123
|
+
.gallery-board > .gallery-card[data-ge-lifted] { z-index: 3; box-shadow: var(--ge-shadow-pop); cursor: grabbing; }
|
|
124
|
+
.gallery-board > .gallery-card[data-ge-sizing] { z-index: 3; }
|
|
125
|
+
.gallery-board > .gallery-card[data-ge-bare][data-ge-hover] { outline: 1px dashed var(--ge-guide); outline-offset: 2px; }
|
|
126
|
+
/* The card the grip stands in: a head's quiet note in its corner fades (the kit's --gallery-grip-shown, board.css). */
|
|
127
|
+
.gallery-board > .gallery-card[data-ge-hover] { --gallery-grip-shown: 1; }
|
|
128
|
+
${gestureLock("data-ge-dragging", { cursor: "grabbing", select: true })}
|
|
129
|
+
${gestureLock("data-ge-sizing", { cursor: "nwse-resize", select: true })}
|
|
130
|
+
.gallery-card > .ge-card-note { display: flex; align-items: center; justify-content: center; box-sizing: border-box; min-height: 100%; padding: 4px; color: var(--ge-tx3); font-size: 13px; line-height: 1.5; text-align: center; }
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
const said = (words) => (typeof words === "string" ? words : words?.[language] ?? words?.zh ?? words?.en ?? "");
|
|
134
|
+
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
|
135
|
+
const cellsSame = (a, b) => a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The board edited, on a custom page served with its map (`map`: { slug, token, version, blocks, hint },
|
|
139
|
+
* routes.mjs: each block of its cards { state, card, settings, parts, fit, bare, name }; `hint`: the
|
|
140
|
+
* home page's first-open note is shown). `reload()`: the page shown again (the workbench's swap;
|
|
141
|
+
* location.reload on its own). `hosted`: the page stands in a workbench's frame, whose top bar has the
|
|
142
|
+
* 添加自定义块 button (the page draws none of its own). Returns { swap() (show the page again once nothing waits), undo(),
|
|
143
|
+
* redo(), idle() (a promise of the ops sent being answered), restoreDefaults() (恢复默认卡片: its
|
|
144
|
+
* answer, { ok, added, skipped }), undoRestore(cards) (that step undone while it is the last one),
|
|
145
|
+
* addCard({ anchor }) (the workbench's 添加自定义块: its answer, { ok, opened }) }.
|
|
146
|
+
*/
|
|
147
|
+
export function startBoard({ map, reload, hosted = false }) {
|
|
148
|
+
addStyle(STYLE);
|
|
149
|
+
const { slug, token, version, blocks = {} } = map;
|
|
150
|
+
const history = createHistory(slug);
|
|
151
|
+
const menu = createMenu();
|
|
152
|
+
const kept = new Map(); // a card taken out -> its node, for its undo to put back
|
|
153
|
+
const leaving = new Set(); // the cards taken off the page whose remove is not yet answered
|
|
154
|
+
// The to-dos its blocks check (workspace.setTodo): each that changed its document one step of undo; an
|
|
155
|
+
// undo's or a redo's own write (`step`) none.
|
|
156
|
+
const todos = createTodoWrites({
|
|
157
|
+
token,
|
|
158
|
+
done: (ask, answer) => {
|
|
159
|
+
if (ask.step || !answer.ok || !answer.changed) return;
|
|
160
|
+
history.push({ kind: "checked", slug: ask.slug, ref: { ...ask.ref, block: answer.block ?? ask.ref.block }, done: ask.done });
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
todos.listen();
|
|
164
|
+
|
|
165
|
+
// ---------- the page shown again ----------
|
|
166
|
+
let gesture = null; // a drag or a resize under way
|
|
167
|
+
let typing = null; // a field of a card typed in and not yet left
|
|
168
|
+
let swapWanted = false;
|
|
169
|
+
let waitTimer = 0;
|
|
170
|
+
const busy = () => client.busy || gesture !== null || formOpen() || (typing !== null && typing.isConnected && document.activeElement === typing);
|
|
171
|
+
const trySwap = () => {
|
|
172
|
+
clearTimeout(waitTimer);
|
|
173
|
+
if (!swapWanted) return;
|
|
174
|
+
if (busy()) {
|
|
175
|
+
waitTimer = setTimeout(trySwap, 250);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
swapWanted = false;
|
|
179
|
+
history.know(basesNow());
|
|
180
|
+
reload();
|
|
181
|
+
};
|
|
182
|
+
const wantSwap = () => {
|
|
183
|
+
swapWanted = true;
|
|
184
|
+
trySwap();
|
|
185
|
+
};
|
|
186
|
+
const client = createClient({ slug, token, version, others: wantSwap });
|
|
187
|
+
window.__galleryPageChanged = (next) => {
|
|
188
|
+
if (client.ours(next)) return;
|
|
189
|
+
if (client.busy) client.idle().then(() => { if (!client.ours(next)) wantSwap(); });
|
|
190
|
+
else wantSwap();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ---------- the cards ----------
|
|
194
|
+
const boards = () => [...document.querySelectorAll(BOARD)];
|
|
195
|
+
const cardsIn = (board) => [...board.querySelectorAll(`:scope > ${CARD}`)];
|
|
196
|
+
const allCards = () => boards().flatMap(cardsIn);
|
|
197
|
+
const cardById = (id) => allCards().find((card) => card.dataset.cardId === id) ?? null;
|
|
198
|
+
const boardById = (id) => boards().find((board) => board.dataset.boardId === id) ?? null;
|
|
199
|
+
const blockIdOf = (card) => packageOf(instanceOf(card)) ?? packageOf(card.querySelector(":scope > template[data-ge-held]")?.content.firstElementChild);
|
|
200
|
+
const infoOf = (card) => blocks[blockIdOf(card)] ?? null;
|
|
201
|
+
const runs = (card) => infoOf(card)?.state === "page" && !card.dataset.geState;
|
|
202
|
+
const narrow = (board) => (Number(board.dataset.columns) || GRID.columns) < GRID.columns;
|
|
203
|
+
const basesNow = () => Object.fromEntries(allCards().map((card) => [card.dataset.cardId, card.dataset.geBase]));
|
|
204
|
+
const layoutOf = (board) => cardsIn(board).map((card) => ({ id: card.dataset.cardId, ...cellsOf(card) }));
|
|
205
|
+
const setCells = (card, cells) => {
|
|
206
|
+
for (const axis of ["x", "y", "w", "h"]) if (card.getAttribute(`data-${axis}`) !== String(cells[axis])) card.setAttribute(`data-${axis}`, String(cells[axis]));
|
|
207
|
+
};
|
|
208
|
+
// A board's grid as drawn: where its columns are (the board stands them in its middle: `rect`, their
|
|
209
|
+
// box's left and right, the board's top), its cell's side, a cell and its gap.
|
|
210
|
+
const geometry = (board) => {
|
|
211
|
+
const box = board.getBoundingClientRect();
|
|
212
|
+
const grid = gridFor(board.clientWidth);
|
|
213
|
+
const cell = parseFloat(board.style.getPropertyValue("--gallery-cell")) || grid.cell;
|
|
214
|
+
const columns = Number(board.dataset.columns) || grid.columns;
|
|
215
|
+
const inset = Math.max(0, (board.clientWidth - spanOf(columns, cell)) / 2) + board.clientLeft;
|
|
216
|
+
const rect = { left: box.left + inset, right: box.left + inset + spanOf(columns, cell), top: box.top, bottom: box.bottom };
|
|
217
|
+
return { rect, cell, step: cell + GAP };
|
|
218
|
+
};
|
|
219
|
+
const drawn = (card) => globalThis.DocBoard?.drawn(card) ?? cellsOf(card);
|
|
220
|
+
// How many rows above its first card the board does not draw (the kit's DocBoard.skipped): its drawn row
|
|
221
|
+
// r is the file's row r + skipped, which the spot, a dragged card's place and a drop are reckoned by.
|
|
222
|
+
const skipped = (board) => globalThis.DocBoard?.skipped?.(board) ?? 0;
|
|
223
|
+
const draft = (board, cells) => globalThis.DocBoard?.draft(board, cells);
|
|
224
|
+
|
|
225
|
+
// What an answer says of the cards: each at its cells, with its base. A card it names that the page
|
|
226
|
+
// has not, or one the page has that it does not name, is someone else's doing: the page is shown again.
|
|
227
|
+
function took(answer, { own = [] } = {}) {
|
|
228
|
+
const named = new Set();
|
|
229
|
+
for (const card of answer.cards ?? []) {
|
|
230
|
+
named.add(card.id);
|
|
231
|
+
const node = cardById(card.id);
|
|
232
|
+
if (!node) {
|
|
233
|
+
if (!own.includes(card.id) && !leaving.has(card.id)) wantSwap(); // one the page took out meanwhile, its remove asked, is its own
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
setCells(node, card);
|
|
237
|
+
node.dataset.geBase = card.base;
|
|
238
|
+
}
|
|
239
|
+
if (allCards().some((card) => !named.has(card.dataset.cardId))) wantSwap();
|
|
240
|
+
history.know(basesNow());
|
|
241
|
+
}
|
|
242
|
+
// An op refused or failed: said, and the page shown as the file is.
|
|
243
|
+
function failed(answer) {
|
|
244
|
+
if (answer?.refused) say(t("这张卡片刚被改过,请再操作一次"), "error");
|
|
245
|
+
else if (answer?.reason === "offline") say(t("连不上本地服务"), "error");
|
|
246
|
+
else say(t("没能改动这个页面"), "error");
|
|
247
|
+
wantSwap();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// The card of a block that does not run here says why, in its frame.
|
|
251
|
+
for (const card of allCards()) {
|
|
252
|
+
const why = card.dataset.geState === "document" ? t("这个块只能放在文档里") : card.dataset.geState === "disabled" ? t("这个块已停用") : null;
|
|
253
|
+
if (why) card.append(element("div", { class: "ge-card-note", "data-doc-dev": "", text: why }));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------- the grip, the corner, 添加自定义块 ----------
|
|
257
|
+
const layer = mount(element("div", { class: "ge-board-layer" }));
|
|
258
|
+
const grip = element("div", { class: "ge-board-grip", role: "button", tabindex: "-1", "aria-haspopup": "menu", "aria-label": t("拖动以移动卡片,点击打开菜单"), html: DOTS });
|
|
259
|
+
// On a narrow board the grip only opens the menu: its tooltip says so, and what gives it its drag back.
|
|
260
|
+
tooltip(grip, () => (shown && narrow(shown.parentElement) ? t("点击打开菜单") : t("拖动以移动")), () => (shown && narrow(shown.parentElement) ? t("窗口宽一些才能拖动") : t("点击打开菜单")));
|
|
261
|
+
const corner = element("div", { class: "ge-board-resize", role: "button", "aria-label": t("拖动调整大小"), html: CORNER });
|
|
262
|
+
tooltip(corner, t("拖动调整大小"));
|
|
263
|
+
layer.append(grip, corner);
|
|
264
|
+
let shown = null; // the card the grip stands in
|
|
265
|
+
let hideTimer = 0;
|
|
266
|
+
let at = null; // where the pointer was last seen: { x, y }
|
|
267
|
+
const layerBox = () => layer.getBoundingClientRect();
|
|
268
|
+
const resizable = (card) => runs(card) && Boolean(infoOf(card)?.card?.resize) && !narrow(card.parentElement);
|
|
269
|
+
const cardOf = (node) => (node instanceof Element ? node.closest(`${BOARD} > ${CARD}`) : null);
|
|
270
|
+
function show(card) {
|
|
271
|
+
clearTimeout(hideTimer);
|
|
272
|
+
if (shown !== card) shown?.removeAttribute("data-ge-hover");
|
|
273
|
+
shown = card;
|
|
274
|
+
card.setAttribute("data-ge-hover", "");
|
|
275
|
+
grip.setAttribute("data-shown", "");
|
|
276
|
+
grip.tabIndex = 0;
|
|
277
|
+
put(card);
|
|
278
|
+
}
|
|
279
|
+
// The grip and the corner on `card` as it stands now (a hide still pending stays pending).
|
|
280
|
+
function put(card) {
|
|
281
|
+
const box = layerBox();
|
|
282
|
+
const rect = card.getBoundingClientRect();
|
|
283
|
+
// Inside the card at its top-right corner, left of the scrollbar a card that scrolls has.
|
|
284
|
+
const bar = Math.max(0, card.offsetWidth - card.clientWidth - 2 * card.clientLeft);
|
|
285
|
+
grip.style.left = `${Math.round(rect.right - bar - GRIP.inset - GRIP.size - box.left)}px`;
|
|
286
|
+
grip.style.top = `${Math.round(rect.top + GRIP.inset - box.top)}px`;
|
|
287
|
+
grip.toggleAttribute("data-narrow", narrow(card.parentElement));
|
|
288
|
+
if (resizable(card)) {
|
|
289
|
+
corner.style.left = `${Math.round(rect.right - box.left - 20)}px`;
|
|
290
|
+
corner.style.top = `${Math.round(rect.bottom - box.top - 20)}px`;
|
|
291
|
+
corner.setAttribute("data-shown", "");
|
|
292
|
+
} else corner.removeAttribute("data-shown");
|
|
293
|
+
}
|
|
294
|
+
function hide() {
|
|
295
|
+
clearTimeout(hideTimer);
|
|
296
|
+
if (menu.isOpen || gesture) return;
|
|
297
|
+
shown?.removeAttribute("data-ge-hover");
|
|
298
|
+
shown = null;
|
|
299
|
+
grip.removeAttribute("data-shown");
|
|
300
|
+
grip.tabIndex = -1;
|
|
301
|
+
corner.removeAttribute("data-shown");
|
|
302
|
+
}
|
|
303
|
+
// The card whose grip shows: the one the pointer is over, else the one the keyboard focus is in (or
|
|
304
|
+
// the grip's own focus: the card it stands in), none when neither is.
|
|
305
|
+
const underPointer = () => {
|
|
306
|
+
if (!at) return null;
|
|
307
|
+
const node = document.elementFromPoint(at.x, at.y);
|
|
308
|
+
return node && (grip.contains(node) || corner.contains(node)) ? shown : cardOf(node);
|
|
309
|
+
};
|
|
310
|
+
const focusedCard = () => {
|
|
311
|
+
const node = document.activeElement;
|
|
312
|
+
if (!(node instanceof Element) || node === document.body) return null;
|
|
313
|
+
if (grip.contains(node)) return shown;
|
|
314
|
+
return node.matches(":focus-visible") ? cardOf(node) : null;
|
|
315
|
+
};
|
|
316
|
+
function settle() {
|
|
317
|
+
clearTimeout(hideTimer);
|
|
318
|
+
if (menu.isOpen || gesture) return;
|
|
319
|
+
const card = underPointer() ?? focusedCard();
|
|
320
|
+
if (card) show(card);
|
|
321
|
+
else hide();
|
|
322
|
+
}
|
|
323
|
+
const hideSoon = () => {
|
|
324
|
+
clearTimeout(hideTimer);
|
|
325
|
+
hideTimer = setTimeout(settle, 150);
|
|
326
|
+
};
|
|
327
|
+
addEventListener("pointermove", (event) => {
|
|
328
|
+
at = { x: event.clientX, y: event.clientY };
|
|
329
|
+
if (gesture || menu.isOpen) return; // the grip stays at the card whose menu is open
|
|
330
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
331
|
+
if (grip.contains(target) || corner.contains(target)) return void clearTimeout(hideTimer);
|
|
332
|
+
const card = cardOf(target);
|
|
333
|
+
if (card) show(card);
|
|
334
|
+
else if (shown) hideSoon();
|
|
335
|
+
}, { passive: true });
|
|
336
|
+
document.documentElement.addEventListener("pointerleave", () => {
|
|
337
|
+
at = null;
|
|
338
|
+
if (!gesture) hideSoon();
|
|
339
|
+
});
|
|
340
|
+
addEventListener("scroll", () => { if (shown && !gesture) show(shown); }, { passive: true });
|
|
341
|
+
addEventListener("focusin", () => { if (!gesture) settle(); });
|
|
342
|
+
addEventListener("focusout", () => { if (!gesture) hideSoon(); });
|
|
343
|
+
|
|
344
|
+
// 添加自定义块 of a page open alone in a tab: a button at the top right of each board, above it (in a
|
|
345
|
+
// workbench the top bar has it: addCard below).
|
|
346
|
+
const adds = new Map(); // a board -> its 添加自定义块
|
|
347
|
+
const PLUS = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4.75v10.5M4.75 10h10.5"/></svg>';
|
|
348
|
+
function placeAdds() {
|
|
349
|
+
if (hosted) return;
|
|
350
|
+
const box = layerBox();
|
|
351
|
+
for (const board of boards()) {
|
|
352
|
+
let add = adds.get(board);
|
|
353
|
+
if (!add) {
|
|
354
|
+
add = element("button", { type: "button", class: "ge-board-add", "aria-label": t("添加自定义块") }, element("span", { html: PLUS }), element("span", { text: t("添加自定义块") }));
|
|
355
|
+
add.addEventListener("mousedown", (event) => event.preventDefault());
|
|
356
|
+
add.addEventListener("click", () => {
|
|
357
|
+
window.focus();
|
|
358
|
+
openLibrary(board, { button: add });
|
|
359
|
+
});
|
|
360
|
+
layer.append(add);
|
|
361
|
+
adds.set(board, add);
|
|
362
|
+
}
|
|
363
|
+
const { rect } = geometry(board); // its right edge the last column's
|
|
364
|
+
add.style.left = `${Math.round(rect.right - box.left - add.offsetWidth)}px`;
|
|
365
|
+
add.style.top = `${Math.round(Math.max(4, rect.top - box.top - add.offsetHeight - 8))}px`;
|
|
366
|
+
}
|
|
367
|
+
for (const [board, add] of adds) {
|
|
368
|
+
if (!board.isConnected) {
|
|
369
|
+
add.remove();
|
|
370
|
+
adds.delete(board);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
placeAdds();
|
|
375
|
+
// The room or a board's columns changed (the window resized, the sidebar folded, a card's size told): the
|
|
376
|
+
// grip stands on its card where it is now, its menu with it (closed where the board's columns changed
|
|
377
|
+
// under it: its rows, 大小 ›, read them).
|
|
378
|
+
let menuColumns = "";
|
|
379
|
+
const columnsNow = () => boards().map((board) => board.dataset.columns).join();
|
|
380
|
+
function placeAgain() {
|
|
381
|
+
placeAdds();
|
|
382
|
+
if (!shown || gesture) return;
|
|
383
|
+
put(shown);
|
|
384
|
+
if (!menu.isOpen) return;
|
|
385
|
+
if (columnsNow() !== menuColumns) menu.close();
|
|
386
|
+
else menu.follow(grip.getBoundingClientRect());
|
|
387
|
+
}
|
|
388
|
+
addEventListener("doc-layout-change", placeAgain);
|
|
389
|
+
addEventListener("resize", placeAgain);
|
|
390
|
+
new ResizeObserver(placeAgain).observe(document.body);
|
|
391
|
+
new MutationObserver(placeAgain).observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ["data-columns"] });
|
|
392
|
+
document.fonts?.ready.then(placeAgain);
|
|
393
|
+
|
|
394
|
+
// ---------- the menu ----------
|
|
395
|
+
const press = new Map(); // a pointer's press on the grip: { card, x, y }
|
|
396
|
+
function openMenu(card) {
|
|
397
|
+
const info = infoOf(card);
|
|
398
|
+
const board = card.parentElement;
|
|
399
|
+
const cells = cellsOf(card);
|
|
400
|
+
const items = [];
|
|
401
|
+
if (runs(card) && info.settings?.length) items.push({ label: t("设置…"), icon: GEAR, run: () => openCardSettings(card) });
|
|
402
|
+
const sizes = (info?.card?.sizes ?? []).filter(([w]) => cells.x + w <= 12);
|
|
403
|
+
if (resizable(card) && sizes.length > 1) {
|
|
404
|
+
items.push({
|
|
405
|
+
label: t("大小"), icon: SIZE, icons: false, compact: true,
|
|
406
|
+
submenu: () => sizes.map(([w, h]) => ({ label: `${w} × ${h}`, current: w === cells.w && h === cells.h, run: () => placeCard(card, { ...cells, w, h }) })),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (items.length) items.push({ separator: true });
|
|
410
|
+
items.push({ label: t("删除"), icon: ICONS.trash, run: () => removeCard(card) });
|
|
411
|
+
if (!board) return;
|
|
412
|
+
show(card);
|
|
413
|
+
menuColumns = columnsNow();
|
|
414
|
+
grip.setAttribute("data-pressed", "");
|
|
415
|
+
menu.open(items, grip.getBoundingClientRect(), {
|
|
416
|
+
label: t("卡片菜单"), highlight: false, align: "end",
|
|
417
|
+
onClose: () => {
|
|
418
|
+
grip.removeAttribute("data-pressed");
|
|
419
|
+
hideSoon();
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
grip.addEventListener("keydown", (event) => {
|
|
424
|
+
// Enter with the menu open is the menu's own (the keys below run its highlighted row); only a closed menu is the grip's to open.
|
|
425
|
+
if (event.defaultPrevented || event.isComposing || (event.key !== "Enter" && event.key !== " ") || !shown || gesture || menu.isOpen) return;
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
event.stopPropagation();
|
|
428
|
+
openMenu(shown);
|
|
429
|
+
});
|
|
430
|
+
addEventListener("pointerdown", (event) => {
|
|
431
|
+
if (!menu.isOpen) return;
|
|
432
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
433
|
+
if (menu.contains(target) || grip.contains(target) || [...adds.values()].some((add) => add.contains(target))) return;
|
|
434
|
+
menu.close();
|
|
435
|
+
}, true);
|
|
436
|
+
|
|
437
|
+
// ---------- moving and resizing ----------
|
|
438
|
+
const spot = element("div", { class: "ge-board-spot", "data-doc-dev": "" });
|
|
439
|
+
// Near the window's top or bottom edge a drag or a resize scrolls the page (the editor's edge-scroll.mjs
|
|
440
|
+
// rule, faster the nearer the edge), each frame, and follows what came into view.
|
|
441
|
+
let pointer = null;
|
|
442
|
+
let scrolling = 0;
|
|
443
|
+
const scrollNear = () => {
|
|
444
|
+
scrolling = 0;
|
|
445
|
+
if (!gesture || !pointer) return;
|
|
446
|
+
if (edgeScroll(pointer.clientX, pointer.clientY)) follow(pointer);
|
|
447
|
+
scrolling = requestAnimationFrame(scrollNear);
|
|
448
|
+
};
|
|
449
|
+
// The spot at cells of the file, drawn where its board draws them.
|
|
450
|
+
const placeSpot = (board, cells) => {
|
|
451
|
+
for (const axis of ["x", "y", "w", "h"]) spot.style.setProperty(`--gallery-${axis}`, String(axis === "y" ? cells.y - skipped(board) : cells[axis]));
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
function begin(kind, card, event) {
|
|
455
|
+
const board = card.parentElement;
|
|
456
|
+
const file = cellsOf(card);
|
|
457
|
+
const rect = card.getBoundingClientRect();
|
|
458
|
+
// skip: the rows the board skipped as the gesture began, which the kit keeps while its draft is shown
|
|
459
|
+
// (a card held above the first row skips fewer): the pointer's row is reckoned by it throughout.
|
|
460
|
+
gesture = { kind, card, board, file, skip: skipped(board), target: { ...file }, grab: { x: event.clientX - rect.left, y: event.clientY - rect.top }, start: { x: event.clientX, y: event.clientY, width: rect.width, height: rect.height, scroll: scrollY } };
|
|
461
|
+
document.documentElement.setAttribute(kind === "move" ? "data-ge-dragging" : "data-ge-sizing", "");
|
|
462
|
+
card.setAttribute(kind === "move" ? "data-ge-lifted" : "data-ge-sizing", "");
|
|
463
|
+
placeSpot(board, drawn(card));
|
|
464
|
+
board.append(spot);
|
|
465
|
+
grip.removeAttribute("data-shown");
|
|
466
|
+
corner.removeAttribute("data-shown");
|
|
467
|
+
follow(event);
|
|
468
|
+
scrolling = requestAnimationFrame(scrollNear);
|
|
469
|
+
}
|
|
470
|
+
function follow(event) {
|
|
471
|
+
const { kind, card, board, file, skip, grab, start } = gesture;
|
|
472
|
+
pointer = { clientX: event.clientX, clientY: event.clientY };
|
|
473
|
+
const { rect, cell, step } = geometry(board);
|
|
474
|
+
const fits = card.dataset.geFit === "content";
|
|
475
|
+
let target;
|
|
476
|
+
if (kind === "move") {
|
|
477
|
+
const left = event.clientX - grab.x - rect.left;
|
|
478
|
+
const top = event.clientY - grab.y - rect.top;
|
|
479
|
+
target = { ...file, x: clamp(cellAt(left, cell), 0, 12 - file.w), y: Math.max(0, cellAt(top, cell) + skip) };
|
|
480
|
+
} else {
|
|
481
|
+
// The card follows the pointer between the least and the most it can snap to (its listed sizes', or
|
|
482
|
+
// its min and max), never past its row's end; the spot shows what it snaps to.
|
|
483
|
+
const block = infoOf(card)?.card ?? {};
|
|
484
|
+
const listed = block.any ? null : block.sizes?.filter(([w]) => file.x + w <= 12); // a card its block frees (any) takes every whole size
|
|
485
|
+
const least = listed?.length ? { w: Math.min(...listed.map(([w]) => w)), h: Math.min(...listed.map(([, h]) => h)) } : { w: block.min?.w ?? 1, h: block.min?.h ?? 1 };
|
|
486
|
+
const most = listed?.length ? { w: Math.max(...listed.map(([w]) => w)), h: Math.max(...listed.map(([, h]) => h)) } : { w: Math.min(12 - file.x, block.max?.w ?? 12), h: block.max?.h ?? 24 };
|
|
487
|
+
const scrolled = scrollY - start.scroll;
|
|
488
|
+
const width = clamp(start.width + event.clientX - start.x, spanOf(least.w, cell), spanOf(most.w, cell));
|
|
489
|
+
const height = clamp(start.height + event.clientY - start.y + scrolled, spanOf(least.h, cell), spanOf(most.h, cell));
|
|
490
|
+
const asked = { w: Math.max(1, Math.round((width + GAP) / step)), h: Math.max(1, Math.round((height + GAP) / step)) };
|
|
491
|
+
const size = listed?.length ? sizeFor({ sizes: listed }, asked) : sizeFor(block, { w: Math.min(asked.w, 12 - file.x), h: asked.h }, 12 - file.x);
|
|
492
|
+
target = { ...file, w: size.w, h: fits ? file.h : size.h };
|
|
493
|
+
card.style.width = `${Math.round(width)}px`;
|
|
494
|
+
if (!fits) card.style.height = `${Math.round(height)}px`;
|
|
495
|
+
}
|
|
496
|
+
if (!cellsSame(target, gesture.target) || !gesture.drafted) {
|
|
497
|
+
gesture.target = target;
|
|
498
|
+
gesture.drafted = true;
|
|
499
|
+
draft(board, new Map([[card, target]]));
|
|
500
|
+
placeSpot(board, drawn(card));
|
|
501
|
+
}
|
|
502
|
+
if (kind === "move") {
|
|
503
|
+
const now = drawn(card);
|
|
504
|
+
card.style.translate = `${Math.round(event.clientX - grab.x - rect.left - now.x * step)}px ${Math.round(event.clientY - grab.y - rect.top - (now.y - skipped(board)) * step)}px`;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function finish(drop) {
|
|
508
|
+
const done = gesture;
|
|
509
|
+
gesture = null;
|
|
510
|
+
cancelAnimationFrame(scrolling);
|
|
511
|
+
pointer = null;
|
|
512
|
+
const { card, board, file, target, kind } = done;
|
|
513
|
+
document.documentElement.removeAttribute("data-ge-dragging");
|
|
514
|
+
document.documentElement.removeAttribute("data-ge-sizing");
|
|
515
|
+
card.removeAttribute("data-ge-lifted");
|
|
516
|
+
card.removeAttribute("data-ge-sizing");
|
|
517
|
+
card.style.translate = "";
|
|
518
|
+
card.style.width = "";
|
|
519
|
+
card.style.height = "";
|
|
520
|
+
spot.remove();
|
|
521
|
+
if (drop && !cellsSame(target, file)) placeCard(card, target, { kind });
|
|
522
|
+
draft(board, null);
|
|
523
|
+
trySwap();
|
|
524
|
+
hideSoon(); // the grip comes back where the pointer is
|
|
525
|
+
}
|
|
526
|
+
grip.addEventListener("pointerdown", (event) => {
|
|
527
|
+
if (event.button !== 0 || !shown || gesture) return;
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
window.focus(); // the page's keys (Escape) in a workbench, whose frame a press kept from moving the focus would not take them
|
|
530
|
+
grip.setPointerCapture(event.pointerId);
|
|
531
|
+
grip.setAttribute("data-pressed", "");
|
|
532
|
+
press.set(event.pointerId, { card: shown, x: event.clientX, y: event.clientY, opened: menu.isOpen });
|
|
533
|
+
});
|
|
534
|
+
grip.addEventListener("pointermove", (event) => {
|
|
535
|
+
const held = press.get(event.pointerId);
|
|
536
|
+
if (!held) return;
|
|
537
|
+
if (gesture) return follow(event);
|
|
538
|
+
if (Math.hypot(event.clientX - held.x, event.clientY - held.y) < THRESHOLD) return;
|
|
539
|
+
held.moved = true; // a press that went this far is no click: a narrow board's moves nothing and opens nothing
|
|
540
|
+
if (narrow(held.card.parentElement)) return;
|
|
541
|
+
menu.close();
|
|
542
|
+
begin("move", held.card, { clientX: held.x, clientY: held.y });
|
|
543
|
+
follow(event);
|
|
544
|
+
});
|
|
545
|
+
const release = (event, drop) => {
|
|
546
|
+
const held = press.get(event.pointerId);
|
|
547
|
+
if (!held) return;
|
|
548
|
+
press.delete(event.pointerId);
|
|
549
|
+
grip.removeAttribute("data-pressed");
|
|
550
|
+
corner.removeAttribute("data-pressed");
|
|
551
|
+
if (gesture) return finish(drop);
|
|
552
|
+
// A click opens the card's menu, and closes it where it stood open as the press began.
|
|
553
|
+
if (drop && held.kind !== "resize" && !held.moved) {
|
|
554
|
+
if (held.opened && menu.isOpen) menu.close();
|
|
555
|
+
else openMenu(held.card);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
grip.addEventListener("pointerup", (event) => release(event, true));
|
|
559
|
+
grip.addEventListener("pointercancel", (event) => release(event, false));
|
|
560
|
+
grip.addEventListener("lostpointercapture", (event) => release(event, false));
|
|
561
|
+
corner.addEventListener("pointerdown", (event) => {
|
|
562
|
+
if (event.button !== 0 || !shown || gesture || !resizable(shown)) return;
|
|
563
|
+
event.preventDefault();
|
|
564
|
+
window.focus();
|
|
565
|
+
corner.setPointerCapture(event.pointerId);
|
|
566
|
+
corner.setAttribute("data-pressed", "");
|
|
567
|
+
press.set(event.pointerId, { card: shown, x: event.clientX, y: event.clientY, kind: "resize" });
|
|
568
|
+
begin("resize", shown, event);
|
|
569
|
+
});
|
|
570
|
+
corner.addEventListener("pointermove", (event) => { if (press.has(event.pointerId) && gesture) follow(event); });
|
|
571
|
+
corner.addEventListener("pointerup", (event) => release(event, true));
|
|
572
|
+
corner.addEventListener("pointercancel", (event) => release(event, false));
|
|
573
|
+
corner.addEventListener("lostpointercapture", (event) => release(event, false));
|
|
574
|
+
|
|
575
|
+
// ---------- the ops ----------
|
|
576
|
+
// Each op is a task of the client's (./client.mjs): what the page shows of it is drawn at once, what it
|
|
577
|
+
// sends is built once the tasks before it are done (each card's base as their answers left it), and its
|
|
578
|
+
// answer and its undo step are taken in before the next task starts. One done inside another task (an
|
|
579
|
+
// undo's) is handed that task's `send` and runs there.
|
|
580
|
+
const inTask = (send, work) => (send ? work(send) : client.run(work));
|
|
581
|
+
|
|
582
|
+
// A card at `cells` (a drop, a resize, 大小 ›): the board as the service's rule has it at once, then as it answers.
|
|
583
|
+
function placeCard(card, cells, { kind = "move" } = {}) {
|
|
584
|
+
const board = card.parentElement;
|
|
585
|
+
const before = layoutOf(board);
|
|
586
|
+
const id = card.dataset.cardId;
|
|
587
|
+
const after = pushDown(before, { id, ...cells });
|
|
588
|
+
for (const one of after) {
|
|
589
|
+
const node = cardById(one.id);
|
|
590
|
+
if (node) setCells(node, one);
|
|
591
|
+
}
|
|
592
|
+
return client.run(async (send) => {
|
|
593
|
+
const answer = await send({ op: "place", card: id, x: cells.x, y: cells.y, w: cells.w, h: cells.h, base: card.dataset.geBase });
|
|
594
|
+
if (!answer?.ok) {
|
|
595
|
+
for (const one of before) {
|
|
596
|
+
const node = cardById(one.id);
|
|
597
|
+
if (node) setCells(node, one);
|
|
598
|
+
}
|
|
599
|
+
return failed(answer);
|
|
600
|
+
}
|
|
601
|
+
took(answer);
|
|
602
|
+
const now = answer.cards.filter((one) => one.board === board.dataset.boardId);
|
|
603
|
+
const changed = moved(before, now).map((one) => ({ id: one.id, before: before.find((was) => was.id === one.id), after: { x: one.x, y: one.y, w: one.w, h: one.h } })).filter(({ before: was }) => was);
|
|
604
|
+
if (changed.length && kind !== "measure") history.push({ kind: "cells", cards: changed.map(({ id: one, before: was, after: is }) => ({ id: one, before: { x: was.x, y: was.y, w: was.w, h: was.h }, after: is })) });
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// A card taken out (删除, an add's undo, a remove's redo): off the page at once, then by the service
|
|
609
|
+
// (`send`: inside a task already), one step of undo where `step` says so.
|
|
610
|
+
function removeCard(card, { step = true, send = null } = {}) {
|
|
611
|
+
const board = card.parentElement;
|
|
612
|
+
const id = card.dataset.cardId;
|
|
613
|
+
const at = card.nextSibling;
|
|
614
|
+
if (shown === card) hide();
|
|
615
|
+
card.remove();
|
|
616
|
+
leaving.add(id);
|
|
617
|
+
return inTask(send, async (post) => {
|
|
618
|
+
const answer = await post({ op: "remove", card: id, base: card.dataset.geBase });
|
|
619
|
+
leaving.delete(id);
|
|
620
|
+
if (!answer?.ok) {
|
|
621
|
+
board.insertBefore(card, at?.parentNode === board ? at : null);
|
|
622
|
+
return failed(answer) ?? false;
|
|
623
|
+
}
|
|
624
|
+
kept.set(id, card);
|
|
625
|
+
took(answer);
|
|
626
|
+
if (step) history.push({ kind: "remove", id, board: board.dataset.boardId, html: answer.removed.html });
|
|
627
|
+
return true;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// A card put back (`html`, as remove gave it): its node, where this page kept it, else the page shown again. Inside an undo's task.
|
|
632
|
+
async function putBack(step, send) {
|
|
633
|
+
const board = boardById(step.board) ?? boards()[0];
|
|
634
|
+
if (!board) return false;
|
|
635
|
+
const answer = await send({ op: "add", html: step.html, board: board.dataset.boardId });
|
|
636
|
+
if (!answer?.ok) return failed(answer) ?? false;
|
|
637
|
+
const node = kept.get(step.id);
|
|
638
|
+
if (answer.card !== step.id) {
|
|
639
|
+
history.renamed(step.id, answer.card);
|
|
640
|
+
step.id = answer.card;
|
|
641
|
+
}
|
|
642
|
+
if (node) {
|
|
643
|
+
kept.delete(step.id);
|
|
644
|
+
node.dataset.cardId = answer.card;
|
|
645
|
+
board.append(node);
|
|
646
|
+
}
|
|
647
|
+
took(answer, { own: [answer.card] });
|
|
648
|
+
if (!node) wantSwap();
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// A block's data parts (`parts`: name -> its inside) written: on the page, then by the service.
|
|
653
|
+
const partNodes = (card, names) => {
|
|
654
|
+
const at = infoOf(card)?.parts ?? {};
|
|
655
|
+
const instance = instanceOf(card);
|
|
656
|
+
return Object.fromEntries(names.map((name) => [name, at[name] && instance ? [...instance.querySelectorAll(at[name])].find((node) => instanceAround(node) === instance) ?? null : null]));
|
|
657
|
+
};
|
|
658
|
+
const putInto = (node, html) => {
|
|
659
|
+
if (!node) return;
|
|
660
|
+
if (node.localName === "script") node.textContent = html;
|
|
661
|
+
else node.innerHTML = html;
|
|
662
|
+
};
|
|
663
|
+
// On the page at once; then, in its turn (`send`: inside a task already), by the service, one step of
|
|
664
|
+
// undo where `step` says so. Gives the answer, or null where it was refused (the parts put back).
|
|
665
|
+
function writeParts(card, parts, { step = false, send = null } = {}) {
|
|
666
|
+
const nodes = partNodes(card, Object.keys(parts));
|
|
667
|
+
const was = Object.fromEntries(Object.entries(nodes).map(([name, node]) => [name, node ? (node.localName === "script" ? node.textContent : node.innerHTML) : null]));
|
|
668
|
+
for (const [name, html] of Object.entries(parts)) putInto(nodes[name], html);
|
|
669
|
+
return inTask(send, async (post) => {
|
|
670
|
+
const answer = await post({ op: "edit", card: card.dataset.cardId, parts, base: card.dataset.geBase });
|
|
671
|
+
if (!answer?.ok) {
|
|
672
|
+
for (const [name, html] of Object.entries(was)) if (html !== null) putInto(nodes[name], html);
|
|
673
|
+
failed(answer);
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
took(answer);
|
|
677
|
+
if (step) history.push({ kind: "edit", id: card.dataset.cardId, parts: answer.parts });
|
|
678
|
+
return answer;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---------- settings ----------
|
|
683
|
+
function settingsOf(card) {
|
|
684
|
+
const node = partNodes(card, ["settings"]).settings;
|
|
685
|
+
try {
|
|
686
|
+
const value = JSON.parse(node?.textContent?.trim() || "{}");
|
|
687
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
688
|
+
} catch {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function openCardSettings(card) {
|
|
693
|
+
const info = infoOf(card);
|
|
694
|
+
openSettings({
|
|
695
|
+
fields: info.settings, values: settingsOf(card), name: info.name, anchor: (shown === card ? grip : card).getBoundingClientRect(),
|
|
696
|
+
save: (values) => client.run(async (send) => {
|
|
697
|
+
const answer = await send({ op: "settings", card: card.dataset.cardId, values, base: card.dataset.geBase });
|
|
698
|
+
if (!answer?.ok) return failed(answer);
|
|
699
|
+
putInto(partNodes(card, ["settings"]).settings, answer.parts.settings.now);
|
|
700
|
+
took(answer);
|
|
701
|
+
if (answer.parts.settings.was !== answer.parts.settings.now) history.push({ kind: "edit", id: card.dataset.cardId, parts: answer.parts });
|
|
702
|
+
}),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ---------- the library ----------
|
|
707
|
+
// Opened under `button` (a page alone's 添加自定义块), else under `rect` (a workbench's: the place of its top
|
|
708
|
+
// bar's button, in this frame's coordinates), else in the middle of the window. Gives whether it opened;
|
|
709
|
+
// asked while it stands, it closes. In a workbench a press elsewhere in it (this frame losing the focus)
|
|
710
|
+
// closes it, as a press on the page does.
|
|
711
|
+
let library = false;
|
|
712
|
+
async function openLibrary(board, { button = null, rect = null } = {}) {
|
|
713
|
+
if (library) {
|
|
714
|
+
menu.close();
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
menu.close();
|
|
718
|
+
let listed = [];
|
|
719
|
+
try {
|
|
720
|
+
listed = (await (await fetch(`/__gallery/pages/${slug}/library?language=${language}`)).json()).blocks ?? [];
|
|
721
|
+
} catch {
|
|
722
|
+
say(t("连不上本地服务"), "error");
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
const rows = (text = "") => listed.filter((block) => {
|
|
726
|
+
const words = `${said(block.name)} ${said(block.description)} ${block.id}`.toLowerCase();
|
|
727
|
+
return !text.trim() || words.includes(text.trim().toLowerCase());
|
|
728
|
+
}).map((block) => ({
|
|
729
|
+
label: said(block.name) || block.id, icon: BLOCK, hint: block.installed ? `${block.size.w} × ${block.size.h}` : t("自带"), ...(block.of ? { of: block.of } : {}),
|
|
730
|
+
...(block.preview || block.description ? { preview: block.preview ? { src: block.preview, says: said(block.description) } : { says: said(block.description) } } : {}),
|
|
731
|
+
run: () => addBlock(board, block),
|
|
732
|
+
}));
|
|
733
|
+
button?.setAttribute("data-pressed", "");
|
|
734
|
+
const closed = () => menu.close();
|
|
735
|
+
if (!button) addEventListener("blur", closed);
|
|
736
|
+
library = true;
|
|
737
|
+
menu.open(rows(), button ? button.getBoundingClientRect() : rect ?? { left: innerWidth / 2, right: innerWidth / 2, top: 0, bottom: 0, width: 0, height: 0 }, {
|
|
738
|
+
label: t("添加自定义块"), align: "end", empty: t("没有能放在页面上的块"),
|
|
739
|
+
...(button || rect ? {} : { stand: ({ width, height }) => ({ left: Math.max(8, (innerWidth - width) / 2), top: Math.max(8, (innerHeight - height) / 3) }) }),
|
|
740
|
+
onClose: () => {
|
|
741
|
+
library = false;
|
|
742
|
+
button?.removeAttribute("data-pressed");
|
|
743
|
+
removeEventListener("blur", closed);
|
|
744
|
+
},
|
|
745
|
+
search: { placeholder: t("搜索块…"), filter: rows, empty: t("没有找到") },
|
|
746
|
+
});
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
// The workbench's 添加自定义块 (the session's add-card command: `anchor` the place of its button in this
|
|
750
|
+
// frame's coordinates, none for the middle of the window): the library of the page's FIRST board;
|
|
751
|
+
// asked while it stands, it closes.
|
|
752
|
+
async function addCard({ anchor = null } = {}) {
|
|
753
|
+
const board = boards()[0];
|
|
754
|
+
if (!board) return { ok: false, reason: "board" };
|
|
755
|
+
window.focus();
|
|
756
|
+
const rect = anchor && [anchor.left, anchor.right, anchor.top, anchor.bottom].every(Number.isFinite)
|
|
757
|
+
? { left: anchor.left, right: anchor.right, top: Math.max(0, anchor.top), bottom: Math.max(0, anchor.bottom), width: anchor.right - anchor.left, height: Math.max(0, anchor.bottom - Math.max(0, anchor.top)) } : null;
|
|
758
|
+
const closing = library;
|
|
759
|
+
const opened = await openLibrary(board, { rect });
|
|
760
|
+
return { ok: true, opened: !closing && opened };
|
|
761
|
+
}
|
|
762
|
+
const addBlock = (board, block) => client.run(async (send) => {
|
|
763
|
+
const answer = await send({ op: "add", block: block.id, board: board.dataset.boardId });
|
|
764
|
+
if (!answer?.ok) {
|
|
765
|
+
if (answer?.reason === "places" || answer?.reason === "disabled" || answer?.reason === "missing") return say(t("这个块不能放在页面上"), "error");
|
|
766
|
+
return failed(answer);
|
|
767
|
+
}
|
|
768
|
+
history.push({ kind: "add", id: answer.card, board: board.dataset.boardId, html: answer.html });
|
|
769
|
+
if (answer.measure) history.toMeasure(answer.card);
|
|
770
|
+
took(answer, { own: [answer.card] });
|
|
771
|
+
// The page shown again, with the block's script and style (the server puts them in), the step kept.
|
|
772
|
+
wantSwap();
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// ---------- 恢复默认卡片 ----------
|
|
776
|
+
// The home page's default cards it misses put back (the service's restore-defaults), a task as every
|
|
777
|
+
// op is: one step of undo for all of them (a group of adds), the page shown again with their blocks'
|
|
778
|
+
// scripts and styles. Gives the service's answer ({ ok, added: [{ card, block, name }], skipped }),
|
|
779
|
+
// which the workbench says, or its refusal.
|
|
780
|
+
const restoreDefaults = () => client.run(async (send) => {
|
|
781
|
+
const answer = await send({ op: "restore-defaults" });
|
|
782
|
+
if (!answer?.ok) return answer ?? { ok: false };
|
|
783
|
+
const added = answer.added ?? [];
|
|
784
|
+
if (added.length > 0) {
|
|
785
|
+
const boardOf = (id) => answer.cards.find((card) => card.id === id)?.board ?? null;
|
|
786
|
+
history.push({ kind: "group", steps: added.map(({ card, html }) => ({ kind: "add", id: card, board: boardOf(card), html })) });
|
|
787
|
+
took(answer, { own: added.map(({ card }) => card) });
|
|
788
|
+
wantSwap();
|
|
789
|
+
}
|
|
790
|
+
return { ok: true, added: added.map(({ card, block, name }) => ({ card, block, name })), skipped: answer.skipped ?? [] };
|
|
791
|
+
});
|
|
792
|
+
// The workbench's 撤销 of that: the step undone only while it is the last one, the cards it names.
|
|
793
|
+
const undoRestore = (cards) => client.run(async (send) => {
|
|
794
|
+
const step = history.nextUndo();
|
|
795
|
+
const ids = step?.kind === "group" ? step.steps.map((one) => one.id) : [];
|
|
796
|
+
if (!Array.isArray(cards) || ids.length !== cards.length || ids.some((id, index) => id !== cards[index])) return { ok: false, reason: "moved-on" };
|
|
797
|
+
history.takeUndo();
|
|
798
|
+
if (!(await perform(step, "undo", send))) return { ok: false, reason: "refused" };
|
|
799
|
+
history.undone(step);
|
|
800
|
+
return { ok: true };
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// ---------- undo and redo ----------
|
|
804
|
+
// A step taken back or done again, inside the task of the undo or the redo (`send`): a group's each
|
|
805
|
+
// in turn (undone from its last), whether any of them went.
|
|
806
|
+
async function perform(step, way, send) {
|
|
807
|
+
if (step.kind === "checked") {
|
|
808
|
+
const answer = await todos.write({ slug: step.slug, ref: step.ref, done: way === "undo" ? !step.done : step.done, step: true });
|
|
809
|
+
if (!answer.ok) {
|
|
810
|
+
say(answer.reason === "offline" ? t("连不上本地服务") : answer.reason === "changed" || answer.reason === "ambiguous" ? t("这一项待办刚被改过,没能撤销") : t("没能改动这一项待办"), "error");
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (answer.block) step.ref = { ...step.ref, block: answer.block };
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
if (step.kind === "group") {
|
|
817
|
+
let any = false;
|
|
818
|
+
for (const one of way === "undo" ? [...step.steps].reverse() : step.steps) any = (await perform(one, way, send)) || any;
|
|
819
|
+
return any;
|
|
820
|
+
}
|
|
821
|
+
if (step.kind === "cells") {
|
|
822
|
+
const cards = step.cards.filter(({ id }) => cardById(id));
|
|
823
|
+
if (cards.length === 0) return false;
|
|
824
|
+
const to = way === "undo" ? "before" : "after";
|
|
825
|
+
for (const one of cards) setCells(cardById(one.id), one[to]);
|
|
826
|
+
const answer = await send({ op: "arrange", cards: cards.map((one) => ({ card: one.id, ...one[to], base: cardById(one.id).dataset.geBase })) });
|
|
827
|
+
if (!answer?.ok) return failed(answer) ?? false;
|
|
828
|
+
took(answer);
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
if (step.kind === "edit") {
|
|
832
|
+
const card = cardById(step.id);
|
|
833
|
+
if (!card) return false;
|
|
834
|
+
// The service's parts are as the page shows them (its own files where the server serves them), put on the page as they are.
|
|
835
|
+
const parts = Object.fromEntries(Object.entries(step.parts).map(([name, { was, now }]) => [name, way === "undo" ? was : now]));
|
|
836
|
+
return Boolean(await writeParts(card, parts, { send }));
|
|
837
|
+
}
|
|
838
|
+
const out = (step.kind === "remove") === (way === "redo");
|
|
839
|
+
if (out) {
|
|
840
|
+
const card = cardById(step.id);
|
|
841
|
+
return card ? removeCard(card, { step: false, send }) : false;
|
|
842
|
+
}
|
|
843
|
+
return putBack(step, send);
|
|
844
|
+
}
|
|
845
|
+
// A task each: the step taken off once the tasks before it are done, so that a Ctrl+Z pressed before a
|
|
846
|
+
// drop or a write is answered takes back that one, and two pressed at once go one after the other. The
|
|
847
|
+
// checks of to-dos a block asked are such writes, though they are no tasks of the client (their own line,
|
|
848
|
+
// todos): their step is pushed as each is answered, so the task waits for those asked before it.
|
|
849
|
+
const undo = () => client.run(async (send) => {
|
|
850
|
+
await todos.idle();
|
|
851
|
+
const step = history.takeUndo();
|
|
852
|
+
if (step && (await perform(step, "undo", send))) history.undone(step);
|
|
853
|
+
});
|
|
854
|
+
const redo = () => client.run(async (send) => {
|
|
855
|
+
await todos.idle();
|
|
856
|
+
const step = history.takeRedo();
|
|
857
|
+
if (step && (await perform(step, "redo", send))) history.redone(step);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// ---------- keys ----------
|
|
861
|
+
addEventListener("keydown", (event) => {
|
|
862
|
+
if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
|
|
863
|
+
if (gesture && event.key === "Escape") {
|
|
864
|
+
event.preventDefault();
|
|
865
|
+
event.stopPropagation();
|
|
866
|
+
const held = [...press.keys()];
|
|
867
|
+
press.clear();
|
|
868
|
+
grip.removeAttribute("data-pressed");
|
|
869
|
+
corner.removeAttribute("data-pressed");
|
|
870
|
+
for (const pointer of held) {
|
|
871
|
+
try {
|
|
872
|
+
grip.releasePointerCapture(pointer);
|
|
873
|
+
corner.releasePointerCapture(pointer);
|
|
874
|
+
} catch {}
|
|
875
|
+
}
|
|
876
|
+
finish(false);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const inField = event.target instanceof Element && event.target.closest(FIELDS);
|
|
880
|
+
if (menu.isOpen && !inField) {
|
|
881
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") menu.move(event.key === "ArrowDown" ? 1 : -1);
|
|
882
|
+
else if (event.key === "Enter") menu.choose();
|
|
883
|
+
else if (event.key === "Escape") menu.close();
|
|
884
|
+
else return;
|
|
885
|
+
event.preventDefault();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const key = event.key.toLowerCase();
|
|
889
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey || inField || formOpen()) return;
|
|
890
|
+
if (key === "z" && !event.shiftKey) undo();
|
|
891
|
+
else if ((key === "z" && event.shiftKey) || key === "y") redo();
|
|
892
|
+
else return;
|
|
893
|
+
event.preventDefault();
|
|
894
|
+
}, true);
|
|
895
|
+
|
|
896
|
+
// ---------- a block's own writes ----------
|
|
897
|
+
// A field of a card typed in: the page is not shown again under it until it is left (plan 4.5).
|
|
898
|
+
addEventListener("input", (event) => {
|
|
899
|
+
const field = event.target instanceof Element ? event.target : null;
|
|
900
|
+
if (field?.closest(`${BOARD} > ${CARD}`) && field.matches("input, textarea, select")) typing = field;
|
|
901
|
+
}, true);
|
|
902
|
+
addEventListener("focusout", (event) => {
|
|
903
|
+
if (event.target === typing) {
|
|
904
|
+
typing = null;
|
|
905
|
+
trySwap();
|
|
906
|
+
}
|
|
907
|
+
}, true);
|
|
908
|
+
// context.edit (the kit's doc-component-edit, cancelled to say it is taken): the block's change run on
|
|
909
|
+
// a draft of each part it names, which goes onto the page when it returns; a change that wrote the part
|
|
910
|
+
// on the page instead is put back and refused, as the document's editor refuses it.
|
|
911
|
+
document.addEventListener("doc-component-edit", (event) => {
|
|
912
|
+
const host = event.target instanceof Element ? event.target : null;
|
|
913
|
+
const card = host?.parentElement?.closest(`${BOARD} > ${CARD}`);
|
|
914
|
+
if (!card || instanceOf(card) !== host || !runs(card)) return;
|
|
915
|
+
const { parts, run } = event.detail ?? {};
|
|
916
|
+
if (!Array.isArray(parts) || typeof run !== "function") return;
|
|
917
|
+
const read = (node) => (node.localName === "script" ? node.textContent : node.innerHTML);
|
|
918
|
+
const before = parts.map(({ part }) => read(part));
|
|
919
|
+
const drafts = parts.map(({ part }) => part.cloneNode(true));
|
|
920
|
+
const ran = run(Object.fromEntries(parts.map(({ name }, index) => [name, drafts[index]])));
|
|
921
|
+
const live = parts.findIndex(({ part }, index) => read(part) !== before[index]);
|
|
922
|
+
if (live >= 0) {
|
|
923
|
+
parts.forEach(({ part }, index) => { if (read(part) !== before[index]) putInto(part, before[index]); });
|
|
924
|
+
console.error(`[DocKit] ${packageOf(host)}: context.edit: it wrote the part ${parts[live].name} on the page: a change writes the draft it is handed, which goes onto the page when it returns`);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (!ran) return;
|
|
928
|
+
event.preventDefault();
|
|
929
|
+
if (typing && card.contains(typing)) typing = null;
|
|
930
|
+
const changed = parts.map(({ name }, index) => [name, read(drafts[index])]).filter(([, html], index) => html !== before[index]);
|
|
931
|
+
if (changed.length === 0) return;
|
|
932
|
+
writeParts(card, Object.fromEntries(changed), { step: true });
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// ---------- as the page starts ----------
|
|
936
|
+
// The steps of the page before this one (its swap), but those of a card someone else changed since.
|
|
937
|
+
history.forgetChanged(basesNow());
|
|
938
|
+
// A card just added whose height is its content's: as high as that once drawn (the add's own step).
|
|
939
|
+
const measuring = history.takeMeasure();
|
|
940
|
+
if (measuring) {
|
|
941
|
+
const card = cardById(measuring);
|
|
942
|
+
const fit = () => {
|
|
943
|
+
if (!card?.isConnected || !runs(card)) return;
|
|
944
|
+
const inner = instanceOf(card);
|
|
945
|
+
const style = getComputedStyle(card);
|
|
946
|
+
const sides = ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth"].reduce((sum, side) => sum + (parseFloat(style[side]) || 0), 0);
|
|
947
|
+
const { cell } = geometry(card.parentElement);
|
|
948
|
+
const cells = cellsOf(card);
|
|
949
|
+
const h = fitHeight((inner?.getBoundingClientRect().height ?? 0) + sides, cell);
|
|
950
|
+
if (h !== cells.h && !narrow(card.parentElement)) placeCard(card, { ...cells, h }, { kind: "measure" });
|
|
951
|
+
};
|
|
952
|
+
const drawnAlready = () => document.documentElement.hasAttribute("data-doc-drawn");
|
|
953
|
+
if (drawnAlready()) requestAnimationFrame(fit);
|
|
954
|
+
else {
|
|
955
|
+
const watch = new MutationObserver(() => {
|
|
956
|
+
if (!drawnAlready()) return;
|
|
957
|
+
watch.disconnect();
|
|
958
|
+
requestAnimationFrame(fit);
|
|
959
|
+
});
|
|
960
|
+
watch.observe(document.documentElement, { attributes: true, attributeFilter: ["data-doc-drawn"] });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// The home page's first-open note, while its map says so, and its note of new default cards (put in as
|
|
964
|
+
// 恢复默认卡片 puts them: restoreDefaults).
|
|
965
|
+
if (map.hint) showHint(map, { hosted });
|
|
966
|
+
if (map.news) showNews(map, { restore: restoreDefaults });
|
|
967
|
+
// Its blocks may write from now on (the kit's context.canEdit, editable).
|
|
968
|
+
dispatchEvent(new Event("doc-editable"));
|
|
969
|
+
return { swap: wantSwap, undo, redo, idle: () => Promise.all([client.idle(), todos.idle()]), restoreDefaults, undoRestore, addCard };
|
|
970
|
+
}
|