@heycys/gallery 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/THIRD-PARTY-NOTICES.md +51 -1
  2. package/browser/editor/components/callout/index.mjs +40 -15
  3. package/browser/editor/components/columns/page.mjs +41 -28
  4. package/browser/editor/components/details/index.mjs +2 -1
  5. package/browser/editor/components/dialog/index.mjs +2 -1
  6. package/browser/editor/components/dialog/page.mjs +0 -18
  7. package/browser/editor/components/divider/index.mjs +1 -1
  8. package/browser/editor/components/figure/page.mjs +10 -1
  9. package/browser/editor/components/file/page.mjs +2 -2
  10. package/browser/editor/components/flowchart/add.mjs +2 -2
  11. package/browser/editor/components/heading/index.mjs +2 -0
  12. package/browser/editor/components/heading/page.mjs +6 -0
  13. package/browser/editor/components/highlight/index.mjs +4 -20
  14. package/browser/editor/components/icons.mjs +5 -2
  15. package/browser/editor/components/image/page.mjs +2 -2
  16. package/browser/editor/components/installed/index.mjs +71 -25
  17. package/browser/editor/components/installed/page.mjs +122 -21
  18. package/browser/editor/components/kit.mjs +3 -0
  19. package/browser/editor/components/media/page.mjs +44 -14
  20. package/browser/editor/components/pages.mjs +2 -2
  21. package/browser/editor/components/registry.mjs +56 -30
  22. package/browser/editor/components/section/index.mjs +2 -1
  23. package/browser/editor/components/set-apart.mjs +24 -0
  24. package/browser/editor/components/table/index.mjs +5 -3
  25. package/browser/editor/components/table/page.mjs +29 -18
  26. package/browser/editor/page/annotate/anchors.mjs +4 -3
  27. package/browser/editor/page/annotate/index.mjs +61 -34
  28. package/browser/editor/page/autosave.mjs +50 -4
  29. package/browser/editor/page/beside.mjs +5 -5
  30. package/browser/editor/page/block-menu/colors.mjs +3 -1
  31. package/browser/editor/page/block-menu/index.mjs +8 -6
  32. package/browser/editor/page/blocks.mjs +16 -7
  33. package/browser/editor/page/caret.mjs +4 -1
  34. package/browser/editor/page/changes/bar.mjs +102 -0
  35. package/browser/editor/page/changes/ghost.mjs +206 -0
  36. package/browser/editor/page/changes/gone-words.mjs +94 -0
  37. package/browser/editor/page/changes/index.mjs +447 -0
  38. package/browser/editor/page/changes/words.mjs +27 -0
  39. package/browser/editor/page/clipboard/import/colors.mjs +3 -3
  40. package/browser/editor/page/clipboard/index.mjs +32 -13
  41. package/browser/editor/page/clipboard/own/blocks.mjs +14 -5
  42. package/browser/editor/page/clipboard/own/describe.mjs +16 -7
  43. package/browser/editor/page/clipboard/own/export.mjs +6 -6
  44. package/browser/editor/page/clipboard/own/selection.mjs +2 -2
  45. package/browser/editor/page/clipboard/own/writers.mjs +0 -11
  46. package/browser/editor/page/columns/index.mjs +1 -1
  47. package/browser/editor/page/commit.mjs +19 -4
  48. package/browser/editor/page/conflicts.mjs +6 -26
  49. package/browser/editor/page/convert.mjs +8 -7
  50. package/browser/editor/page/core.mjs +55 -17
  51. package/browser/editor/page/drag/index.mjs +40 -24
  52. package/browser/editor/page/drag/preview.mjs +9 -4
  53. package/browser/editor/page/editor.mjs +70 -15
  54. package/browser/editor/page/emoji-picker/emoji-tab.mjs +22 -13
  55. package/browser/editor/page/emoji-picker/grid.mjs +7 -5
  56. package/browser/editor/page/emoji-picker/icon-tab.mjs +4 -4
  57. package/browser/editor/page/emoji-picker/icons.mjs +0 -1
  58. package/browser/editor/page/emoji-picker/index.mjs +11 -9
  59. package/browser/editor/page/emoji-picker/styles.mjs +161 -2
  60. package/browser/editor/page/emoji-picker/support.mjs +6 -4
  61. package/browser/editor/page/emoji-picker/upload.mjs +12 -11
  62. package/browser/editor/page/fills.mjs +5 -3
  63. package/browser/editor/page/fold/index.mjs +348 -0
  64. package/browser/editor/page/fold/rule.mjs +125 -0
  65. package/browser/editor/page/folded.mjs +23 -0
  66. package/browser/editor/page/format-bar/bar.mjs +2 -1
  67. package/browser/editor/page/format-bar/index.mjs +1 -1
  68. package/browser/editor/page/geometry.mjs +8 -6
  69. package/browser/editor/page/handle/index.mjs +17 -6
  70. package/browser/editor/page/hints/index.mjs +1 -1
  71. package/browser/editor/page/insert/index.mjs +30 -17
  72. package/browser/editor/page/long-runs.mjs +75 -0
  73. package/browser/editor/page/look.mjs +15 -3
  74. package/browser/editor/page/margin.mjs +45 -24
  75. package/browser/editor/page/menu.mjs +69 -25
  76. package/browser/editor/page/merge.mjs +24 -0
  77. package/browser/editor/page/notices.mjs +1 -1
  78. package/browser/editor/page/offered.mjs +1 -1
  79. package/browser/editor/page/outline/folds.mjs +11 -10
  80. package/browser/editor/page/outline/index.mjs +131 -45
  81. package/browser/editor/page/outline/list.mjs +23 -11
  82. package/browser/editor/page/outline/read.mjs +1 -1
  83. package/browser/editor/page/outline/strip.mjs +7 -1
  84. package/browser/editor/page/panel.mjs +5 -4
  85. package/browser/editor/page/resize/index.mjs +1 -1
  86. package/browser/editor/page/selection/delete.mjs +8 -6
  87. package/browser/editor/page/selection/index.mjs +10 -3
  88. package/browser/editor/page/session.mjs +54 -3
  89. package/browser/editor/page/table/cell-bar.mjs +4 -1
  90. package/browser/editor/page/table/cells.mjs +1 -1
  91. package/browser/editor/page/table/icons.mjs +1 -1
  92. package/browser/editor/page/table/index.mjs +13 -8
  93. package/browser/editor/page/table/widths.mjs +9 -4
  94. package/browser/editor/page/text-select/index.mjs +18 -3
  95. package/browser/editor/page/tooltip.mjs +1 -1
  96. package/browser/editor/page/type-grid.mjs +4 -3
  97. package/browser/editor/page/typing/code-edges.mjs +63 -17
  98. package/browser/editor/page/typing/index.mjs +12 -6
  99. package/browser/editor/page/typing/leave.mjs +1 -1
  100. package/browser/shared/annotations.mjs +8 -10
  101. package/browser/shared/code-folds.mjs +5 -1
  102. package/browser/shared/colors.mjs +15 -13
  103. package/browser/shared/component-catalog.mjs +24 -10
  104. package/browser/shared/components/callout.mjs +4 -2
  105. package/browser/shared/components/index.mjs +61 -7
  106. package/browser/shared/components/installed.mjs +75 -5
  107. package/browser/shared/components/page.mjs +10 -1
  108. package/browser/shared/components/table.mjs +1 -1
  109. package/browser/shared/components/text.mjs +2 -2
  110. package/browser/shared/document-look.mjs +178 -0
  111. package/browser/shared/emoji/data/LICENSE-lucide.txt +43 -0
  112. package/browser/shared/emoji/data/README.md +4 -2
  113. package/browser/shared/emoji/data/categories.json +1 -1
  114. package/browser/shared/emoji/data/keywords-en.json +1 -1
  115. package/browser/shared/emoji/data/keywords-zh.json +1 -1
  116. package/browser/shared/emoji/data/names.json +1 -1
  117. package/browser/shared/emoji/data/positions.json +1 -1
  118. package/browser/shared/emoji/data/twemoji-index.json +1 -1
  119. package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
  120. package/browser/shared/emoji/data.mjs +10 -12
  121. package/browser/shared/emoji/emoji.mjs +8 -7
  122. package/browser/shared/emoji/icon-value.mjs +4 -3
  123. package/browser/shared/html-tree.mjs +11 -5
  124. package/browser/shared/i18n/en.mjs +67 -8
  125. package/browser/shared/long-runs.mjs +39 -0
  126. package/browser/shared/new-names.mjs +5 -2
  127. package/browser/shared/outline.mjs +194 -0
  128. package/browser/shared/preferences.mjs +10 -4
  129. package/browser/shared/session.mjs +5 -4
  130. package/browser/shared/workspace-look.mjs +91 -0
  131. package/browser/shared/zoom.mjs +5 -5
  132. package/dist/chunk-3HIRPVLU.mjs +2 -0
  133. package/dist/chunk-4FS7Z7WR.mjs +2 -0
  134. package/dist/{chunk-LHTFLRB6.mjs → chunk-5MHMDLZD.mjs} +2 -2
  135. package/dist/{chunk-YXAUQPVK.mjs → chunk-5ZKJ3H4P.mjs} +3 -3
  136. package/dist/{chunk-ZBIM6ARY.mjs → chunk-64K7IPLO.mjs} +55 -16
  137. package/dist/{chunk-QYKCJ7FG.mjs → chunk-6QXGZM36.mjs} +1 -1
  138. package/dist/chunk-6RGJOMJP.mjs +16 -0
  139. package/dist/{chunk-EUX7IC5J.mjs → chunk-7DTINW6U.mjs} +2 -2
  140. package/dist/{chunk-6CYG3Z4V.mjs → chunk-AJ73XJF3.mjs} +1 -1
  141. package/dist/chunk-AUZGSZJH.mjs +13 -0
  142. package/dist/{chunk-7QQBR7PB.mjs → chunk-BFE45AKL.mjs} +1 -1
  143. package/dist/chunk-BGGDRNYQ.mjs +10 -0
  144. package/dist/{chunk-L4DVONOG.mjs → chunk-BXO2ZY5P.mjs} +1 -1
  145. package/dist/chunk-CEH4MRCC.mjs +2 -0
  146. package/dist/chunk-CY2FNNAB.mjs +16 -0
  147. package/dist/{chunk-LC7YNQFG.mjs → chunk-DFUDV4LX.mjs} +1 -1
  148. package/dist/chunk-DKXG4XDG.mjs +2 -0
  149. package/dist/chunk-E7LWZRXY.mjs +4 -0
  150. package/dist/chunk-EZQ34M3F.mjs +124 -0
  151. package/dist/chunk-FDLXYYG4.mjs +4 -0
  152. package/dist/chunk-FUKQNOQS.mjs +9 -0
  153. package/dist/{chunk-DB4WKGYF.mjs → chunk-GB5SOYQJ.mjs} +2 -2
  154. package/dist/{chunk-ACOPDCNB.mjs → chunk-IVDLMMBD.mjs} +1 -1
  155. package/dist/chunk-JPOSLGOC.mjs +6 -0
  156. package/dist/chunk-JVHHHGEA.mjs +3 -0
  157. package/dist/chunk-L3QKH4OK.mjs +26 -0
  158. package/dist/{chunk-B3VTYBYR.mjs → chunk-LCTHTZIL.mjs} +2 -2
  159. package/dist/chunk-LEF6DRCK.mjs +59 -0
  160. package/dist/chunk-LS4F7JUM.mjs +2 -0
  161. package/dist/chunk-NO5NJCHO.mjs +2 -0
  162. package/dist/{chunk-UNUEWH4S.mjs → chunk-NQPVAU63.mjs} +1 -1
  163. package/dist/chunk-O35FIXRB.mjs +2 -0
  164. package/dist/chunk-PL6GHLGD.mjs +15 -0
  165. package/dist/{chunk-G7TOXONE.mjs → chunk-QJIR2BLP.mjs} +1 -1
  166. package/dist/chunk-QUPLVPB2.mjs +7 -0
  167. package/dist/chunk-R7BNENLY.mjs +4 -0
  168. package/dist/chunk-S6O4UHLC.mjs +2 -0
  169. package/dist/{chunk-HMHK3EQ6.mjs → chunk-SGJWQKEI.mjs} +1 -1
  170. package/dist/chunk-SYGN7GLU.mjs +2 -0
  171. package/dist/chunk-TYK6VNAB.mjs +10 -0
  172. package/dist/{chunk-EVJ3BX3M.mjs → chunk-U6KNPJPR.mjs} +1 -1
  173. package/dist/{chunk-YZ5Z6H4W.mjs → chunk-UALRNVSE.mjs} +1 -1
  174. package/dist/chunk-UMHUSOPJ.mjs +2 -0
  175. package/dist/chunk-V3HXVQEM.mjs +2 -0
  176. package/dist/{chunk-UXUK4PMC.mjs → chunk-V5JTW2LH.mjs} +1 -1
  177. package/dist/chunk-V5VXUJKG.mjs +2 -0
  178. package/dist/chunk-VBXMADMP.mjs +2 -0
  179. package/dist/chunk-VNV55RUA.mjs +2 -0
  180. package/dist/chunk-VPD2PS35.mjs +2 -0
  181. package/dist/chunk-W3BGBJQW.mjs +2 -0
  182. package/dist/chunk-W4VKN3GF.mjs +7 -0
  183. package/dist/chunk-W5BXXPDJ.mjs +3 -0
  184. package/dist/{chunk-Z52HTUW7.mjs → chunk-XNROSH2T.mjs} +2 -2
  185. package/dist/chunk-XUNHRCOG.mjs +6 -0
  186. package/dist/{chunk-6U2SLSDN.mjs → chunk-Y6RLRPG3.mjs} +1 -1
  187. package/dist/cli.mjs +1 -1
  188. package/dist/server.mjs +1 -1
  189. package/dist/worker.mjs +1 -1
  190. package/kit/math.js +18 -2
  191. package/kit/report.css +248 -137
  192. package/kit/runtime.js +126 -51
  193. package/kit/tables.js +51 -1
  194. package/kit/ui.js +97 -0
  195. package/package.json +1 -1
  196. package/release.json +8 -8
  197. package/sdk/doc-kit.d.ts +9 -3
  198. package/skill/SKILL.md +22 -10
  199. package/skill/references/building-components.md +46 -6
  200. package/skill/references/example-report.html +1 -1
  201. package/skill/references/writing-html.md +4 -4
  202. package/skill/sdk/doc-kit.d.ts +9 -3
  203. package/web/assets/index-Bh74DfsV.css +1 -0
  204. package/web/assets/index-UW0RyFFt.js +13 -0
  205. package/web/index.html +2 -2
  206. package/browser/editor/components/callout/page.mjs +0 -13
  207. package/dist/chunk-2PBALTR3.mjs +0 -57
  208. package/dist/chunk-3LA3EMDE.mjs +0 -15
  209. package/dist/chunk-5FRMCDPY.mjs +0 -16
  210. package/dist/chunk-5LEK2CBS.mjs +0 -2
  211. package/dist/chunk-5OFN5AHC.mjs +0 -2
  212. package/dist/chunk-5SIYS5HN.mjs +0 -2
  213. package/dist/chunk-5TEK3RAD.mjs +0 -2
  214. package/dist/chunk-7GHT2UCA.mjs +0 -15
  215. package/dist/chunk-ATCJYAR4.mjs +0 -2
  216. package/dist/chunk-C7HWOBIF.mjs +0 -2
  217. package/dist/chunk-E7VYDG2X.mjs +0 -2
  218. package/dist/chunk-FQGQH4KM.mjs +0 -2
  219. package/dist/chunk-GPF4YWWB.mjs +0 -9
  220. package/dist/chunk-GYQV7Y4J.mjs +0 -120
  221. package/dist/chunk-HQQMJCQJ.mjs +0 -10
  222. package/dist/chunk-IERUOGC3.mjs +0 -6
  223. package/dist/chunk-ITNLYXXV.mjs +0 -3
  224. package/dist/chunk-NFIBI4IP.mjs +0 -26
  225. package/dist/chunk-O52JMOCZ.mjs +0 -2
  226. package/dist/chunk-QMQUXTU4.mjs +0 -12
  227. package/dist/chunk-QVWVFC5K.mjs +0 -6
  228. package/dist/chunk-QZY6Q64C.mjs +0 -2
  229. package/dist/chunk-TDQP3DKQ.mjs +0 -10
  230. package/dist/chunk-V4CPWKMZ.mjs +0 -2
  231. package/dist/chunk-VACRB2NE.mjs +0 -3
  232. package/dist/chunk-W63L7367.mjs +0 -7
  233. package/dist/chunk-W7CXPCRV.mjs +0 -6
  234. package/dist/chunk-XINNG4F2.mjs +0 -4
  235. package/dist/chunk-YL2DPWVS.mjs +0 -4
  236. package/web/assets/index-B6Ai3wsa.js +0 -13
  237. package/web/assets/index-SYPC3EZy.css +0 -1
