@atom-forge/brickee 0.0.1 → 0.0.3

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 (72) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README-AI.md +34 -28
  3. package/dist/Editor.svelte +713 -867
  4. package/dist/Editor.svelte.d.ts +0 -3
  5. package/dist/LinkPrompt.svelte +1 -14
  6. package/dist/Preview.svelte +27 -58
  7. package/dist/Preview.svelte.d.ts +2 -7
  8. package/dist/PropsBar.svelte +75 -249
  9. package/dist/PropsBar.svelte.d.ts +0 -3
  10. package/dist/code.d.ts +12 -0
  11. package/dist/code.js +15 -0
  12. package/dist/dom.d.ts +8 -0
  13. package/dist/dom.js +9 -0
  14. package/dist/editor/BlockMenu.svelte +102 -0
  15. package/dist/editor/BlockMenu.svelte.d.ts +32 -0
  16. package/dist/editor/FormatBar.svelte +41 -0
  17. package/dist/editor/FormatBar.svelte.d.ts +16 -0
  18. package/dist/editor/drag.svelte.d.ts +63 -0
  19. package/dist/editor/drag.svelte.js +95 -0
  20. package/dist/editor/history.d.ts +24 -0
  21. package/dist/editor/history.js +86 -0
  22. package/dist/editor/sweep.svelte.d.ts +21 -0
  23. package/dist/editor/sweep.svelte.js +56 -0
  24. package/dist/html.d.ts +12 -0
  25. package/dist/html.js +206 -0
  26. package/dist/i18n.js +1 -1
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +4 -3
  29. package/dist/kit/Empty.svelte +4 -15
  30. package/dist/kit/Frame.svelte +7 -37
  31. package/dist/locales/hu.js +6 -8
  32. package/dist/markdown.d.ts +20 -6
  33. package/dist/markdown.js +380 -85
  34. package/dist/model.js +2 -3
  35. package/dist/plugins/Gap.svelte +5 -8
  36. package/dist/plugins/GapPreview.svelte +4 -6
  37. package/dist/plugins/Map.svelte +2 -17
  38. package/dist/plugins/MapPreview.svelte +8 -10
  39. package/dist/plugins/Table.svelte +233 -282
  40. package/dist/plugins/TablePreview.svelte +28 -28
  41. package/dist/plugins/YouTube.svelte +3 -31
  42. package/dist/plugins/YouTubePreview.svelte +45 -43
  43. package/dist/plugins/index.js +15 -2
  44. package/dist/schema.js +1 -1
  45. package/dist/types.d.ts +27 -10
  46. package/dist/types.js +8 -1
  47. package/dist/view.js +1 -1
  48. package/docs/en/bundled-plugins.md +1 -1
  49. package/docs/en/documents-and-markdown.md +24 -19
  50. package/docs/en/editor-and-preview.md +7 -10
  51. package/docs/en/getting-started.md +14 -2
  52. package/docs/en/host-contexts.md +26 -132
  53. package/docs/en/index.md +5 -5
  54. package/docs/en/recipes-and-pitfalls.md +12 -11
  55. package/docs/en/server-side.md +0 -17
  56. package/docs/en/upgrading.md +34 -4
  57. package/docs/en/writing-plugins.md +147 -21
  58. package/docs/hu/bundled-plugins.md +1 -1
  59. package/docs/hu/documents-and-markdown.md +24 -19
  60. package/docs/hu/editor-and-preview.md +7 -10
  61. package/docs/hu/getting-started.md +14 -2
  62. package/docs/hu/host-contexts.md +26 -132
  63. package/docs/hu/index.md +5 -5
  64. package/docs/hu/recipes-and-pitfalls.md +12 -11
  65. package/docs/hu/server-side.md +0 -17
  66. package/docs/hu/upgrading.md +34 -4
  67. package/docs/hu/writing-plugins.md +147 -21
  68. package/package.json +14 -2
  69. package/dist/ImagePicker.svelte +0 -131
  70. package/dist/ImagePicker.svelte.d.ts +0 -13
  71. package/dist/media.d.ts +0 -17
  72. package/dist/media.js +0 -12
