@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.
- package/THIRD-PARTY-NOTICES.md +51 -1
- package/browser/editor/components/callout/index.mjs +40 -15
- package/browser/editor/components/columns/page.mjs +41 -28
- package/browser/editor/components/details/index.mjs +2 -1
- package/browser/editor/components/dialog/index.mjs +2 -1
- package/browser/editor/components/dialog/page.mjs +0 -18
- package/browser/editor/components/divider/index.mjs +1 -1
- package/browser/editor/components/figure/page.mjs +10 -1
- package/browser/editor/components/file/page.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +2 -2
- package/browser/editor/components/heading/index.mjs +2 -0
- package/browser/editor/components/heading/page.mjs +6 -0
- package/browser/editor/components/highlight/index.mjs +4 -20
- package/browser/editor/components/icons.mjs +5 -2
- package/browser/editor/components/image/page.mjs +2 -2
- package/browser/editor/components/installed/index.mjs +71 -25
- package/browser/editor/components/installed/page.mjs +122 -21
- package/browser/editor/components/kit.mjs +3 -0
- package/browser/editor/components/media/page.mjs +44 -14
- package/browser/editor/components/pages.mjs +2 -2
- package/browser/editor/components/registry.mjs +56 -30
- package/browser/editor/components/section/index.mjs +2 -1
- package/browser/editor/components/set-apart.mjs +24 -0
- package/browser/editor/components/table/index.mjs +5 -3
- package/browser/editor/components/table/page.mjs +29 -18
- package/browser/editor/page/annotate/anchors.mjs +4 -3
- package/browser/editor/page/annotate/index.mjs +61 -34
- package/browser/editor/page/autosave.mjs +50 -4
- package/browser/editor/page/beside.mjs +5 -5
- package/browser/editor/page/block-menu/colors.mjs +3 -1
- package/browser/editor/page/block-menu/index.mjs +8 -6
- package/browser/editor/page/blocks.mjs +16 -7
- package/browser/editor/page/caret.mjs +4 -1
- package/browser/editor/page/changes/bar.mjs +102 -0
- package/browser/editor/page/changes/ghost.mjs +206 -0
- package/browser/editor/page/changes/gone-words.mjs +94 -0
- package/browser/editor/page/changes/index.mjs +447 -0
- package/browser/editor/page/changes/words.mjs +27 -0
- package/browser/editor/page/clipboard/import/colors.mjs +3 -3
- package/browser/editor/page/clipboard/index.mjs +32 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +14 -5
- package/browser/editor/page/clipboard/own/describe.mjs +16 -7
- package/browser/editor/page/clipboard/own/export.mjs +6 -6
- package/browser/editor/page/clipboard/own/selection.mjs +2 -2
- package/browser/editor/page/clipboard/own/writers.mjs +0 -11
- package/browser/editor/page/columns/index.mjs +1 -1
- package/browser/editor/page/commit.mjs +19 -4
- package/browser/editor/page/conflicts.mjs +6 -26
- package/browser/editor/page/convert.mjs +8 -7
- package/browser/editor/page/core.mjs +55 -17
- package/browser/editor/page/drag/index.mjs +40 -24
- package/browser/editor/page/drag/preview.mjs +9 -4
- package/browser/editor/page/editor.mjs +70 -15
- package/browser/editor/page/emoji-picker/emoji-tab.mjs +22 -13
- package/browser/editor/page/emoji-picker/grid.mjs +7 -5
- package/browser/editor/page/emoji-picker/icon-tab.mjs +4 -4
- package/browser/editor/page/emoji-picker/icons.mjs +0 -1
- package/browser/editor/page/emoji-picker/index.mjs +11 -9
- package/browser/editor/page/emoji-picker/styles.mjs +161 -2
- package/browser/editor/page/emoji-picker/support.mjs +6 -4
- package/browser/editor/page/emoji-picker/upload.mjs +12 -11
- package/browser/editor/page/fills.mjs +5 -3
- package/browser/editor/page/fold/index.mjs +348 -0
- package/browser/editor/page/fold/rule.mjs +125 -0
- package/browser/editor/page/folded.mjs +23 -0
- package/browser/editor/page/format-bar/bar.mjs +2 -1
- package/browser/editor/page/format-bar/index.mjs +1 -1
- package/browser/editor/page/geometry.mjs +8 -6
- package/browser/editor/page/handle/index.mjs +17 -6
- package/browser/editor/page/hints/index.mjs +1 -1
- package/browser/editor/page/insert/index.mjs +30 -17
- package/browser/editor/page/long-runs.mjs +75 -0
- package/browser/editor/page/look.mjs +15 -3
- package/browser/editor/page/margin.mjs +45 -24
- package/browser/editor/page/menu.mjs +69 -25
- package/browser/editor/page/merge.mjs +24 -0
- package/browser/editor/page/notices.mjs +1 -1
- package/browser/editor/page/offered.mjs +1 -1
- package/browser/editor/page/outline/folds.mjs +11 -10
- package/browser/editor/page/outline/index.mjs +131 -45
- package/browser/editor/page/outline/list.mjs +23 -11
- package/browser/editor/page/outline/read.mjs +1 -1
- package/browser/editor/page/outline/strip.mjs +7 -1
- package/browser/editor/page/panel.mjs +5 -4
- package/browser/editor/page/resize/index.mjs +1 -1
- package/browser/editor/page/selection/delete.mjs +8 -6
- package/browser/editor/page/selection/index.mjs +10 -3
- package/browser/editor/page/session.mjs +54 -3
- package/browser/editor/page/table/cell-bar.mjs +4 -1
- package/browser/editor/page/table/cells.mjs +1 -1
- package/browser/editor/page/table/icons.mjs +1 -1
- package/browser/editor/page/table/index.mjs +13 -8
- package/browser/editor/page/table/widths.mjs +9 -4
- package/browser/editor/page/text-select/index.mjs +18 -3
- package/browser/editor/page/tooltip.mjs +1 -1
- package/browser/editor/page/type-grid.mjs +4 -3
- package/browser/editor/page/typing/code-edges.mjs +63 -17
- package/browser/editor/page/typing/index.mjs +12 -6
- package/browser/editor/page/typing/leave.mjs +1 -1
- package/browser/shared/annotations.mjs +8 -10
- package/browser/shared/code-folds.mjs +5 -1
- package/browser/shared/colors.mjs +15 -13
- package/browser/shared/component-catalog.mjs +24 -10
- package/browser/shared/components/callout.mjs +4 -2
- package/browser/shared/components/index.mjs +61 -7
- package/browser/shared/components/installed.mjs +75 -5
- package/browser/shared/components/page.mjs +10 -1
- package/browser/shared/components/table.mjs +1 -1
- package/browser/shared/components/text.mjs +2 -2
- package/browser/shared/document-look.mjs +178 -0
- package/browser/shared/emoji/data/LICENSE-lucide.txt +43 -0
- package/browser/shared/emoji/data/README.md +4 -2
- package/browser/shared/emoji/data/categories.json +1 -1
- package/browser/shared/emoji/data/keywords-en.json +1 -1
- package/browser/shared/emoji/data/keywords-zh.json +1 -1
- package/browser/shared/emoji/data/names.json +1 -1
- package/browser/shared/emoji/data/positions.json +1 -1
- package/browser/shared/emoji/data/twemoji-index.json +1 -1
- package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
- package/browser/shared/emoji/data.mjs +10 -12
- package/browser/shared/emoji/emoji.mjs +8 -7
- package/browser/shared/emoji/icon-value.mjs +4 -3
- package/browser/shared/html-tree.mjs +11 -5
- package/browser/shared/i18n/en.mjs +67 -8
- package/browser/shared/long-runs.mjs +39 -0
- package/browser/shared/new-names.mjs +5 -2
- package/browser/shared/outline.mjs +194 -0
- package/browser/shared/preferences.mjs +10 -4
- package/browser/shared/session.mjs +5 -4
- package/browser/shared/workspace-look.mjs +91 -0
- package/browser/shared/zoom.mjs +5 -5
- package/dist/chunk-3HIRPVLU.mjs +2 -0
- package/dist/chunk-4FS7Z7WR.mjs +2 -0
- package/dist/{chunk-LHTFLRB6.mjs → chunk-5MHMDLZD.mjs} +2 -2
- package/dist/{chunk-YXAUQPVK.mjs → chunk-5ZKJ3H4P.mjs} +3 -3
- package/dist/{chunk-ZBIM6ARY.mjs → chunk-64K7IPLO.mjs} +55 -16
- package/dist/{chunk-QYKCJ7FG.mjs → chunk-6QXGZM36.mjs} +1 -1
- package/dist/chunk-6RGJOMJP.mjs +16 -0
- package/dist/{chunk-EUX7IC5J.mjs → chunk-7DTINW6U.mjs} +2 -2
- package/dist/{chunk-6CYG3Z4V.mjs → chunk-AJ73XJF3.mjs} +1 -1
- package/dist/chunk-AUZGSZJH.mjs +13 -0
- package/dist/{chunk-7QQBR7PB.mjs → chunk-BFE45AKL.mjs} +1 -1
- package/dist/chunk-BGGDRNYQ.mjs +10 -0
- package/dist/{chunk-L4DVONOG.mjs → chunk-BXO2ZY5P.mjs} +1 -1
- package/dist/chunk-CEH4MRCC.mjs +2 -0
- package/dist/chunk-CY2FNNAB.mjs +16 -0
- package/dist/{chunk-LC7YNQFG.mjs → chunk-DFUDV4LX.mjs} +1 -1
- package/dist/chunk-DKXG4XDG.mjs +2 -0
- package/dist/chunk-E7LWZRXY.mjs +4 -0
- package/dist/chunk-EZQ34M3F.mjs +124 -0
- package/dist/chunk-FDLXYYG4.mjs +4 -0
- package/dist/chunk-FUKQNOQS.mjs +9 -0
- package/dist/{chunk-DB4WKGYF.mjs → chunk-GB5SOYQJ.mjs} +2 -2
- package/dist/{chunk-ACOPDCNB.mjs → chunk-IVDLMMBD.mjs} +1 -1
- package/dist/chunk-JPOSLGOC.mjs +6 -0
- package/dist/chunk-JVHHHGEA.mjs +3 -0
- package/dist/chunk-L3QKH4OK.mjs +26 -0
- package/dist/{chunk-B3VTYBYR.mjs → chunk-LCTHTZIL.mjs} +2 -2
- package/dist/chunk-LEF6DRCK.mjs +59 -0
- package/dist/chunk-LS4F7JUM.mjs +2 -0
- package/dist/chunk-NO5NJCHO.mjs +2 -0
- package/dist/{chunk-UNUEWH4S.mjs → chunk-NQPVAU63.mjs} +1 -1
- package/dist/chunk-O35FIXRB.mjs +2 -0
- package/dist/chunk-PL6GHLGD.mjs +15 -0
- package/dist/{chunk-G7TOXONE.mjs → chunk-QJIR2BLP.mjs} +1 -1
- package/dist/chunk-QUPLVPB2.mjs +7 -0
- package/dist/chunk-R7BNENLY.mjs +4 -0
- package/dist/chunk-S6O4UHLC.mjs +2 -0
- package/dist/{chunk-HMHK3EQ6.mjs → chunk-SGJWQKEI.mjs} +1 -1
- package/dist/chunk-SYGN7GLU.mjs +2 -0
- package/dist/chunk-TYK6VNAB.mjs +10 -0
- package/dist/{chunk-EVJ3BX3M.mjs → chunk-U6KNPJPR.mjs} +1 -1
- package/dist/{chunk-YZ5Z6H4W.mjs → chunk-UALRNVSE.mjs} +1 -1
- package/dist/chunk-UMHUSOPJ.mjs +2 -0
- package/dist/chunk-V3HXVQEM.mjs +2 -0
- package/dist/{chunk-UXUK4PMC.mjs → chunk-V5JTW2LH.mjs} +1 -1
- package/dist/chunk-V5VXUJKG.mjs +2 -0
- package/dist/chunk-VBXMADMP.mjs +2 -0
- package/dist/chunk-VNV55RUA.mjs +2 -0
- package/dist/chunk-VPD2PS35.mjs +2 -0
- package/dist/chunk-W3BGBJQW.mjs +2 -0
- package/dist/chunk-W4VKN3GF.mjs +7 -0
- package/dist/chunk-W5BXXPDJ.mjs +3 -0
- package/dist/{chunk-Z52HTUW7.mjs → chunk-XNROSH2T.mjs} +2 -2
- package/dist/chunk-XUNHRCOG.mjs +6 -0
- package/dist/{chunk-6U2SLSDN.mjs → chunk-Y6RLRPG3.mjs} +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/math.js +18 -2
- package/kit/report.css +248 -137
- package/kit/runtime.js +126 -51
- package/kit/tables.js +51 -1
- package/kit/ui.js +97 -0
- package/package.json +1 -1
- package/release.json +8 -8
- package/sdk/doc-kit.d.ts +9 -3
- package/skill/SKILL.md +22 -10
- package/skill/references/building-components.md +46 -6
- package/skill/references/example-report.html +1 -1
- package/skill/references/writing-html.md +4 -4
- package/skill/sdk/doc-kit.d.ts +9 -3
- package/web/assets/index-Bh74DfsV.css +1 -0
- package/web/assets/index-UW0RyFFt.js +13 -0
- package/web/index.html +2 -2
- package/browser/editor/components/callout/page.mjs +0 -13
- package/dist/chunk-2PBALTR3.mjs +0 -57
- package/dist/chunk-3LA3EMDE.mjs +0 -15
- package/dist/chunk-5FRMCDPY.mjs +0 -16
- package/dist/chunk-5LEK2CBS.mjs +0 -2
- package/dist/chunk-5OFN5AHC.mjs +0 -2
- package/dist/chunk-5SIYS5HN.mjs +0 -2
- package/dist/chunk-5TEK3RAD.mjs +0 -2
- package/dist/chunk-7GHT2UCA.mjs +0 -15
- package/dist/chunk-ATCJYAR4.mjs +0 -2
- package/dist/chunk-C7HWOBIF.mjs +0 -2
- package/dist/chunk-E7VYDG2X.mjs +0 -2
- package/dist/chunk-FQGQH4KM.mjs +0 -2
- package/dist/chunk-GPF4YWWB.mjs +0 -9
- package/dist/chunk-GYQV7Y4J.mjs +0 -120
- package/dist/chunk-HQQMJCQJ.mjs +0 -10
- package/dist/chunk-IERUOGC3.mjs +0 -6
- package/dist/chunk-ITNLYXXV.mjs +0 -3
- package/dist/chunk-NFIBI4IP.mjs +0 -26
- package/dist/chunk-O52JMOCZ.mjs +0 -2
- package/dist/chunk-QMQUXTU4.mjs +0 -12
- package/dist/chunk-QVWVFC5K.mjs +0 -6
- package/dist/chunk-QZY6Q64C.mjs +0 -2
- package/dist/chunk-TDQP3DKQ.mjs +0 -10
- package/dist/chunk-V4CPWKMZ.mjs +0 -2
- package/dist/chunk-VACRB2NE.mjs +0 -3
- package/dist/chunk-W63L7367.mjs +0 -7
- package/dist/chunk-W7CXPCRV.mjs +0 -6
- package/dist/chunk-XINNG4F2.mjs +0 -4
- package/dist/chunk-YL2DPWVS.mjs +0 -4
- package/web/assets/index-B6Ai3wsa.js +0 -13
- package/web/assets/index-SYPC3EZy.css +0 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { t } from "../../../shared/i18n/index.mjs";
|
|
2
|
+
import { onPreferences, preference } from "../../../shared/preferences.mjs";
|
|
3
|
+
import { readText, rangeOf } from "../annotate/anchors.mjs";
|
|
4
|
+
import { GONE_WORDS, nestedBlocks } from "../core.mjs";
|
|
5
|
+
import { createFills } from "../fills.mjs";
|
|
6
|
+
import { FOLLOW, task } from "../frames.mjs";
|
|
7
|
+
import { onSettle, own, provide } from "../overlays.mjs";
|
|
8
|
+
import { failure, postAnswer } from "../post.mjs";
|
|
9
|
+
import { addStyle } from "../ui.mjs";
|
|
10
|
+
import { UNDO_SHOWN, createBar } from "./bar.mjs";
|
|
11
|
+
import { GHOST_STYLE, createGhosts } from "./ghost.mjs";
|
|
12
|
+
import { editsBy, goneMark, placeOf, putMark, takeMark } from "./gone-words.mjs";
|
|
13
|
+
import { wordMarks } from "./words.mjs";
|
|
14
|
+
|
|
15
|
+
// The Agent's changes on the page (docs/editor.md "Agent's changes"), as Cursor reviews an Agent's:
|
|
16
|
+
// what an Agent wrote to the report since the reader last kept or reverted it (the server's review,
|
|
17
|
+
// editor/changes.mjs, read at the editor's start and never holding it back, drawn once the page is
|
|
18
|
+
// ready), marked without a change to the file: a block added (or changed with no words to tell, or
|
|
19
|
+
// whose words cannot be compared) a green fill (fills.mjs), words added the 批注's look in green (a
|
|
20
|
+
// CSS highlight under the 批注's own), the words taken out shown where they were, in red struck
|
|
21
|
+
// through (gone-words.mjs: an empty inline mark of the editor's own, its words drawn by CSS, which
|
|
22
|
+
// nothing that reads or writes the block's content takes), the blocks deleted shown where they were,
|
|
23
|
+
// a ghost of them in red struck through in room made for it (ghost.mjs, drawn in the overlay layer:
|
|
24
|
+
// nothing of it in the document). Nothing to rest the pointer on: all of it shows at once. The reader
|
|
25
|
+
// editing a block takes its words taken out away first (the keys, input, an input method, a paste,
|
|
26
|
+
// cut or drop in it, the format bar over it), and they are not drawn in it again until the caret has
|
|
27
|
+
// left it (the review read again then). The bar at the window's bottom (bar.mjs) says how many blocks
|
|
28
|
+
// and offers 撤回 (the server writes the baseline back: the reader's own saved edits stay; undone within
|
|
29
|
+
// UNDO_SHOWN from the bar of the page shown again) and 确认 (no more marks). 显示变更 off (设置),
|
|
30
|
+
// nothing is drawn and a review read is kept at once, quietly. The page reads the review again when
|
|
31
|
+
// its server says it changed (another tab kept it, an Agent wrote more) and 1.5s after a commit of its
|
|
32
|
+
// own while the bar shows. Not on a page opened on its own in a frame (?solo), printed, or presented
|
|
33
|
+
// (the preview's ?present has no editor).
|
|
34
|
+
|
|
35
|
+
const ROUTE = "/__gallery/edit/changes";
|
|
36
|
+
const REREAD_AFTER = 1500; // after a commit of the page's own, the review read again this long after (the reader paused)
|
|
37
|
+
const LEFT_AFTER = 150; // the caret left a block the reader edited: the review read again this long after (once saved)
|
|
38
|
+
const KEEP_TRIES = 3; // a quiet keep refused (the review grew meanwhile) is tried again this often
|
|
39
|
+
// What may edit the block it happens in: its words taken out go first (gone-words.mjs editsBy for a key).
|
|
40
|
+
const EDITS = ["keydown", "beforeinput", "compositionstart", "paste", "cut", "drop", "dragstart"];
|
|
41
|
+
|
|
42
|
+
const STYLE = `
|
|
43
|
+
::highlight(ge-diff-add) { background-color: var(--ge-diff-add); text-decoration: underline 2px var(--ge-diff-add-line); }
|
|
44
|
+
.ge-diff-del { user-select: none; pointer-events: none; }
|
|
45
|
+
.ge-diff-del::before { content: attr(data-ge-gone-words); border-radius: 3px; background: var(--ge-diff-del); color: var(--ge-diff-del-text); text-decoration: line-through; text-decoration-color: var(--ge-diff-del-text); -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
|
46
|
+
.ge-diff-del[data-ge-gone-lead]::before { margin-left: .28em; }
|
|
47
|
+
.ge-diff-del[data-ge-gone-tail]::after { content: " "; }
|
|
48
|
+
${GHOST_STYLE}
|
|
49
|
+
@media print { ::highlight(ge-diff-add) { background-color: transparent; text-decoration: none; } .ge-diff-del, .ge-diff-halo { display: none; } }
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const highlights = () => Boolean(globalThis.CSS?.highlights && globalThis.Highlight);
|
|
53
|
+
const holdsSelection = (block) => {
|
|
54
|
+
const selection = getSelection();
|
|
55
|
+
return selection.rangeCount > 0 && (block.contains(selection.anchorNode) || block.contains(selection.focusNode));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* slug, token: the map's; commits(), autosave(): the page's (made later than this); flush(): what
|
|
60
|
+
* waits saved now (as Ctrl+S); say(text, kind); session: sessionStorage (storage.mjs storageOf), where
|
|
61
|
+
* a revert's undo is kept over the page shown again; across: the selection across blocks (text-select/:
|
|
62
|
+
* texts(), redraw(texts)), kept on its words while marks go and come. Returns { start(),
|
|
63
|
+
* committed(receipt), leave() }.
|
|
64
|
+
*/
|
|
65
|
+
export function createChanges({ slug, token, commits, autosave, flush, say, session, across = null }) {
|
|
66
|
+
const undoKey = `gallery-edit:${slug}:changes-undo`;
|
|
67
|
+
const shows = () => preference("showChanges");
|
|
68
|
+
let review = null; // the review as last read and drawn: { stamp, counts, added, changed, removed }
|
|
69
|
+
let started = false;
|
|
70
|
+
let left = false;
|
|
71
|
+
let busy = false; // 撤回, 确认 or 撤销 under way
|
|
72
|
+
let reading = read(); // at once: the page's start does not wait for it
|
|
73
|
+
let rereadTimer = 0;
|
|
74
|
+
let keepTries = 0;
|
|
75
|
+
let offering = 0; // until when the bar offers 撤销 of this tab's revert
|
|
76
|
+
const filled = new Set(); // block elements filled green
|
|
77
|
+
const changedBlocks = new Map(); // a text block -> its words in the baseline
|
|
78
|
+
const added = new Map(); // a text block -> the ranges of its words added (the green highlight's)
|
|
79
|
+
let dels = []; // the words taken out shown: { node, block }
|
|
80
|
+
const edited = new Set(); // text blocks the reader edited since their words taken out were drawn
|
|
81
|
+
const ghosts = createGhosts();
|
|
82
|
+
let fills = null;
|
|
83
|
+
const bar = createBar({ revert: () => revert(), accept: () => keep() });
|
|
84
|
+
|
|
85
|
+
function read() {
|
|
86
|
+
return postAnswer(ROUTE, token, { slug, action: "read" });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------- finding the blocks it names (in the file now) ----------
|
|
90
|
+
const unitOf = (id) => document.querySelector(`[data-ge-unit="${id}"]`);
|
|
91
|
+
const body = () => document.querySelector("main");
|
|
92
|
+
function find(ref) {
|
|
93
|
+
if (!ref) return null;
|
|
94
|
+
const main = body() ?? document;
|
|
95
|
+
if (ref.title) return main.querySelector('[data-ge-kind="title"]'); // the page's own title, as the editor made it a text block
|
|
96
|
+
if (ref.id) return main.querySelector(`[data-block-id="${CSS.escape(ref.id)}"]`);
|
|
97
|
+
if (typeof ref.hash !== "string") return null;
|
|
98
|
+
const list = commits();
|
|
99
|
+
if (!list) return null;
|
|
100
|
+
let nth = 0;
|
|
101
|
+
for (const node of main.querySelectorAll("[data-ge-unit]")) {
|
|
102
|
+
if (list.fileHash(Number(node.getAttribute("data-ge-unit"))) !== ref.hash) continue;
|
|
103
|
+
if (nth++ === ref.nth) return node;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
// A unit's own text blocks in the page's order (the server's: its scope line's first, if a table's).
|
|
108
|
+
function ownBlocks(unit, ref) {
|
|
109
|
+
if (ref.title) return [unit];
|
|
110
|
+
const owner = (block) => {
|
|
111
|
+
const at = block.closest("[data-ge-unit], [data-ge-lead]");
|
|
112
|
+
return at?.hasAttribute("data-ge-lead") && !at.hasAttribute("data-ge-unit") ? unitOf(at.getAttribute("data-ge-lead")) : at;
|
|
113
|
+
};
|
|
114
|
+
const lead = document.querySelector(`[data-ge-lead="${unit.getAttribute("data-ge-unit")}"]`);
|
|
115
|
+
const within = (root) => [root, ...root.querySelectorAll("[data-ge-block]")].filter((block) => block.hasAttribute("data-ge-block") && owner(block) === unit);
|
|
116
|
+
return [...(lead && lead !== unit ? within(lead) : []), ...within(unit)];
|
|
117
|
+
}
|
|
118
|
+
// A text block's words as the server reads them: a list item's own, not the blocks it holds.
|
|
119
|
+
const readBlock = (block) => {
|
|
120
|
+
const held = nestedBlocks(block);
|
|
121
|
+
return readText([block], held.length ? { skip: (child) => held.includes(child) } : undefined);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ---------- drawing ----------
|
|
125
|
+
const frames = task("changes", { run: place, fail: () => keepingAcross(takeDown) });
|
|
126
|
+
// A selection across blocks (text-select/) kept on its words while marks go and come: its ends are
|
|
127
|
+
// points of its own, which a text node joined again (takeMark) would leave off the page, and so its
|
|
128
|
+
// pieces' live ranges are taken first (a text node split, putMark, they follow; one joined, takeMark
|
|
129
|
+
// sets them) and it is drawn again from them.
|
|
130
|
+
function keepingAcross(change) {
|
|
131
|
+
const texts = across?.texts() ?? null;
|
|
132
|
+
change(texts ? texts.map(({ range }) => range) : []);
|
|
133
|
+
if (texts) across.redraw(texts);
|
|
134
|
+
}
|
|
135
|
+
function takeDown(ranges = []) {
|
|
136
|
+
fills?.clear();
|
|
137
|
+
for (const { node } of dels) if (node.isConnected) takeMark(node, ranges);
|
|
138
|
+
dels = [];
|
|
139
|
+
ghosts.clear();
|
|
140
|
+
filled.clear();
|
|
141
|
+
changedBlocks.clear();
|
|
142
|
+
added.clear();
|
|
143
|
+
if (highlights()) CSS.highlights.delete("ge-diff-add");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function draw() {
|
|
147
|
+
keepingAcross((ranges) => drawNow(ranges));
|
|
148
|
+
}
|
|
149
|
+
function drawNow(ranges) {
|
|
150
|
+
takeDown(ranges);
|
|
151
|
+
if (!review || !shows()) {
|
|
152
|
+
edited.clear();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
addStyle(STYLE);
|
|
156
|
+
fills ??= createFills({ kind: "changes", className: "ge-diff-halo", stands: (block) => filled.has(block) });
|
|
157
|
+
for (const { ref } of review.added) {
|
|
158
|
+
const unit = find(ref);
|
|
159
|
+
if (unit) filled.add(unit);
|
|
160
|
+
}
|
|
161
|
+
for (const change of review.changed) {
|
|
162
|
+
const unit = find(change.ref);
|
|
163
|
+
if (!unit) continue;
|
|
164
|
+
if (change.whole) {
|
|
165
|
+
filled.add(unit);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const blocks = ownBlocks(unit, change.ref);
|
|
169
|
+
for (const { nth, base } of change.texts ?? []) {
|
|
170
|
+
if (blocks[nth]) changedBlocks.set(blocks[nth], base);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// A block the reader edited keeps its words taken out away while the caret is in it.
|
|
174
|
+
for (const block of [...edited]) if (!changedBlocks.has(block) || !holdsSelection(block)) edited.delete(block);
|
|
175
|
+
for (const [block, base] of changedBlocks) markWords(block, base, !edited.has(block));
|
|
176
|
+
paintAdded();
|
|
177
|
+
for (const group of review.removed) {
|
|
178
|
+
const anchor = group.before ? find(group.before) : group.after ? find(group.after) : body();
|
|
179
|
+
if (anchor) ghosts.add(group, anchor, group.before ? "before" : group.after ? "after" : "into");
|
|
180
|
+
}
|
|
181
|
+
if (filled.size || added.size || dels.length || ghosts.count) frames.wake();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// A changed text block's marks: the ranges of its words added (live: they follow the reader's
|
|
185
|
+
// typing), and, `taken`, its words taken out put in where they were (from the last, so that each
|
|
186
|
+
// place read before is still where it was: a text node is split only after it).
|
|
187
|
+
function markWords(block, base, taken) {
|
|
188
|
+
const text = readBlock(block);
|
|
189
|
+
const { ins, cuts } = wordMarks(base, text.text);
|
|
190
|
+
added.set(block, ins.map(([from, to]) => rangeOf(text, from, to)).filter(Boolean));
|
|
191
|
+
if (!taken) return;
|
|
192
|
+
for (const { at, text: words } of [...cuts].reverse()) {
|
|
193
|
+
const where = placeOf(text, at);
|
|
194
|
+
if (!where) continue;
|
|
195
|
+
const node = goneMark(words, document, { before: text.text[at - 1], after: text.text[at] });
|
|
196
|
+
const entry = { node, block };
|
|
197
|
+
own(node, "changes", { owner: block, alive: () => block.contains(node) && dels.includes(entry) });
|
|
198
|
+
putMark(node, where);
|
|
199
|
+
dels.push(entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function paintAdded() {
|
|
204
|
+
if (!highlights()) return;
|
|
205
|
+
const ranges = [...added.values()].flat();
|
|
206
|
+
if (ranges.length === 0) return void CSS.highlights.delete("ge-diff-add");
|
|
207
|
+
const highlight = new Highlight(...ranges);
|
|
208
|
+
highlight.priority = -1; // a 批注's marks over these
|
|
209
|
+
CSS.highlights.set("ge-diff-add", highlight);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Each frame something may have moved: the fills where their blocks are, the ghosts where their rooms are.
|
|
213
|
+
function place() {
|
|
214
|
+
if (!review) return false;
|
|
215
|
+
fills?.paint([...filled].filter((block) => block.isConnected));
|
|
216
|
+
ghosts.place();
|
|
217
|
+
return filled.size || ghosts.count ? FOLLOW : false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// What the page's edits took off the page goes with it (a block deleted, its marks): the words taken
|
|
221
|
+
// out shown in a block deleted or turned into another are taken out of it too, so that undo, which
|
|
222
|
+
// puts the very same element back, puts back none of them.
|
|
223
|
+
// A copy of a mark that a change made (a cell turned into a header cell or back, cells merged or
|
|
224
|
+
// filled, a disclosure's title made a paragraph, a block duplicated: whatever clones a block's
|
|
225
|
+
// content) stands for nothing and goes at once, before the overlays' audit would find it a stray:
|
|
226
|
+
// the review read again after the change's commit draws the words taken out where they are then.
|
|
227
|
+
onSettle(() => {
|
|
228
|
+
const drawn = dels.length > 0;
|
|
229
|
+
for (const block of [...filled]) if (!block.isConnected) filled.delete(block);
|
|
230
|
+
dels = dels.filter((entry) => {
|
|
231
|
+
if (entry.block.isConnected && entry.node.isConnected) return true;
|
|
232
|
+
if (entry.node.parentNode) takeMark(entry.node);
|
|
233
|
+
return false;
|
|
234
|
+
});
|
|
235
|
+
if (drawn) {
|
|
236
|
+
const ours = new Set(dels.map((entry) => entry.node));
|
|
237
|
+
for (const node of document.querySelectorAll(`main ${GONE_WORDS}`)) if (!ours.has(node)) takeMark(node);
|
|
238
|
+
}
|
|
239
|
+
ghosts.settle();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---------- the reader edits a block holding words taken out ----------
|
|
243
|
+
// They go first, before the edit touches the block (window, capture: before the editor's own
|
|
244
|
+
// listeners): the text either side of each one text node again, the caret where it was, and the
|
|
245
|
+
// block's words added marked anew (a range that ended in the half that went would cover the wrong
|
|
246
|
+
// words). The block is `edited`: drawn without them until the caret leaves it.
|
|
247
|
+
// The blocks an edit may touch: the one it happens in, those the selection reaches, the browser's
|
|
248
|
+
// or the page's across blocks (text-select/, which the browser's then holds none of).
|
|
249
|
+
function blocksOf(event) {
|
|
250
|
+
const selection = getSelection();
|
|
251
|
+
const range = selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
252
|
+
const target = event?.target instanceof Node ? event.target : null;
|
|
253
|
+
const reached = across?.texts()?.map(({ host }) => host) ?? [];
|
|
254
|
+
const hit = new Set();
|
|
255
|
+
for (const { block } of dels) {
|
|
256
|
+
if (hit.has(block)) continue;
|
|
257
|
+
if ((target && block.contains(target)) || (range && range.intersectsNode(block)) || reached.some((host) => host === block || host.contains(block) || block.contains(host))) hit.add(block);
|
|
258
|
+
}
|
|
259
|
+
return hit;
|
|
260
|
+
}
|
|
261
|
+
function takeOut(blocks) {
|
|
262
|
+
if (blocks.size === 0) return;
|
|
263
|
+
keepingAcross((ranges) => {
|
|
264
|
+
for (const entry of dels) if (blocks.has(entry.block) && entry.node.isConnected) takeMark(entry.node, ranges);
|
|
265
|
+
});
|
|
266
|
+
dels = dels.filter((entry) => !blocks.has(entry.block));
|
|
267
|
+
for (const block of blocks) {
|
|
268
|
+
edited.add(block);
|
|
269
|
+
const base = changedBlocks.get(block);
|
|
270
|
+
if (base !== undefined) markWords(block, base, false);
|
|
271
|
+
}
|
|
272
|
+
paintAdded();
|
|
273
|
+
}
|
|
274
|
+
function editing(event) {
|
|
275
|
+
if (dels.length === 0 || (event.type === "keydown" && !editsBy(event))) return;
|
|
276
|
+
takeOut(blocksOf(event));
|
|
277
|
+
}
|
|
278
|
+
// The format bar over words of such a block (its buttons keep the selection where it is).
|
|
279
|
+
function pressed(event) {
|
|
280
|
+
if (dels.length > 0 && event.target instanceof Element && event.target.closest(".ge-format")) takeOut(blocksOf(null));
|
|
281
|
+
}
|
|
282
|
+
// The caret left a block the reader edited: the review read again once what waits is saved, and drawn.
|
|
283
|
+
function moved() {
|
|
284
|
+
if (edited.size === 0) return;
|
|
285
|
+
let leaving = false;
|
|
286
|
+
for (const block of [...edited]) {
|
|
287
|
+
if (block.isConnected && holdsSelection(block)) continue;
|
|
288
|
+
edited.delete(block);
|
|
289
|
+
leaving = true;
|
|
290
|
+
}
|
|
291
|
+
if (leaving) rereadSaved(LEFT_AFTER);
|
|
292
|
+
}
|
|
293
|
+
for (const type of EDITS) addEventListener(type, editing, true);
|
|
294
|
+
addEventListener("pointerdown", pressed, true);
|
|
295
|
+
document.addEventListener("selectionchange", moved);
|
|
296
|
+
|
|
297
|
+
// ---------- what the server says ----------
|
|
298
|
+
function take(answer) {
|
|
299
|
+
if (left) return;
|
|
300
|
+
review = answer?.ok && answer.pending ? answer : null;
|
|
301
|
+
if (review && !shows()) return void keep({ quiet: true });
|
|
302
|
+
if (busy) return;
|
|
303
|
+
if (review) bar.counts(review.counts);
|
|
304
|
+
else if (Date.now() >= offering) bar.hide();
|
|
305
|
+
draw();
|
|
306
|
+
}
|
|
307
|
+
async function reread() {
|
|
308
|
+
clearTimeout(rereadTimer);
|
|
309
|
+
if (left || !started) return;
|
|
310
|
+
reading = read();
|
|
311
|
+
take(await reading);
|
|
312
|
+
}
|
|
313
|
+
// Read again `after` ms from now, once nothing waits to be saved (the reader paused).
|
|
314
|
+
function rereadSaved(after) {
|
|
315
|
+
clearTimeout(rereadTimer);
|
|
316
|
+
const again = () => {
|
|
317
|
+
if (autosave()?.state.waiting > 0) rereadTimer = setTimeout(again, REREAD_AFTER);
|
|
318
|
+
else reread();
|
|
319
|
+
};
|
|
320
|
+
rereadTimer = setTimeout(again, after);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 确认: the review ended; `quiet`, as the setting being off does it (nothing said).
|
|
324
|
+
async function keep({ quiet = false } = {}) {
|
|
325
|
+
if (!review || busy) return;
|
|
326
|
+
const stamp = review.stamp;
|
|
327
|
+
busy = true;
|
|
328
|
+
review = null;
|
|
329
|
+
draw();
|
|
330
|
+
bar.hide();
|
|
331
|
+
const answer = await postAnswer(ROUTE, token, { slug, action: "accept", stamp });
|
|
332
|
+
busy = false;
|
|
333
|
+
if (answer.ok) {
|
|
334
|
+
keepTries = 0;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Refused (the review grew meanwhile, the file was held): read again, and kept again when quiet, a few times at most.
|
|
338
|
+
if (quiet) {
|
|
339
|
+
if (++keepTries > KEEP_TRIES) return;
|
|
340
|
+
} else if (answer.reason === "changed") say(failure(answer, t("Agent 刚刚又改了文档,再看一眼")), "note");
|
|
341
|
+
else say(t("没能确认:{0}", failure(answer, t("预览服务器出错"))), "error");
|
|
342
|
+
reread();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 撤回: what waits is saved first, and nothing may wait for the reader (a conflict); then the server
|
|
346
|
+
// writes the baseline, and the page is shown again as after a write of its own (autosave showOwn: no
|
|
347
|
+
// question of what that write put back of the reader's, which the reader asked for).
|
|
348
|
+
async function revert() {
|
|
349
|
+
if (!review || busy) return;
|
|
350
|
+
const stamp = review.stamp;
|
|
351
|
+
busy = true;
|
|
352
|
+
await flush();
|
|
353
|
+
const state = autosave()?.state;
|
|
354
|
+
if (!state || state.waiting > 0 || state.syncing || state.held > 0 || state.offline) {
|
|
355
|
+
busy = false;
|
|
356
|
+
say(t("还有修改没保存或有冲突待处理,处理完再撤回"), "error");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let answer = null;
|
|
360
|
+
const shown = await autosave().showOwn(async () => {
|
|
361
|
+
answer = await postAnswer(ROUTE, token, { slug, action: "revert", stamp });
|
|
362
|
+
if (!answer.ok || answer.none) return false;
|
|
363
|
+
session?.set(undoKey, JSON.stringify({ undo: answer.undo, at: Date.now() }));
|
|
364
|
+
return true;
|
|
365
|
+
});
|
|
366
|
+
if (shown) return; // the page goes: the one shown again offers 撤销
|
|
367
|
+
busy = false;
|
|
368
|
+
if (answer?.ok) return void reread(); // nothing was under review any more
|
|
369
|
+
say(answer?.reason === "changed" ? failure(answer, t("Agent 刚刚又改了文档,再看一眼")) : t("没能撤回:{0}", failure(answer ?? {}, t("预览服务器出错"))), answer?.reason === "changed" ? "note" : "error");
|
|
370
|
+
reread();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 撤销 of a revert: the server writes back what the revert wrote over, while the file is still the revert's.
|
|
374
|
+
async function undo(id) {
|
|
375
|
+
if (busy) return;
|
|
376
|
+
busy = true;
|
|
377
|
+
offering = 0;
|
|
378
|
+
session?.remove(undoKey);
|
|
379
|
+
bar.hide();
|
|
380
|
+
await flush();
|
|
381
|
+
let answer = null;
|
|
382
|
+
const shown = await autosave().showOwn(async () => {
|
|
383
|
+
answer = await postAnswer(ROUTE, token, { slug, action: "undo", undo: id });
|
|
384
|
+
return Boolean(answer.ok);
|
|
385
|
+
});
|
|
386
|
+
if (shown) return;
|
|
387
|
+
busy = false;
|
|
388
|
+
say(t("没能撤销:{0}", failure(answer ?? {}, t("预览服务器出错"))), "error");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// A revert of this tab's, the page shown again since: 撤销 on the bar for what is left of UNDO_SHOWN.
|
|
392
|
+
function undoShown() {
|
|
393
|
+
let kept = null;
|
|
394
|
+
try { kept = JSON.parse(session?.get(undoKey) ?? "null"); } catch {}
|
|
395
|
+
const remaining = kept && typeof kept.undo === "string" ? UNDO_SHOWN - (Date.now() - kept.at) : 0;
|
|
396
|
+
if (remaining <= 0) {
|
|
397
|
+
session?.remove(undoKey);
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
offering = Date.now() + remaining;
|
|
401
|
+
bar.reverted(remaining, () => undo(kept.undo));
|
|
402
|
+
setTimeout(() => session?.remove(undoKey), remaining);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// The server's word that the review changed (another tab kept or reverted it, the Agent wrote more).
|
|
407
|
+
const heard = () => {
|
|
408
|
+
clearTimeout(rereadTimer);
|
|
409
|
+
rereadTimer = setTimeout(reread, 100);
|
|
410
|
+
};
|
|
411
|
+
addEventListener("doc-changes-changed", heard);
|
|
412
|
+
// 显示变更 switched (设置, another page of the origin): off, what is under review is kept and nothing drawn; on, read again.
|
|
413
|
+
onPreferences(() => {
|
|
414
|
+
if (!started || left) return;
|
|
415
|
+
if (!shows()) {
|
|
416
|
+
if (review) keep({ quiet: true });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
reread();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
provide("changes", () => ({ pending: Boolean(review), counts: review?.counts ?? null, filled: filled.size, words: dels.length, ghosts: ghosts.count, edited: edited.size }));
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
/** The page is ready: what was read is drawn (the bar, or 撤销 after a revert of this tab's). */
|
|
426
|
+
async start() {
|
|
427
|
+
if (started) return;
|
|
428
|
+
started = true;
|
|
429
|
+
undoShown();
|
|
430
|
+
take(await reading);
|
|
431
|
+
},
|
|
432
|
+
/** A commit of the page's was taken in: while the bar shows, the review is read again once the reader pauses. */
|
|
433
|
+
committed(receipt) {
|
|
434
|
+
if (!receipt?.wrote || !review || !started) return;
|
|
435
|
+
rereadSaved(REREAD_AFTER);
|
|
436
|
+
},
|
|
437
|
+
/** The page goes. */
|
|
438
|
+
leave() {
|
|
439
|
+
left = true;
|
|
440
|
+
clearTimeout(rereadTimer);
|
|
441
|
+
removeEventListener("doc-changes-changed", heard);
|
|
442
|
+
for (const type of EDITS) removeEventListener(type, editing, true);
|
|
443
|
+
removeEventListener("pointerdown", pressed, true);
|
|
444
|
+
document.removeEventListener("selectionchange", moved);
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { wordDiff } from "../merge.mjs";
|
|
2
|
+
|
|
3
|
+
// The words an Agent changed in a text block, as the page marks them (docs/editor.md "Agent's
|
|
4
|
+
// changes"): the word diff of the block's text in the baseline against its text on the page
|
|
5
|
+
// (merge.mjs wordDiff), as places in the page's text.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What `now` (the block's text on the page) has that `base` did not and where `base`'s words were
|
|
9
|
+
* taken out of it: { ins: [[from, to]] (ranges of `now`), cuts: [{ at, text }] (a place in `now`,
|
|
10
|
+
* what was taken out there; one where several runs meet) }.
|
|
11
|
+
*/
|
|
12
|
+
export function wordMarks(base, now) {
|
|
13
|
+
const ins = [];
|
|
14
|
+
const cuts = [];
|
|
15
|
+
let at = 0;
|
|
16
|
+
for (const { tag, text } of wordDiff(base, now)) {
|
|
17
|
+
if (tag === "del") {
|
|
18
|
+
const last = cuts.at(-1);
|
|
19
|
+
if (last?.at === at) last.text += text;
|
|
20
|
+
else cuts.push({ at, text });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (tag === "ins") ins.push([at, at + text.length]);
|
|
24
|
+
at += text.length;
|
|
25
|
+
}
|
|
26
|
+
return { ins, cuts };
|
|
27
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Pasted colors onto the page's nine (feishu-lite model/color-match.ts and css-color.ts, after Feishu
|
|
2
2
|
// 855470 · bn / mG / WW, 99121 · FB, 768176 CIEDE2000; Notion keeps only its own colors). Pure, no
|
|
3
3
|
// DOM. A color is given as the CSS text of a style (named, #hex, rgb[a](), hsl[a]()).
|
|
4
|
-
// exact a color equal to one of the page's (the kit's light values
|
|
5
|
-
// doc-highlight-*) is that one: what a page whose styles are not meant (any web page)
|
|
4
|
+
// exact a color equal to one of the page's (the kit's light values: doc-text-*, and
|
|
5
|
+
// doc-highlight-* as a block's fill or a run of words') is that one: what a page whose styles are not meant (any web page)
|
|
6
6
|
// keeps
|
|
7
7
|
// nearest for sources whose styles are meant (Word, Google Docs, Notion): text — too dark (HSV
|
|
8
8
|
// v ≤ 0.3) none, unsaturated (s < 0.08) gray, a pure CSS color by a fixed table, else the
|
|
@@ -14,7 +14,7 @@ import { BODY, COLORS } from "../../../../shared/colors.mjs";
|
|
|
14
14
|
|
|
15
15
|
// The page's light values (src/shared/colors.mjs).
|
|
16
16
|
const TEXT = COLORS.map(({ name, text }) => [name, text[0]]);
|
|
17
|
-
const BACKGROUND = COLORS.
|
|
17
|
+
const BACKGROUND = COLORS.flatMap(({ name, fill, mark }) => [[name, fill[0]], [name, mark[0]]]);
|
|
18
18
|
const BODY_TEXT = BODY.text[0];
|
|
19
19
|
const BODY_BACKGROUND = BODY.background[0];
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ import { t } from "../../../shared/i18n/index.mjs";
|
|
|
32
32
|
// Ctrl+V reads the clipboard in Notion's order: the page's own blocks (or text), else another
|
|
33
33
|
// program's content (import/read.mjs) as blocks, one formatted line, or plain text; picture
|
|
34
34
|
// files (a screenshot) become images, unless they come with Office's or a table's HTML (Excel
|
|
35
|
-
// puts a picture of the cells beside them).
|
|
35
|
+
// puts a picture of the cells beside them), right after the block, where blocks pasted there go.
|
|
36
36
|
// With the caret in a text block: one line goes in at the caret, keeping the format bar's marks
|
|
37
37
|
// where the block takes them, its own alone (not the caret's: notion-lite's text pasted) (text
|
|
38
38
|
// copied in a block of the page with its line breaks); blocks
|
|
@@ -75,7 +75,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
75
75
|
copyText(event, selection.getRangeAt(0));
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const picked = editing() ? blocks().picked : [];
|
|
78
|
+
const picked = editing() ? blocks().folds.takenWith(blocks().picked) : []; // a folded heading with what it hides (fold/)
|
|
79
79
|
if (picked.length === 0) return;
|
|
80
80
|
event.preventDefault();
|
|
81
81
|
// A list's item selected on its own carries its list's kind (notion-lite's item is a block of its
|
|
@@ -86,7 +86,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
86
86
|
});
|
|
87
87
|
const refused = picked.find((_, index) => !described[index]);
|
|
88
88
|
if (refused) {
|
|
89
|
-
say(refusalOf(refused), "note", 6000);
|
|
89
|
+
say(refusalOf(unmade(refused)), "note", 6000); // the block in it the page cannot make, as a selection across blocks says it
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
// A cut takes the blocks off the page, where a card's copy is of its unit: what is made only of its unit is refused, not lost.
|
|
@@ -261,13 +261,30 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
-
// Files pasted by `at`: a block
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
264
|
+
// Files pasted by `at`: a block for the first of them the first component takes, in the order of the
|
|
265
|
+
// page objects (`acceptsFile`, `fromFiles`: notion-lite's fileBlockDef; a picture an image, any other
|
|
266
|
+
// file a file block). Where it goes is the paste's (notion-lite's pasteFiles, moveBlocks(ids, { parent:
|
|
267
|
+
// anchor.parent, after })): right after the block of `at` (unitElement: a new code block's code stands
|
|
268
|
+
// for the block), whole, where a block pasted there goes (placeFor: the first box out that takes it, a
|
|
269
|
+
// list or quote that does not splitting after the block), one undo step (`put`, handed to the
|
|
270
|
+
// component, which makes the block and fills it). Returns whether one did.
|
|
271
|
+
const fromFiles = (at, files) => {
|
|
272
|
+
const unit = unitElement(at);
|
|
273
|
+
if (!unit?.closest("main")) return false;
|
|
274
|
+
const put = (block) => {
|
|
275
|
+
const place = placeFor(unit, [block.getAttribute("data-ge-unit-kind")]);
|
|
276
|
+
if (!place) {
|
|
277
|
+
say(t("这里放不下这种块。"));
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
struct(place.parents, [], () => ({ touched: [], focus: null, created: placeAfter(place, [block]) }));
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
return pageObjects().some((page) => {
|
|
284
|
+
const file = page.acceptsFile && [...files].find(page.acceptsFile);
|
|
285
|
+
return Boolean(file) && page.fromFiles(put, [file]);
|
|
286
|
+
});
|
|
287
|
+
};
|
|
271
288
|
|
|
272
289
|
// ---------- blocks into the page ----------
|
|
273
290
|
|
|
@@ -284,7 +301,9 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
284
301
|
let stays = false;
|
|
285
302
|
let box = anchor.parentElement;
|
|
286
303
|
while (!takes(box)) {
|
|
287
|
-
|
|
304
|
+
// A container made on the page and not written yet (a list just made of "- ", whose
|
|
305
|
+
// contenteditable OUTER passes over) is the block around what it holds, as the file's is.
|
|
306
|
+
const outer = box?.closest(`${OUTER}, [data-ge-new][data-ge-box]`);
|
|
288
307
|
if (!outer?.closest("main")) return null;
|
|
289
308
|
parents.push(box);
|
|
290
309
|
// A list or a quote whose box does not take them splits after the anchor.
|
|
@@ -632,7 +651,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
632
651
|
if (!data || (!host && picked.length === 0)) return;
|
|
633
652
|
event.preventDefault();
|
|
634
653
|
const anchor = host ?? picked.at(-1);
|
|
635
|
-
// Picture or other files (a screenshot): an image or a file block
|
|
654
|
+
// Picture or other files (a screenshot): an image or a file block after the block, unless they
|
|
636
655
|
// come with the page's own blocks, or Office's or a table's HTML (Excel's picture of its cells).
|
|
637
656
|
const files = data.files;
|
|
638
657
|
if (files?.length > 0 && !ownData(data)) {
|
|
@@ -681,7 +700,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
|
|
|
681
700
|
return;
|
|
682
701
|
}
|
|
683
702
|
if (!host.isConnected || !host.contains(range.startContainer)) return;
|
|
684
|
-
// An address alone: a link wherever the block takes them (a cell, a
|
|
703
|
+
// An address alone: a link wherever the block takes them (a cell, a caption too).
|
|
685
704
|
if (href && can(host, "links")) {
|
|
686
705
|
const link = document.createElement("a");
|
|
687
706
|
link.setAttribute("href", page?.href ?? href);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeText } from "../../../../shared/escape.mjs";
|
|
2
|
-
import { newContainer, newHost, pastedLines } from "../../core.mjs";
|
|
2
|
+
import { heldIn, newContainer, newHost, pastedLines } from "../../core.mjs";
|
|
3
3
|
import { pageBuild } from "../../../components/builders.mjs";
|
|
4
|
-
import { BLOCK_TYPES, COPIED_AS_ITSELF, TEXT_TYPES, TOGGLES, UNSAID, accepts, clipboardHooks, clipboardOf, isLead, sizeValue } from "../../../components/registry.mjs";
|
|
4
|
+
import { BLOCK_TYPES, COPIED_AS_ITSELF, HOLDS_ONE, TEXT_TYPES, TOGGLES, UNSAID, accepts, clipboardHooks, clipboardOf, isLead, sizeValue } from "../../../components/registry.mjs";
|
|
5
5
|
import { BLOCK_COLORS } from "../../../components/colors.mjs";
|
|
6
6
|
import { MAX_CELL_HTML } from "../../../components/table/index.mjs";
|
|
7
7
|
import { copied, copyOf, reportOf } from "./describe.mjs";
|
|
@@ -62,7 +62,8 @@ export function checkBlocks(blocks) {
|
|
|
62
62
|
*/
|
|
63
63
|
export function unmadeCopy(doc, blocks) {
|
|
64
64
|
for (const block of blocks) {
|
|
65
|
-
|
|
65
|
+
// A copy made of its data is made of what that holds too (an instance's slots: a card in one).
|
|
66
|
+
const found = block.type === "copy" ? ((doc && copied(doc, block)) ? null : block.data ? unmadeCopy(doc, heldBy(block.data)) : block) : unmadeCopy(doc, heldBy(block));
|
|
66
67
|
if (found) return found;
|
|
67
68
|
}
|
|
68
69
|
return null;
|
|
@@ -165,8 +166,16 @@ export function build(doc, block, options = {}) {
|
|
|
165
166
|
};
|
|
166
167
|
const made = clipboardOf(block.type).build ?? pageBuild(block.type);
|
|
167
168
|
if (made) return made(what);
|
|
168
|
-
const
|
|
169
|
-
element
|
|
169
|
+
const { box, leaf } = BLOCK_TYPES[block.type] ?? {};
|
|
170
|
+
const element = box ? what.box() : what.host();
|
|
171
|
+
element.append(...(box ? what.inner(block.blocks) : what.inline(block.html)));
|
|
172
|
+
// A box that always holds a block (holdsOne: a highlight block, a callout) described with none (one
|
|
173
|
+
// the file has empty, copied or cut) holds an empty block of its `leaf` (a paragraph, as a new one
|
|
174
|
+
// made from "/" holds), after its lead (a highlight block's icon), as one a change empties does
|
|
175
|
+
// (core.mjs tidyEmptied) and a disclosure's content made again (details/page.mjs): a block to write
|
|
176
|
+
// in, and one the commit writes, where a box of no blocks is written by none (core.mjs nodeOf) and
|
|
177
|
+
// was gone at the next reload.
|
|
178
|
+
if (leaf && HOLDS_ONE.has(box) && heldIn(element).length === 0) element.append(what.host(leaf));
|
|
170
179
|
return what.colored(element);
|
|
171
180
|
}
|
|
172
181
|
|