@@ -1,5 +1,5 @@
1
1
  import { escapeText } from "../../../shared/escape.mjs";
2
- import { ATTRIBUTE, FALLBACK, INSTANCE, PACKAGE_ID, instanceMatch, refusedInCopy } from "../../../shared/components/installed.mjs";
2
+ import { ATTRIBUTE, FALLBACK, INSTANCE, PACKAGE_ID, SLOT, SLOT_PART, componentAt, heldBy, instanceMatch, refusedInCopy, slotsIn } from "../../../shared/components/installed.mjs";
3
3
  import { wordsIn } from "../../page/clipboard/own/writers.mjs";
4
4
  import { svg } from "../icons.mjs";
5
5
  import { t } from "../../../shared/i18n/index.mjs";
@@ -7,12 +7,16 @@ import { t } from "../../../shared/i18n/index.mjs";
7
7
  // 组件: the one kind of block every component package is to the engines, the host of a package's
8
8
  // instances (src/shared/components/installed.mjs). It uses only what a definition offers any block:
9
9
  // the engines know no package and no part of one. An instance is one block, moved, copied and
10
- // deleted whole, no container, nothing going in or out; its parts edited as text are text blocks of
11
- // this kind (file.mjs finds them, as its manifest says), and everything else in it is the component's
12
- // (page.mjs). A press on its outer frame or on the handle is Gallery's, as for any block.
10
+ // deleted whole; its parts edited as text are text blocks of this kind (file.mjs finds them, as its
11
+ // manifest says), and everything else in its own region is the component's (page.mjs). A press on
12
+ // its outer frame or on the handle is Gallery's, as for any block. Its slots (data-doc-slot, the parts
13
+ // its manifest edits as "flow") are the one way in and out: boxes of kind "slot", keyed by the part's
14
+ // name, whose blocks are the document's, as a column's are (notion-lite's column: a block's own area
15
+ // of child blocks, the frame around it none of theirs).
13
16
  //