package/dist/dom.js CHANGED
@@ -8,6 +8,15 @@ export function rich(node, value) {
8
8
  }
9
9
  };
10
10
  }
11
+ /**
12
+ * Svelte action: moves the element to the end of the body. A floating menu positioned with `position: fixed` is then
13
+ * placed against the viewport even when the editor sits in a transformed parent (an animated drawer, a dialog), where
14
+ * `fixed` would be relative to that parent.
15
+ */
16
+ export function portal(node) {
17
+ document.body.appendChild(node);
18
+ return { destroy: () => node.remove() };
19
+ }
11
20
  export function serialize(frag) {
12
21
  const d = document.createElement('div');
13
22
  d.append(frag);
@@ -0,0 +1,102 @@
1
+ <!-- A block menu of the editor: the slash menu (block types to insert) and the turn-into menu (what the selected
2
+ blocks can become). Compact one-line rows: icon, label, and the hint after it in grey. The editor keeps the state
3
+ (which row is active, where the menu is) and handles the keys. -->
4
+ <script lang="ts" module>
5
+ import type { Icon } from '../palette';
6
+
7
+ export interface MenuRow {
8
+ key: string;
9
+ icon: Icon;
10
+ label: string;
11
+ hint: string;
12
+ /** The hint as a warning (e.g. "Loses formatting"). */
13
+ warn?: boolean;
14
+ /** A destructive row (delete), in red. */
15
+ danger?: boolean;
16
+ /** The current type of the selection: a check mark. */
17
+ current?: boolean;
18
+ /** A separator line above this row. */
19
+ gap?: boolean;
20
+ }
21
+ </script>
22
+
23
+ <script lang="ts">
24
+ import { Check } from '@lucide/svelte';
25
+ import { portal } from '../dom';
26
+
27
+ let {
28
+ rows,
29
+ index,
30
+ x,
31
+ y,
32
+ up,
33
+ turn = false,
34
+ caption,
35
+ label,
36
+ onpick,
37
+ onhover
38
+ }: {
39
+ rows: MenuRow[];
40
+ index: number;
41
+ x: number;
42
+ y: number;
43
+ /** Opens upwards from `y` (no room below). */
44
+ up: boolean;
45
+ /** The turn-into menu: a little narrower and taller. */
46
+ turn?: boolean;
47
+ caption?: string;
48
+ label: string;
49
+ onpick: (n: number) => void;
50
+ onhover: (n: number) => void;
51
+ } = $props();
52
+
53
+ let menu: HTMLDivElement;
54
+ // Keep the highlighted row visible while moving through the menu with the arrow keys.
55
+ $effect(() => {
56
+ void index;
57
+ menu?.querySelector('.active')?.scrollIntoView({ block: 'nearest' });
58
+ });
59
+ </script>
60
+
61
+ <!-- `menu`, `turn` and `active` are hooks (closing on an outside click, scrolling the active row into view), not styles.
62
+ It lives in the body (portal), so it keeps the editor's type size and colour itself. -->
63
+ <div
64
+ bind:this={menu}
65
+ use:portal
66
+ class="menu fixed z-30 overflow-auto text-[17px] leading-[1.65] text-surface-contrast rounded-xl border border-frame bg-surface p-1.5 shadow-[0_10px_36px_rgb(0_0_0/0.2)] {turn
67
+ ? 'turn max-h-[340px] w-60'
68
+ : 'max-h-[300px] w-[260px]'} {up ? '-translate-y-full' : ''}"
69
+ style:left="{x}px"
70
+ style:top="{y}px"
71
+ role="listbox"
72
+ aria-label={label}
73
+ >
74
+ {#if caption}
75
+ <div class="px-2 pt-1 pb-1.5 font-[system-ui] text-[11px] leading-[normal] font-semibold tracking-[0.06em] text-muted-contrast uppercase">{caption}</div>
76
+ {/if}
77
+ {#each rows as row, n (row.key)}
78
+ {#if row.gap}<div class="mx-1 my-1.5 h-px bg-frame"></div>{/if}
79
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
80
+ <div
81
+ class="item flex cursor-pointer items-center gap-2 rounded-md px-1.5 py-[3px] {n === index ? 'active bg-accent/14' : ''} {row.danger ? 'text-[#dc2626]' : ''}"
82
+ role="option"
83
+ tabindex="-1"
84
+ aria-selected={n === index}
85
+ onmousedown={(e) => {
86
+ e.preventDefault();
87
+ onpick(n);
88
+ }}
89
+ onmouseenter={() => onhover(n)}
90
+ >
91
+ <span class="grid size-6 flex-none place-items-center rounded-md border border-frame {row.danger ? 'text-[#dc2626]' : 'text-surface-contrast'}"
92
+ ><row.icon size={14} /></span
93
+ >
94
+ <span class="flex min-w-0 items-baseline gap-1.5 overflow-hidden whitespace-nowrap"
95
+ ><b class="font-[system-ui] text-[13px] leading-[normal] font-medium">{row.label}</b><small
96
+ class="overflow-hidden text-[11px] text-ellipsis {row.warn ? 'text-[#b45309]' : 'text-muted-contrast'}">{row.hint}</small
97
+ ></span
98
+ >
99
+ {#if row.current}<span class="ml-auto flex text-accent"><Check size={16} /></span>{/if}
100
+ </div>
101
+ {/each}
102
+ </div>
@@ -0,0 +1,32 @@
1
+ import type { Icon } from '../palette';
2
+ export interface MenuRow {
3
+ key: string;
4
+ icon: Icon;
5
+ label: string;
6
+ hint: string;
7
+ /** The hint as a warning (e.g. "Loses formatting"). */
8
+ warn?: boolean;
9
+ /** A destructive row (delete), in red. */
10
+ danger?: boolean;
11
+ /** The current type of the selection: a check mark. */
12
+ current?: boolean;
13
+ /** A separator line above this row. */
14
+ gap?: boolean;
15
+ }
16
+ type $$ComponentProps = {
17
+ rows: MenuRow[];
18
+ index: number;
19
+ x: number;
20
+ y: number;
21
+ /** Opens upwards from `y` (no room below). */
22
+ up: boolean;
23
+ /** The turn-into menu: a little narrower and taller. */
24
+ turn?: boolean;
25
+ caption?: string;
26
+ label: string;
27
+ onpick: (n: number) => void;
28
+ onhover: (n: number) => void;
29
+ };
30
+ declare const BlockMenu: import("svelte").Component<$$ComponentProps, {}, "">;
31
+ type BlockMenu = ReturnType<typeof BlockMenu>;
32
+ export default BlockMenu;
@@ -0,0 +1,41 @@
1
+ <!-- The formatting bar above a text selection: bold, italic… link, plain text. -->
2
+ <script lang="ts">
3
+ import { portal } from '../dom';
4
+ import type { Icon } from '../palette';
5
+
6
+ let {
7
+ x,
8
+ y,
9
+ tools,
10
+ label,
11
+ onpick
12
+ }: {
13
+ /** The middle of the selection's top edge; the bar sits centred above it. */
14
+ x: number;
15
+ y: number;
16
+ tools: { cmd: string; icon: Icon; title: string }[];
17
+ label: string;
18
+ onpick: (cmd: string) => void;
19
+ } = $props();
20
+ </script>
21
+
22
+ <!-- in the body (portal): placed against the viewport even inside a transformed parent -->
23
+ <div
24
+ use:portal
25
+ class="fixed z-20 flex -translate-x-1/2 translate-y-[calc(-100%-8px)] gap-0.5 rounded-[10px] border border-frame bg-surface p-1 shadow-[0_6px_24px_rgb(0_0_0/0.18)]"
26
+ style:left="{x}px"
27
+ style:top="{y}px"
28
+ role="toolbar"
29
+ aria-label={label}
30
+ >
31
+ {#each tools as t (t.cmd)}
32
+ <button
33
+ type="button"
34
+ class="grid h-[30px] min-w-[30px] cursor-pointer place-items-center rounded-md border-0 bg-transparent text-surface-contrast hover:bg-[rgb(120_120_140/0.14)]"
35
+ title={t.title}
36
+ aria-label={t.title}
37
+ onmousedown={(e) => e.preventDefault()}
38
+ onclick={() => onpick(t.cmd)}><t.icon size={16} /></button
39
+ >
40
+ {/each}
41
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { Icon } from '../palette';
2
+ type $$ComponentProps = {
3
+ /** The middle of the selection's top edge; the bar sits centred above it. */
4
+ x: number;
5
+ y: number;
6
+ tools: {
7
+ cmd: string;
8
+ icon: Icon;
9
+ title: string;
10
+ }[];
11
+ label: string;
12
+ onpick: (cmd: string) => void;
13
+ };
14
+ declare const FormatBar: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type FormatBar = ReturnType<typeof FormatBar>;
16
+ export default FormatBar;
@@ -0,0 +1,63 @@
1
+ import type { StoredBlock } from '../types';
2
+ export type DragSource = {
3
+ /** The token of the editor the blocks come from. */
4
+ editor: string;
5
+ ids: string[];
6
+ /** The blocks in their stored form, for another editor to copy in. */
7
+ payload: StoredBlock[];
8
+ /** Their rows (a drop target inside one of them is inside the dragged blocks). */
9
+ rows: HTMLElement[];
10
+ /** Takes the blocks out of their editor (after another editor copied them in). */
11
+ remove: () => void;
12
+ };
13
+ export type Hub = {
14
+ src: DragSource | null;
15
+ target: {
16
+ editor: string;
17
+ over: string;
18
+ after: boolean;
19
+ } | null;
20
+ /** The editor worked in last. */
21
+ active: string | null;
22
+ };
23
+ /** The hub of this tree of editors: the parent editor's, or a new one for a top editor. Call in component init. */
24
+ export declare function useHub(): Hub;
25
+ export interface DragHost {
26
+ /** This editor's token (a string, not an object: $state would proxy an object and break identity checks). */
27
+ token: string;
28
+ hub: Hub;
29
+ root(): HTMLElement;
30
+ ownRow(node: Element | null): HTMLElement | null;
31
+ /** The blocks a drag from this block moves: the block selection if the block is in it, otherwise the block. */
32
+ moving(id: string): string[];
33
+ /** The stored form of these blocks. */
34
+ payload(ids: string[]): StoredBlock[];
35
+ /** Takes these blocks out (an undo step). */
36
+ remove(ids: string[]): void;
37
+ /** Can this editor hold these blocks (the plugins it offers)? */
38
+ holds(payload: StoredBlock[]): boolean;
39
+ /** Moves its own blocks before or after another one (an undo step). */
40
+ move(ids: string[], over: string, after: boolean): void;
41
+ /** Puts another editor's blocks before or after one of its own (an undo step). */
42
+ insert(payload: StoredBlock[], over: string, after: boolean): void;
43
+ /** Does a plugin this editor offers take files? */
44
+ takesFiles(): boolean;
45
+ /** Files dropped from outside the page, before or after one of its blocks. */
46
+ files(files: File[], over: string, after: boolean): void;
47
+ }
48
+ export declare function createDrag(host: DragHost): {
49
+ /** On a handle's dragstart. */
50
+ start(e: DragEvent, id: string): void;
51
+ /** On the editor's dragover. */
52
+ over(e: DragEvent): void;
53
+ /** On the editor's drop. */
54
+ drop(e: DragEvent): void;
55
+ /** On dragend: nothing is dragged any more. */
56
+ end(): void;
57
+ /** On the editor's dragleave: files dragged out of it leave no drop line behind (they have no dragend here). */
58
+ leave(e: DragEvent): void;
59
+ /** Is this block of this editor being dragged? */
60
+ moves: (id: string) => boolean;
61
+ /** The drop line before (or after) this block. */
62
+ mark: (id: string, after: boolean) => boolean;
63
+ };
@@ -0,0 +1,95 @@
1
+ // Dragging blocks by their handle, within an editor and between an editor and the editors nested in its blocks (and
2
+ // back). The editors of one tree share a hub through a context: what is being dragged, where it would land, and which
3
+ // editor was worked in last (only that one keeps a block selection).
4
+ import { getContext, setContext } from 'svelte';
5
+ const KEY = 'brickee:hub';
6
+ /** The hub of this tree of editors: the parent editor's, or a new one for a top editor. Call in component init. */
7
+ export function useHub() {
8
+ const own = $state({ src: null, target: null, active: null });
9
+ const hub = getContext(KEY) ?? own;
10
+ setContext(KEY, hub);
11
+ return hub;
12
+ }
13
+ export function createDrag(host) {
14
+ const { hub, token } = host;
15
+ /** Can the dragged blocks land in this editor? Not inside themselves, and only plugins this editor offers. */
16
+ function accepts(src) {
17
+ if (src.editor === token)
18
+ return true;
19
+ if (src.rows.some((r) => r.contains(host.root())))
20
+ return false;
21
+ return host.holds(src.payload);
22
+ }
23
+ return {
24
+ /** On a handle's dragstart. */
25
+ start(e, id) {
26
+ const ids = host.moving(id);
27
+ hub.src = {
28
+ editor: token,
29
+ ids,
30
+ payload: host.payload(ids),
31
+ rows: ids.map((x) => host.root().querySelector(`[data-block="${x}"]`)).filter(Boolean),
32
+ remove: () => host.remove(ids)
33
+ };
34
+ hub.target = null;
35
+ if (!e.dataTransfer)
36
+ return;
37
+ e.dataTransfer.effectAllowed = 'move';
38
+ e.dataTransfer.setData('text/plain', ' ');
39
+ const row = e.currentTarget.closest('.row');
40
+ if (row)
41
+ e.dataTransfer.setDragImage(row, 0, 0);
42
+ },
43
+ /** On the editor's dragover. */
44
+ over(e) {
45
+ // A nested editor deeper in the tree already took this event.
46
+ if (e.defaultPrevented)
47
+ return;
48
+ // Files from outside the page, when a plugin here takes files.
49
+ const files = !hub.src && !!e.dataTransfer?.types.includes('Files') && host.takesFiles();
50
+ if (!hub.src && !files)
51
+ return;
52
+ const row = host.ownRow(e.target);
53
+ if (!row || (hub.src && !accepts(hub.src)))
54
+ return;
55
+ e.preventDefault();
56
+ const r = row.getBoundingClientRect();
57
+ hub.target = { editor: token, over: row.dataset.block, after: e.clientY > r.top + r.height / 2 };
58
+ },
59
+ /** On the editor's drop. */
60
+ drop(e) {
61
+ const src = hub.src;
62
+ const at = hub.target;
63
+ if (at?.editor !== token)
64
+ return;
65
+ e.preventDefault();
66
+ hub.src = hub.target = null;
67
+ if (!src) {
68
+ const files = [...(e.dataTransfer?.files ?? [])];
69
+ if (files.length)
70
+ host.files(files, at.over, at.after);
71
+ return;
72
+ }
73
+ if (src.ids.includes(at.over))
74
+ return;
75
+ if (src.editor === token)
76
+ return host.move(src.ids, at.over, at.after);
77
+ // From another editor: copy in through the stored form, then remove at the source.
78
+ host.insert(src.payload, at.over, at.after);
79
+ src.remove();
80
+ },
81
+ /** On dragend: nothing is dragged any more. */
82
+ end() {
83
+ hub.src = hub.target = null;
84
+ },
85
+ /** On the editor's dragleave: files dragged out of it leave no drop line behind (they have no dragend here). */
86
+ leave(e) {
87
+ if (!hub.src && hub.target?.editor === token && !host.root().contains(e.relatedTarget))
88
+ hub.target = null;
89
+ },
90
+ /** Is this block of this editor being dragged? */
91
+ moves: (id) => hub.src?.editor === token && hub.src.ids.includes(id),
92
+ /** The drop line before (or after) this block. */
93
+ mark: (id, after) => hub.target?.editor === token && hub.target.over === id && hub.target.after === after && !hub.src?.ids.includes(id)
94
+ };
95
+ }
@@ -0,0 +1,24 @@
1
+ export declare class History<S> {
2
+ #private;
3
+ private take;
4
+ private apply;
5
+ private limit;
6
+ private settle;
7
+ /**
8
+ * @param take a snapshot of the editor now
9
+ * @param apply puts the editor back into a snapshot
10
+ * @param limit how many steps are kept
11
+ * @param settle how long (ms) typing waits before its step is closed
12
+ */
13
+ constructor(take: () => S, apply: (s: S) => void, limit?: number, settle?: number);
14
+ /** Call on every text edit: consecutive typing collapses into one undo step. */
15
+ typed(): void;
16
+ /** Wrap every structural change: pushes an undo step. */
17
+ mutate(fn: () => void): void;
18
+ /** Drops the undo and redo steps (a document loaded from outside). */
19
+ forget(): void;
20
+ undo(): void;
21
+ redo(): void;
22
+ /** Stops the pending timer (the editor is going away). */
23
+ dispose(): void;
24
+ }
@@ -0,0 +1,86 @@
1
+ // The editor's undo history: snapshots of the document (and where the caret was), one step per structural change,
2
+ // consecutive typing collapsed into one step.
3
+ export class History {
4
+ take;
5
+ apply;
6
+ limit;
7
+ settle;
8
+ #past = [];
9
+ #future = [];
10
+ #last;
11
+ #pending = false;
12
+ #timer;
13
+ /**
14
+ * @param take a snapshot of the editor now
15
+ * @param apply puts the editor back into a snapshot
16
+ * @param limit how many steps are kept
17
+ * @param settle how long (ms) typing waits before its step is closed
18
+ */
19
+ constructor(take, apply, limit = 300, settle = 600) {
20
+ this.take = take;
21
+ this.apply = apply;
22
+ this.limit = limit;
23
+ this.settle = settle;
24
+ this.#last = take();
25
+ }
26
+ #push(s) {
27
+ this.#past.push(s);
28
+ if (this.#past.length > this.limit)
29
+ this.#past.shift();
30
+ this.#future = [];
31
+ }
32
+ #settle() {
33
+ clearTimeout(this.#timer);
34
+ this.#pending = false;
35
+ }
36
+ /** Call on every text edit: consecutive typing collapses into one undo step. */
37
+ typed() {
38
+ if (!this.#pending) {
39
+ this.#push(this.#last);
40
+ this.#pending = true;
41
+ }
42
+ clearTimeout(this.#timer);
43
+ this.#timer = setTimeout(() => {
44
+ this.#pending = false;
45
+ this.#last = this.take();
46
+ }, this.settle);
47
+ }
48
+ /** Wrap every structural change: pushes an undo step. */
49
+ mutate(fn) {
50
+ this.#settle();
51
+ this.#push(this.take());
52
+ fn();
53
+ this.#last = this.take();
54
+ }
55
+ /** Drops the undo and redo steps (a document loaded from outside). */
56
+ forget() {
57
+ this.#settle();
58
+ this.#past = [];
59
+ this.#future = [];
60
+ this.#last = this.take();
61
+ }
62
+ undo() {
63
+ this.#settle();
64
+ const s = this.#past.pop();
65
+ if (!s)
66
+ return;
67
+ this.#future.push(this.take());
68
+ this.#restore(s);
69
+ }
70
+ redo() {
71
+ this.#settle();
72
+ const s = this.#future.pop();
73
+ if (!s)
74
+ return;
75
+ this.#past.push(this.take());
76
+ this.#restore(s);
77
+ }
78
+ #restore(s) {
79
+ this.#last = s;
80
+ this.apply(s);
81
+ }
82
+ /** Stops the pending timer (the editor is going away). */
83
+ dispose() {
84
+ clearTimeout(this.#timer);
85
+ }
86
+ }
@@ -0,0 +1,21 @@
1
+ export interface SweepHost {
2
+ /** The editor's root element. */
3
+ root(): HTMLElement;
4
+ /** The editor's own row (not a nested editor's) that holds this node, or null. */
5
+ ownRow(node: Element | null): HTMLElement | null;
6
+ readonly(): boolean;
7
+ /** Select the blocks from `anchor` to `head`. */
8
+ select(anchor: string, head: string): void;
9
+ /** Drop the block selection (back to text selection). */
10
+ clear(): void;
11
+ /** Take the focus from the text field, to the root (the block selection's keys). */
12
+ focusRoot(): void;
13
+ }
14
+ export declare function createSweep(host: SweepHost): {
15
+ /** True while whole blocks are being swept (the browser must not extend its own selection meanwhile). */
16
+ readonly sweeping: boolean;
17
+ /** On the editor's mousedown: a press in a block's text may start a sweep. */
18
+ start(e: MouseEvent): void;
19
+ /** Stops a sweep and its listeners (also when the editor goes away). */
20
+ end: () => void;
21
+ };
@@ -0,0 +1,56 @@
1
+ // A text selection dragged out of its block turns into a selection of whole blocks (each block is its own editable
2
+ // field, so the browser cannot select text across them). Nested editors run the same logic, so a sweep that leaves a
3
+ // box is taken over by the editor around it, with the box as one block.
4
+ export function createSweep(host) {
5
+ let sweeping = $state(false);
6
+ let from = null;
7
+ function move(e) {
8
+ const start = host.ownRow(from);
9
+ const here = document.elementFromPoint(e.clientX, e.clientY);
10
+ if (!start || !here)
11
+ return;
12
+ const root = host.root();
13
+ const row = root.contains(here) ? host.ownRow(here) : null;
14
+ // Left this editor, or came back to the starting block: back to plain text selection.
15
+ if (!root.contains(here) || row === start) {
16
+ if (sweeping) {
17
+ sweeping = false;
18
+ host.clear();
19
+ }
20
+ return;
21
+ }
22
+ if (!row)
23
+ return;
24
+ if (!sweeping) {
25
+ sweeping = true;
26
+ host.focusRoot();
27
+ }
28
+ getSelection()?.removeAllRanges();
29
+ host.select(start.dataset.block, row.dataset.block);
30
+ }
31
+ function end() {
32
+ removeEventListener('mousemove', move);
33
+ removeEventListener('mouseup', end);
34
+ from = null;
35
+ sweeping = false;
36
+ }
37
+ return {
38
+ /** True while whole blocks are being swept (the browser must not extend its own selection meanwhile). */
39
+ get sweeping() {
40
+ return sweeping;
41
+ },
42
+ /** On the editor's mousedown: a press in a block's text may start a sweep. */
43
+ start(e) {
44
+ if (host.readonly() || e.button !== 0 || e.shiftKey)
45
+ return;
46
+ const t = e.target;
47
+ if (!t.closest('.ce') || t.closest('.grab'))
48
+ return;
49
+ from = t;
50
+ addEventListener('mousemove', move);
51
+ addEventListener('mouseup', end, { once: true });
52
+ },
53
+ /** Stops a sweep and its listeners (also when the editor goes away). */
54
+ end
55
+ };
56
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Block, BlockPlugin } from './types';
2
+ /** HTML (a pasted fragment or a page) as markdown. Browser only. */
3
+ export declare function markdownFromHtml(html: string): string;
4
+ /**
5
+ * HTML -> blocks, the way the editor takes a formatted paste: headings, paragraphs, lists (nested, numbered with their
6
+ * start, to-dos from checkboxes), quotes, code (`pre`, with a `language-…` class), rules, and inline marks — also
7
+ * Google Docs' styled spans. A table goes to the first of `plugins` that takes tables (`fromTable`). Images become
8
+ * links; scripts, styles and form controls are dropped. Browser only.
9
+ */
10
+ export declare function fromHtml(html: string, opts?: {
11
+ plugins?: BlockPlugin[];
12
+ }): Block[];