14
- // Where the engines reach it (src/editor/page/ unless named): ../source/ (the scan stops at one,
15
- // natural.mjs and old names pass it by: `isInstance`, src/shared/components/), hints/ (`readOnly`), clipboard/own/
17
+ // Where the engines reach it (src/editor/page/ unless named): ../source/ (the scan stops at one and
18
+ // goes on in its slots, natural.mjs and old names pass its own region by: `isInstance`,
19
+ // `instanceSlots`, src/shared/components/), hints/ (`readOnly`), clipboard/own/
16
20
  // (`clipboard`, copied as itself), caret.mjs (`sealed`: `editable`, where most of the page's events
17
21
  // start from), text-select/, editor.mjs's entries and the tools that go by a kit's class (`keepsKeys`,
18
22
  // page.mjs), formula/, the "/" menu, the format bar and clipboard/ (`formula`). The "/" menu and 在下方新建
@@ -23,21 +27,27 @@ import { t } from "../../../shared/i18n/index.mjs";
23
27
 
24
28
  // An instance as Markdown, the words of the file and none of what a component draws: as its package's
25
29
  // manifest says (`markdown`), each part in the file's order with `parts`, a table a table, one edited
26
- // as text a paragraph and any other its words and pictures, or with `text`, and where no package says,
27
- // its words as any whole block's.
30
+ // as text a paragraph, a slot its blocks as each is written anywhere (notion-lite's column, `flatten`)
31
+ // and any other its words and pictures, or with `text`, and where no package says, its words as any
32
+ // whole block's.
28
33
  function instanceParts(unit, how) {
29
34
  const found = how.packageOf(unit.getAttribute(ATTRIBUTE));
30
35
  if (found?.markdown !== "parts") return [how.words(unit)];
31
36
  return saidParts(unit, found.parts).flatMap(({ part, edit }) => {
37
+ if (edit === "flow") {
38
+ const lines = how.lines(how.unitsOf(part)); // as a column's, not in from the margin as a disclosure's
39
+ return lines.length > 0 ? [lines] : [];
40
+ }
32
41
  if (part.matches("table")) return [how.writtenAs("table", part)];
33
42
  return (edit === "text" ? [how.own(part)] : [how.wordsIn(part).join(" · "), ...how.picturesIn(part)]).map((line) => [line]);
34
43
  }).filter(([first, ...more]) => first || more.length > 0);
35
44
  }
36
45
 
37
- // The parts of an instance that say something, each once and none inside another, in the file's order.
46
+ // The parts of an instance that say something, each once and none inside another, in the file's order:
47
+ // of its own region, a slot only as a part edited as flow and no other part.
38
48
  function saidParts(unit, parts) {
39
49
  const said = Object.values(parts).flatMap(({ at, edit }) => [...unit.querySelectorAll(at)].map((part) => ({ part, edit })))
40
- .filter(({ part }) => !part.matches(`${INSTANCE}, script, style, template`) && part.parentElement.closest(INSTANCE) === unit);
50
+ .filter(({ part, edit }) => !part.matches(`${INSTANCE}, script, style, template`) && part.parentElement.closest(INSTANCE) === unit && heldBy(part) === unit && (edit === "flow") === part.matches(SLOT_PART));
41
51
  const found = [...new Map(said.map((each) => [each.part, each])).values()];
42
52
  return found.filter(({ part }) => !found.some((other) => other.part !== part && other.part.contains(part)))
43
53
  .sort((a, b) => (a.part.compareDocumentPosition(b.part) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
@@ -46,10 +56,13 @@ function saidParts(unit, parts) {
46
56
  // A copy of an instance as blocks, what other programs and a field of plain text read of it: as its
47
57
  // package says, as a page export writes it (instanceParts), but for pictures, which are no words. Its
48
58
  // words are paragraphs, as `markdown: "parts"` has them; a text part that is a heading says so for the
49
- // clipboard's HTML (`as`: the type it is written as there, the parts writer's).
50
- function copiedParts(copy, found, { serializeInner, clipboardOf, copiedType }) {
51
- if (found?.markdown !== "parts" || !found.parts) return [{ type: "paragraph", html: escapeText(wordsIn(copy).join(" · ")) }].filter(({ html }) => html);
59
+ // clipboard's HTML (`as`: the type it is written as there, the parts writer's). A slot is its blocks
60
+ // (`flows`, by name, as the clipboard describes them), the copy's slots being empty; its words are
61
+ // among the instance's (`words`, of it with what its slots hold).
62
+ function copiedParts(copy, found, { serializeInner, clipboardOf, copiedType }, { flows = {}, words = wordsIn(copy) } = {}) {
63
+ if (found?.markdown !== "parts" || !found.parts) return [{ type: "paragraph", html: escapeText(words.join(" · ")) }].filter(({ html }) => html);
52
64
  return saidParts(copy, found.parts).flatMap(({ part, edit }) => {
65
+ if (edit === "flow") return flows[part.getAttribute(SLOT)] ?? [];
53
66
  if (part.matches("table")) return [clipboardOf("table").data(part, { serializeInner })].filter(Boolean);
54
67
  const html = edit === "text" ? serializeInner(part) : escapeText(wordsIn(part).join(" · "));
55
68
  const own = edit === "text" ? copiedType(part) : null;
@@ -78,11 +91,12 @@ export const ICON = svg('<path d="M4 6.75A1.75 1.75 0 0 1 5.75 5H8a2 2 0 0 1 4 0
78
91
 
79
92
  export default {
80
93
  name: "installed",
81
- // Ahead of every rule of the page's: all that stands in an instance is the component's, its
82
- // controls too, and one of no package, or whose component failed, says why nothing there answers.
94
+ // Ahead of every rule of the page's: all that stands in an instance's own region is the component's,
95
+ // its controls too, and one of no package, or whose component failed, says why nothing there answers.
96
+ // What its slots hold is the page's, each block saying what it says anywhere (componentAt).
83
97
  readOnly: [
84
- { rank: 1, at: `[${FALLBACK}] *`, says: t("这个组件没有运行:内容只能看,可以整块移动、复制和删除。") },
85
- { rank: 2, at: `[${ATTRIBUTE}] *`, says: null },
98
+ { rank: 1, at: (element) => componentAt(element) && Boolean(heldBy(element)?.hasAttribute(FALLBACK)), says: t("这个组件没有运行:内容只能看,可以整块移动、复制和删除。") },
99
+ { rank: 2, at: componentAt, says: null },
86
100
  ],
87
101
  attributes: [ATTRIBUTE],
88
102
  sealed: INSTANCE,
@@ -96,6 +110,11 @@ export default {
96
110
  copy: "itself",
97
111
  },
98
112
  },
113
+ boxes: {
114
+ // A slot of an instance (data-doc-slot, its manifest's part edited as flow): the document's blocks, as a
115
+ // column's are, a disclosure's content's rules for an empty one (holdsOne, a plain div a group).
116
+ slot: { role: "flow", name: t("内容区"), groups: true, holdsOne: true, selectAll: { content: true }, find: (unit, key) => slotsIn(unit).find((slot) => slot.getAttribute(SLOT) === key) ?? null },
117
+ },
99
118
  hosts: {
100
119
  // A part a manifest edits as text: words as in a paragraph, emptied but never split, joined or
101
120
  // taken out on its own, as a media block's caption is. No formula: the kit draws none in an
@@ -108,24 +127,51 @@ export default {
108
127
  // no unit to be made from (`data-ge-copy`).
109
128
  installed: {
110
129
  tag: "div", kind: "installed", bare: true,
111
- extras: (element) => ({ package: element.getAttribute(ATTRIBUTE), ...(element.hasAttribute("data-ge-copy") ? { html: element.getAttribute("data-ge-copy") } : {}) }),
130
+ // Its markup is the file's, from its template or what was carried (`data-ge-copy`, its slots
131
+ // empty); what its slots hold goes apart, blocks made on the page as any (`children`, each in the
132
+ // slot `boxKeys` names), so that the commit's answer names each, as for a block made in a column.
133
+ extras(element, { nodes }) {
134
+ const held = slotsIn(element).filter((slot) => slot.hasAttribute("data-ge-box")).flatMap((slot) => (nodes?.(slot) ?? []).map((node) => [slot.getAttribute(SLOT), node]));
135
+ return {
136
+ package: element.getAttribute(ATTRIBUTE),
137
+ ...(element.hasAttribute("data-ge-copy") ? { html: element.getAttribute("data-ge-copy") } : {}),
138
+ ...(slotsIn(element).length > 0 ? { children: held.map(([, node]) => node), boxKeys: held.map(([key]) => key) } : {}),
139
+ };
140
+ },
112
141
  clipboard: {
113
142
  kind: "parts", read: instanceParts,
114
- // What carries it where its unit is not (cut, another document): its markup as the kit would
115
- // start it, with the ids it names itself, and what it says to whoever reads no blocks. One
116
- // whose markup no clipboard may bring anywhere (a handler, a script) has none, and stays a
117
- // copy within its document.
118
- data(node, { serializeInner, copyOf, clipboardOf, copiedType, page }) {
143
+ // What carries it where its unit is not (cut, another document), and within its document too
144
+ // when it has slots: its markup as the kit would start it, with the ids it names itself, its
145
+ // slots empty, the blocks each holds described as any (`flows`, by name: blocks made on the page
146
+ // where it is pasted, as a disclosure's are), and what it says to whoever reads no blocks. One
147
+ // whose markup no clipboard may bring anywhere (a handler, a script) has none, and stays a copy
148
+ // within its document; so does one with a slot that is no box (words straight in it), or with a
149
+ // block in one the page cannot make, which then is no copy at all (clipboard/own/describe.mjs).
150
+ data(node, { serializeInner, copyOf, clipboardOf, copiedType, page, holding }) {
151
+ const slots = slotsIn(node);
152
+ const flows = Object.fromEntries(slots.map((slot) => [slot.getAttribute(SLOT), slot.hasAttribute("data-ge-box") ? holding?.(slot) ?? null : null]));
153
+ if (Object.values(flows).some((blocks) => !blocks)) return null;
119
154
  const copy = copyOf(node, { named: true });
155
+ const words = wordsIn(copy);
156
+ slotsIn(copy).forEach((slot) => slot.replaceChildren());
120
157
  const html = copy.outerHTML;
121
158
  if (html.length > CARRIED || refused(copy) || !elementOf(html, node.ownerDocument)) return null;
122
159
  const id = node.getAttribute(ATTRIBUTE);
123
- return { type: "installed", package: id, html, blocks: copiedParts(copy, page("installed")?.packageOf(id), { serializeInner, clipboardOf, copiedType }) };
160
+ return { type: "installed", package: id, html, ...(slots.length > 0 ? { flows } : {}), blocks: copiedParts(copy, page("installed")?.packageOf(id), { serializeInner, clipboardOf, copiedType }, { flows, words }) };
124
161
  },
162
+ // What its slots hold, among a paste's blocks (blocks.mjs `holds`).
163
+ holds: (block) => (block.flows && typeof block.flows === "object" ? Object.values(block.flows).filter(Array.isArray).flat() : []),
164
+ // Its slots empty in its markup, each named once in `flows`, which holds blocks a slot takes.
125
165
  check(block, { string, valid }) {
126
166
  if (!string(block.package, 200) || !PACKAGE_ID.test(block.package) || !string(block.html, CARRIED) || !Array.isArray(block.blocks) || !block.blocks.every((inner) => valid(inner))) return false;
127
167
  const root = elementOf(block.html);
128
- return Boolean(root?.matches(instanceMatch(block.package))) && !refused(root);
168
+ if (!root?.matches(instanceMatch(block.package)) || refused(root)) return false;
169
+ const slots = slotsIn(root);
170
+ if (slots.length === 0) return block.flows === undefined;
171
+ const names = slots.map((slot) => slot.getAttribute(SLOT));
172
+ const { flows } = block;
173
+ return Boolean(flows) && typeof flows === "object" && !Array.isArray(flows) && new Set(names).size === names.length && names.length === Object.keys(flows).length
174
+ && slots.every((slot) => !slot.firstElementChild && !slot.textContent.trim() && Array.isArray(flows[slot.getAttribute(SLOT)]) && flows[slot.getAttribute(SLOT)].every((inner) => valid(inner, "slot")));
129
175
  },
130
176
  },
131
177
  },
@@ -1,10 +1,14 @@
1
- import { ARRIVING, ATTRIBUTE, DATA_MOST, DATA_SHOWN, FALLBACK, FIELDS, INSTANCE, NO_TEXT, PACKAGE_MARK, SCHEMA, fitsDataScript, instanceMatch, isDataScript, noDataPart, refusedInData, schemaAhead } from "../../../shared/components/installed.mjs";
1
+ import { ARRIVING, ATTRIBUTE, DATA_MOST, DATA_SHOWN, FALLBACK, FIELDS, INSTANCE, NO_TEXT, PACKAGE_MARK, SCHEMA, SLOT, SLOT_PART, componentAt, fitsDataScript, heldBy, instanceMatch, isDataScript, noDataPart, refusedInData, schemaAhead, slotsIn } from "../../../shared/components/installed.mjs";
2
2
  import { referencedIds } from "../../../shared/copy-ids.mjs";
3
- import { language } from "../../../shared/i18n/index.mjs";
3
+ import { language, t } from "../../../shared/i18n/index.mjs";
4
4
  import { caretAt, editable } from "../../page/caret.mjs";
5
+ import { htmlToBlocks, parseClipboardHTML } from "../../page/clipboard/import/html.mjs";
6
+ import { toBlocks } from "../../page/clipboard/import/to-blocks.mjs";
7
+ import { build as buildBlock } from "../../page/clipboard/own/blocks.mjs";
5
8
  import { copyOf } from "../../page/clipboard/own/describe.mjs";
6
9
  import { ownIds } from "../../page/clipboard/own/ids.mjs";
7
- import { unitElement } from "../../page/core.mjs";
10
+ import { firstHost, newHost, unitElement } from "../../page/core.mjs";
11
+ import { addStyle } from "../../page/ui.mjs";
8
12
  import { accepts, capsFor } from "../registry.mjs";
9
13
  import { ICON } from "./index.mjs";
10
14
  import { followCatalog, followSettings, isBehind } from "./served.mjs";
@@ -38,6 +42,18 @@ import { followCatalog, followSettings, isBehind } from "./served.mjs";
38
42
  // the reader writes over someone else's change of a part (仍然调整) is a step of its own from theirs
39
43
  // (`overwrite`), so that one undo gives the file's back.
40
44
  // One past its manifest's `schema` is read-only, its words and all.
45
+ //
46
+ // Its slots (data-doc-slot, the parts its manifest edits as flow) are the page's: boxes of kind "slot"
47
+ // (index.mjs), whose blocks are edited, moved and undone as anywhere, a press or a key in one no
48
+ // component's (componentAt), whatever state the instance is in, one of no package too. Enter in a text
49
+ // part goes into the first slot after it, as in a disclosure's title. An empty one says so while the
50
+ // page is edited, and a click on it makes a paragraph (insert/, holdsOne), as a disclosure's content.
51
+ // The component reads a slot (context.part) and hears what changes in it (source), and writes nothing
52
+ // there: a slot it takes off its instance is put back where it stood (`keepSlots`), so that no commit
53
+ // takes the reader's blocks out. An instance made on the page, from "/", a clipboard or 恢复我的, has its
54
+ // slots as boxes made on the page, their blocks made on the page as any (`slotsMade`), which the commit
55
+ // carries apart from its markup (index.mjs `extras`) and the answer names, so that they edit at once; a
56
+ // copy carries them so too, in its document as well (`copiesAsItself`, index.mjs `data`).
41
57
 
42
58
  let held = null; // the map, what makes a block with no editor (the transfer worker) finds none of
43
59
 
@@ -119,9 +135,10 @@ function bring({ id, stamp, definition, kit = [], tags }) {
119
135
  const partsOf = (instance) => Object.entries(globalThis.DocKit?.description(instance.getAttribute(ATTRIBUTE))?.parts ?? {});
120
136
  // Whether an instance holds its content in a shape its manifest does not reach, as the kit was told it.
121
137
  const ahead = (instance) => schemaAhead(instance.getAttribute(SCHEMA), globalThis.DocKit?.description(instance.getAttribute(ATTRIBUTE))?.schema);
122
- // The elements of a part an instance holds, none of an instance in it nor of what the kit draws of a
123
- // script of data there, which is no element of the file's.
124
- const elementsOf = (instance, at) => [...instance.querySelectorAll(at)].filter((part) => !part.matches(INSTANCE) && part.parentElement.closest(INSTANCE) === instance && !part.closest(`.${DATA_SHOWN}`));
138
+ // The elements of a part an instance holds, of its own region (none of an instance in it, none of what
139
+ // its slots hold, no slot itself) and none of what the kit draws of a script of data there, which is
140
+ // no element of the file's.
141
+ const elementsOf = (instance, at) => [...instance.querySelectorAll(at)].filter((part) => !part.matches(INSTANCE) && !part.matches(SLOT_PART) && part.parentElement.closest(INSTANCE) === instance && heldBy(part) === instance && !part.closest(`.${DATA_SHOWN}`));
125
142
 
126
143
  // The elements of an instance its component writes, [[name, element]], as the server finds them
127
144
  // (file.mjs dataParts): of each part edited as data the first it holds, as context.part does. One in
@@ -170,7 +187,7 @@ const openness = (instance) => (instance.hasAttribute(FALLBACK) ? "false" : "tru
170
187
  function followFallback(instance) {
171
188
  const value = openness(instance);
172
189
  for (const part of instance.querySelectorAll('[data-ge-kind="installed"]')) {
173
- if (part.parentElement.closest(INSTANCE) === instance && part.getAttribute("contenteditable") !== value) part.setAttribute("contenteditable", value);
190
+ if (part.parentElement.closest(INSTANCE) === instance && heldBy(part) === instance && part.getAttribute("contenteditable") !== value) part.setAttribute("contenteditable", value);
174
191
  }
175
192
  }
176
193
 
@@ -196,12 +213,40 @@ function newInstance(doc, html) {
196
213
  return instance;
197
214
  }
198
215
 
199
- /** An instance from what a clipboard carried (`data`): the file is written from that markup, which the commit sends. */
200
- export function build({ doc, block }) {
216
+ /**
217
+ * The slots of a new instance as boxes made on the page, each holding what `fill(slot)` gives (blocks
218
+ * made on the page), or an empty paragraph, where the reader writes: what the commit writes in them
219
+ * goes apart from the instance's markup (index.mjs `extras`), and the page knows each block of it by
220
+ * the commit's answer, as one made in a new column.
221
+ */
222
+ function slotsMade(doc, instance, fill) {
223
+ for (const slot of slotsIn(instance)) {
224
+ const blocks = fill(slot);
225
+ slot.setAttribute("data-ge-box", "");
226
+ slot.setAttribute("data-ge-box-kind", "slot");
227
+ slot.replaceChildren(...(blocks.length > 0 ? blocks : [newHost(doc, "paragraph")]));
228
+ }
229
+ }
230
+
231
+ // What a template's slot holds, as blocks made on the page: read as HTML pasted from elsewhere is
232
+ // (clipboard/import/), each block what a paste makes of it there.
233
+ function blocksOfTemplate(doc, slot) {
234
+ if (!slot.firstElementChild && !slot.textContent.trim()) return [];
235
+ const specs = htmlToBlocks(parseClipboardHTML(slot.innerHTML), {});
236
+ return specs.length > 0 ? toBlocks(specs, "slot").map((block) => buildBlock(doc, block)) : [];
237
+ }
238
+
239
+ /**
240
+ * An instance from what a clipboard carried (`data`): the file is written from that markup, which the
241
+ * commit sends, its slots empty; what they held comes as blocks of their own (`flows`, by name),
242
+ * made again here as any pasted block (`inner`).
243
+ */
244
+ export function build({ doc, block, inner }) {
201
245
  const entry = entryOf(block.package);
202
246
  if (entry) bring(entry);
203
247
  const instance = newInstance(doc, block.html);
204
248
  instance.setAttribute("data-ge-copy", block.html);
249
+ slotsMade(doc, instance, (slot) => inner?.(block.flows?.[slot.getAttribute(SLOT)] ?? []) ?? []);
205
250
  editableParts(instance);
206
251
  return instance;
207
252
  }
@@ -232,7 +277,7 @@ const inStep = (entry) => listed && entry !== null && !overtaken(entry);
232
277
  * blocks where it came with none.
233
278
  */
234
279
  function arrive(instance) {
235
- if (!instance.isConnected || instance.parentElement?.closest(INSTANCE)) return;
280
+ if (!instance.isConnected || heldBy(instance)) return;
236
281
  const id = instance.getAttribute(ATTRIBUTE);
237
282
  const entry = entryOf(id);
238
283
  if (isBehind(id) && !inStep(entry)) return void window.dispatchEvent(new CustomEvent("doc-components-changed"));
@@ -242,12 +287,34 @@ function arrive(instance) {
242
287
  if (!instance.querySelector("[data-ge-block], [data-ge-part]")) editableParts(instance);
243
288
  }
244
289
 
290
+ // A slot of the page's (a box) taken off an instance still on the page, by its component's script
291
+ // (`node`, what was taken off from `from`, before `next`; the instance itself taken off is the editor's
292
+ // doing, and none of this): a slot is the document's, which no component writes, so it goes back where
293
+ // it stood, what it holds with it, and the reader's blocks stay, on the page and in the file. Who
294
+ // wrote the package hears of it.
295
+ function keepSlots(node, from, next) {
296
+ if (node.nodeType !== 1 || node.matches(INSTANCE) || !from.isConnected) return;
297
+ const instance = from.closest(INSTANCE);
298
+ const slots = [node, ...node.querySelectorAll(SLOT_PART)].filter((slot) => slot.matches(`:is(${SLOT_PART})[data-ge-box]`) && !slot.parentElement?.closest(INSTANCE));
299
+ if (!instance || slots.length === 0) return;
300
+ for (const slot of slots) from.insertBefore(slot, next?.parentNode === from ? next : null);
301
+ console.error(`[DocKit] ${instance.getAttribute(ATTRIBUTE)}: a slot left its instance: it is put back, its blocks the document's`);
302
+ }
303
+
245
304
  // A package's picture where a menu shows a mark.
246
305
  const pictureOf = ({ preview }) => (preview ? `<img src="${preview}" alt="">` : null);
247
306
 
307
+ // A slot the file has empty (an Agent's): what a disclosure's content says (details/page.mjs), a click
308
+ // on it making a paragraph there (insert/ clickBlank).
309
+ const SLOT_EMPTY = `
310
+ [data-ge-editing] [data-doc-slot][data-ge-box]:not(:has(> *))::before { content: ${JSON.stringify(t("空白内容区。点击或拖动区块到这里。"))}; display: block; padding: 3px 2px; border-radius: 4px; color: var(--ge-tx3); cursor: pointer; transition: background 20ms ease-in; }
311
+ [data-ge-editing] [data-doc-slot][data-ge-box]:not(:has(> *)):hover::before { background: var(--ge-hover); }
312
+ `;
313
+
248
314
  /** An instance on the page: what its events are, what Enter does in its words, and what the menus make of its package. */
249
- export function create({ map, newParagraph, active, sourceStep }) {
315
+ export function create({ map, newParagraph, active, sourceStep, struct }) {
250
316
  held = map;
317
+ addStyle(SLOT_EMPTY);
251
318
  // Whether a key in an instance, on no field of it, is undo or redo: a button or what a component draws
252
319
  // has no undo of its own, and a press on one that wrote leaves the focus there, from where the page's
253
320
  // undo goes on through every step, as notion-lite's does from anywhere (onHistoryKey). A field keeps
@@ -294,12 +361,30 @@ export function create({ map, newParagraph, active, sourceStep }) {
294
361
  return true;
295
362
  }
296
363
  return {
297
- keepsKeys: (node, event) => node?.nodeType === 1 && Boolean(node.parentElement?.closest(INSTANCE)) && !editable(node) && !pageUndo(node, event),
298
- undoHere: (node) => Boolean(node.parentElement?.closest(INSTANCE)) && !editable(node),
364
+ keepsKeys: (node, event) => componentAt(node) && !editable(node) && !pageUndo(node, event),
365
+ undoHere: (node) => componentAt(node) && !editable(node),
299
366
  keys: {
300
- // notion-lite's captionEnter: words of a block that are not the block's own line do not split,
301
- // Enter there is a text block after the block, the caret in it.
367
+ // In a text part with a slot after it (in the file's order): the caret goes into the slot, as from
368
+ // a disclosure's title into its content (details/page.mjs), at the start of its first block, or in
369
+ // a paragraph made first in it, one step of undo. Else notion-lite's captionEnter: words of a
370
+ // block that are not the block's own line do not split, Enter there is a text block after the
371
+ // block, the caret in it.
302
372
  enter({ host }) {
373
+ const instance = host.parentElement?.closest(INSTANCE);
374
+ const slot = instance && slotsIn(instance).find((each) => each.hasAttribute("data-ge-box") && host.compareDocumentPosition(each) & Node.DOCUMENT_POSITION_FOLLOWING && !host.contains(each));
375
+ if (slot) {
376
+ const first = firstHost(slot);
377
+ if (first) caretAt(first, "start");
378
+ else {
379
+ const paragraph = newHost(document, "paragraph");
380
+ struct([slot], [], () => {
381
+ slot.prepend(paragraph);
382
+ return { touched: [paragraph], focus: paragraph };
383
+ });
384
+ caretAt(paragraph, "start");
385
+ }
386
+ return true;
387
+ }
303
388
  const unit = unitElement(host);
304
389
  const box = unit?.parentElement;
305
390
  if (!box || !accepts(box.getAttribute("data-ge-box-kind") ?? "flow", "paragraph")) return false;
@@ -311,7 +396,9 @@ export function create({ map, newParagraph, active, sourceStep }) {
311
396
  // by block types), by its id, as notion-lite's variants: a row of the menus, found by its name in
312
397
  // either language and its words, and what makes one (notion-lite's
313
398
  // insertTypedBlock: the caret in its first text part; with none, in an empty paragraph after it, as
314
- // for a divider).
399
+ // for a divider). Its slots are boxes made on the page at once, what the template has in each made
400
+ // blocks of the page's (an empty paragraph where it has none), so that the reader writes in one
401
+ // before the instance is saved, and Enter in a text part goes into the slot after it.
315
402
  menuVariants: () => Object.fromEntries((map.components ?? []).filter(({ template, state }) => template && state === "normal").map((entry) => [entry.id, {
316
403
  label: entry.name[language],
317
404
  icon: pictureOf(entry) ?? ICON,
@@ -321,20 +408,33 @@ export function create({ map, newParagraph, active, sourceStep }) {
321
408
  create({ doc, put: place, paragraph }) {
322
409
  bring(entry);
323
410
  const instance = newInstance(doc, entry.template);
411
+ slotsMade(doc, instance, (slot) => blocksOfTemplate(doc, slot));
324
412
  ownIds(doc, [instance]);
325
413
  place(instance);
326
- return editableParts(instance)[0] ?? place(paragraph());
414
+ return editableParts(instance)[0] ?? firstHost(instance) ?? place(paragraph());
327
415
  },
328
416
  }])),
417
+ /**
418
+ * Whether a copy of an instance's unit stands for it: not where it has slots, whose blocks that copy
419
+ * would write whole, with no ids the page knows; such an instance is copied by what it carries alone
420
+ * (index.mjs `data`: its slots' blocks apart), in its document too.
421
+ */
422
+ copiesAsItself: (unit) => slotsIn(unit).length === 0,
329
423
  /** What the reader calls a block of it: its package's name, where the page knows its package. */
330
424
  nameOf: (unit) => entryOf(unit.getAttribute(ATTRIBUTE))?.name[language] ?? null,
331
425
  /**
332
426
  * One the file dropped that the page still shows, a reader's to keep: it goes from now on as what a
333
- * clipboard carried does, its markup as the kit would start it. Its template would not hold what
334
- * the reader has.
427
+ * clipboard carried does, its markup as the kit would start it, its slots empty: what they hold is
428
+ * made on the page from now on too (sync-state.mjs asMade), and goes apart (index.mjs `extras`).
429
+ * Its template would not hold what the reader has.
335
430
  */
336
431
  renew(instance) {
337
- if (!instance.hasAttribute("data-ge-copy")) instance.setAttribute("data-ge-copy", copyOf(instance, { named: true }).outerHTML);
432
+ if (!instance.hasAttribute("data-ge-copy")) {
433
+ const copy = copyOf(instance, { named: true });
434
+ const boxes = slotsIn(instance).map((slot) => slot.hasAttribute("data-ge-box"));
435
+ slotsIn(copy).forEach((slot, index) => { if (boxes[index]) slot.replaceChildren(); });
436
+ instance.setAttribute("data-ge-copy", copy.outerHTML);
437
+ }
338
438
  instance.setAttribute("data-ge-type", "installed");
339
439
  return true;
340
440
  },
@@ -401,12 +501,13 @@ export function create({ map, newParagraph, active, sourceStep }) {
401
501
  }
402
502
  document.addEventListener("doc-component-edit", (event) => { if (write(event.detail, event.target)) event.preventDefault(); });
403
503
  new MutationObserver((records) => {
404
- for (const { type, target, addedNodes } of records) {
504
+ for (const { type, target, addedNodes, removedNodes, nextSibling } of records) {
405
505
  if (type === "attributes") followFallback(target);
406
506
  for (const node of addedNodes) {
407
507
  if (node.nodeType !== 1 || node.hasAttribute("data-doc-dev")) continue;
408
508
  [...(node.matches(INSTANCE) ? [node] : []), ...node.querySelectorAll(INSTANCE)].forEach(arrive);
409
509
  }
510
+ for (const node of removedNodes) keepSlots(node, target, nextSibling);
410
511
  }
411
512
  }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [FALLBACK] });
412
513
  window.addEventListener("doc-catalog-changed", readList);
@@ -51,6 +51,9 @@ export const CODE = Object.freeze({
51
51
  /** A highlight block, the opening summary included: the one place the editor says which elements are one. */
52
52
  export const HIGHLIGHT = Object.freeze({ match: component("highlight").match });
53
53
 
54
+ /** A callout (提示块). */
55
+ export const CALLOUT = Object.freeze({ match: component("callout").match });
56
+
54
57
  // A display formula block and an inline one (the math description's two rules).
55
58
  const [mathInline, mathBlock] = component("math").match.split(",").map((rule) => rule.trim());
56
59
  export const MATH = Object.freeze({ inline: mathInline, block: mathBlock });
@@ -1,6 +1,5 @@
1
1
  import { MEDIA_BLOCKS, MEDIA_CLASS, mediaBlockOf } from "../../../shared/components/index.mjs";
2
2
  import { caretAt } from "../../page/caret.mjs";
3
- import { UNIT } from "../../page/core.mjs";
4
3
  import { openMediaMenu } from "../../page/media-menu/index.mjs";
5
4
  import { keptByComponent } from "../../page/page-objects.mjs";
6
5
  import { blobBase64 } from "../../page/post.mjs";
@@ -8,6 +7,7 @@ import { addStyle } from "../../page/ui.mjs";
8
7
  import { barClass } from "../placeholder-bar.mjs";
9
8
  import { captionOf } from "./parts.mjs";
10
9
  import { TOOLBAR_STYLE } from "./toolbar.mjs";
10
+ import { ZWSP } from "../../../shared/inline-spacing.mjs";
11
11
  import { t } from "../../../shared/i18n/index.mjs";
12
12
 
13
13
  // What the four media blocks (图片, 文件, 网页书签, 嵌入) do on the page alike (notion-lite's
@@ -15,13 +15,17 @@ import { t } from "../../../shared/i18n/index.mjs";
15
15
  // block's menu; what is chosen replaces the placeholder with a NEW element (never the same one, so
16
16
  // that undo brings back the placeholder as it was and the file loses the block); a caption shows
17
17
  // once it is asked for or focused, goes out of sight when it is left empty, and Enter in it makes a
18
- // paragraph after the block. What a block makes of what is chosen (its picture, row, card or frame),
19
- // its menu, rows and tools are its own (image/, file/, bookmark/, embed/ page.mjs).
18
+ // paragraph after the block; Backspace or Delete in an empty one closes it and selects its block. It
19
+ // is plain text under the block, with no fill under the pointer or the caret. What a block makes of
20
+ // what is chosen (its picture, row, card or frame), its menu, rows and tools are its own (image/,
21
+ // file/, bookmark/, embed/ page.mjs).
20
22
 
21
23
  const STYLE = `${TOOLBAR_STYLE}
22
24
  /* A caption is shown with words, while it has the caret, or once 标题 asks for it (data-ge-caption), with Notion's placeholder. */
23
25
  ${MEDIA_BLOCKS.map((id) => `[data-ge-editing] .${MEDIA_CLASS[id]}[data-ge-caption] > figcaption`).join(", ")} { display: block; }
24
26
  ${MEDIA_BLOCKS.map((id) => `[data-ge-editing] .${MEDIA_CLASS[id]} > figcaption:empty::before`).join(", ")} { content: ${JSON.stringify(t("写一个标题…"))}; color: var(--ge-tx3); }
27
+ /* A caption is plain text under its block, as Notion's and Feishu's: no fill under the pointer or the caret (editor.mjs's --ge-text-tint). */
28
+ [data-ge-editing] [data-ge-kind="mediaCaption"][contenteditable="true"] { --ge-text-tint: none; }
25
29
  `;
26
30
 
27
31
  // Every media placeholder (a gray bar), by the selector the page finds it by.
@@ -30,6 +34,10 @@ const BARS = MEDIA_BLOCKS.map((id) => `main figure.${MEDIA_CLASS[id]} > .${barCl
30
34
  // What a placeholder's words were before something was under way (busy), by the words' element.
31
35
  const idleWords = new WeakMap();
32
36
 
37
+ // A caption with nothing in it: no words, no chip (a formula, a page's link); what the browser or a
38
+ // pad leaves (a <br>, a zero-width space) is nothing. A space is something: Backspace takes it.
39
+ const blank = (text) => !text.textContent.replaceAll(ZWSP, "") && !text.querySelector("[data-ge-atom]");
40
+
33
41
  /**
34
42
  * The media blocks on the page, with what the editor gives them: struct (an undo step), say,
35
43
  * active() (the block tools may act), blocks() (the block tools), newParagraph(parent, after),
@@ -78,6 +86,30 @@ export function create({ struct, finishStep, look, say, active, blocks, newParag
78
86
  event.preventDefault();
79
87
  newParagraph(figure.parentElement, figure);
80
88
  }, true);
89
+ // Backspace or Delete in an empty caption closes it (the user's rule, 2026-11-06; Notion's and
90
+ // notion-lite's do nothing there): out of sight as when the caret leaves it, the file keeping its
91
+ // empty <figcaption> (no undo step: nothing of the file changes), and its block selected, as Esc
92
+ // there does and as Backspace selects a block above that holds no text (typing/): Backspace again
93
+ // deletes the block. A key held down stops at the empty caption (`repeat`), so that holding
94
+ // Backspace to clear its words never takes the block with them.
95
+ document.addEventListener("keydown", (event) => {
96
+ if ((event.key !== "Backspace" && event.key !== "Delete") || event.ctrlKey || event.metaKey || event.altKey || event.isComposing || event.keyCode === 229) return;
97
+ const text = event.target?.closest?.('[data-ge-kind="mediaCaption"]');
98
+ const figure = text?.parentElement?.closest("figure");
99
+ if (!figure || !active() || !blank(text)) return;
100
+ event.preventDefault();
101
+ event.stopImmediatePropagation();
102
+ if (!event.repeat) closeCaption(text, figure);
103
+ }, true);
104
+
105
+ // A caption closed: emptied of what the browser left, its mark gone, its block selected (which
106
+ // takes the focus off it; where the block cannot be selected, the focus leaves it all the same).
107
+ function closeCaption(text, figure) {
108
+ text.replaceChildren();
109
+ figure.removeAttribute("data-ge-caption");
110
+ select(figure);
111
+ if (text.contains(document.activeElement)) text.blur();
112
+ }
81
113
 
82
114
  /** The parts the editing of media block `id` is made from. */
83
115
  function block(id) {
@@ -133,25 +165,23 @@ export function create({ struct, finishStep, look, say, active, blocks, newParag
133
165
  if (!pasted) select(next);
134
166
  else if (picked) blocks().select(next, { quiet: true });
135
167
  },
136
- /** The caption shown (an empty one too), the caret at its end. */
168
+ /** The caption shown (an empty one too), the caret at its end, no block left selected (notion-lite's editCaption: setSelection([])). */
137
169
  caption(figure) {
138
170
  const text = figure && captionOf(figure);
139
171
  if (!text) return;
172
+ blocks()?.clear();
140
173
  figure.setAttribute("data-ge-caption", "");
141
174
  caretAt(text, "end");
142
175
  },
143
176
  /**
144
- * `file` pasted into `host` (a text block): a new block made by `make(doc)` below its block, then
145
- * `fill(figure, file)` (its placeholder filled). Returns whether it took the file.
177
+ * `file` pasted: a new block made by `make(doc)`, put in its place by `put` (the paste's, clipboard/:
178
+ * where a block pasted there goes, one undo step; false when it could not), then `fill(figure, file)`
179
+ * (its placeholder filled). Returns whether it took the file.
146
180
  */
147
- paste(host, file, make, fill) {
148
- const unit = host?.closest?.(UNIT);
149
- if (!file || !unit?.parentElement) return false;
150
- const figure = make(host.ownerDocument);
151
- struct([unit.parentElement], [], () => {
152
- unit.after(figure);
153
- return { touched: [], focus: null };
154
- });
181
+ paste(put, file, make, fill) {
182
+ if (!file) return false;
183
+ const figure = make(document);
184
+ if (!put(figure)) return false;
155
185
  fill(figure, file);
156
186
  return true;
157
187
  },
@@ -1,5 +1,4 @@
1
1
  import { create as bookmark } from "./bookmark/page.mjs";
2
- import { create as callout } from "./callout/page.mjs";
3
2
  import { create as code } from "./code/page.mjs";
4
3
  import { create as columns } from "./columns/page.mjs";
5
4
  import { create as details } from "./details/page.mjs";
@@ -15,6 +14,7 @@ import { create as installed } from "./installed/page.mjs";
15
14
  import { create as list } from "./list/page.mjs";
16
15
  import { create as math } from "./math/page.mjs";
17
16
  import { create as media } from "./media/page.mjs";
17
+ import { create as heading } from "./heading/page.mjs";
18
18
  import { create as paragraph } from "./paragraph/page.mjs";
19
19
  import { create as section } from "./section/page.mjs";
20
20
  import { create as sheets } from "./sheets/page.mjs";
@@ -29,7 +29,7 @@ import { create as table } from "./table/page.mjs";
29
29
  // listeners onto the document as it is made, and media comes before the four blocks made of its
30
30
  // parts (block(id)). It is also the order they are asked in where every one is: the columns tidy
31
31
  // before the lists (`tidy`), an image takes a pasted file before a file block does (`acceptsFile`).
32
- const PAGES = { columns, code, list, flowchart, nodeAdder, details, dialog, callout, paragraph, section, icon, sheets, sheetPills, figure, media, image, file, bookmark, embed, math, table, installed };
32
+ const PAGES = { columns, code, list, flowchart, nodeAdder, details, dialog, paragraph, heading, section, icon, sheets, sheetPills, figure, media, image, file, bookmark, embed, math, table, installed };
33
33
 
34
34
  /**
35
35
  * Every component's page object, by its component's name (and the flowchart's "+", nodeAdder, and a