creo-edit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/clipboard/drop.d.ts +17 -0
- package/dist/clipboard/htmlParser.d.ts +18 -0
- package/dist/clipboard/htmlSerializer.d.ts +9 -0
- package/dist/commands/imageCommands.d.ts +32 -0
- package/dist/commands/insertCommands.d.ts +33 -0
- package/dist/commands/listCommands.d.ts +19 -0
- package/dist/commands/markCommands.d.ts +21 -0
- package/dist/commands/navigationCommands.d.ts +27 -0
- package/dist/commands/structuralCommands.d.ts +22 -0
- package/dist/commands/textCommands.d.ts +18 -0
- package/dist/controller/history.d.ts +31 -0
- package/dist/controller/navigation.d.ts +18 -0
- package/dist/controller/selection.d.ts +25 -0
- package/dist/controller/wordBoundary.d.ts +25 -0
- package/dist/createEditor.d.ts +252 -0
- package/dist/dom/anchorMap.d.ts +27 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +7833 -0
- package/dist/index.js.map +72 -0
- package/dist/input/keymap.d.ts +47 -0
- package/dist/input/mobile.d.ts +13 -0
- package/dist/input/nativeInput.d.ts +27 -0
- package/dist/markdown/serialize.d.ts +2 -0
- package/dist/model/blockText.d.ts +42 -0
- package/dist/model/cellAccess.d.ts +2 -0
- package/dist/model/doc.d.ts +45 -0
- package/dist/model/fractional.d.ts +57 -0
- package/dist/model/rebalance.d.ts +12 -0
- package/dist/model/types.d.ts +133 -0
- package/dist/plugin/anchorCodec.d.ts +18 -0
- package/dist/plugin/atomic.d.ts +2 -0
- package/dist/plugin/builtin.d.ts +10 -0
- package/dist/plugin/decorations.d.ts +35 -0
- package/dist/plugin/htmlCodec.d.ts +8 -0
- package/dist/plugin/keymapMatch.d.ts +2 -0
- package/dist/plugin/registry.d.ts +29 -0
- package/dist/plugin/runsAt.d.ts +9 -0
- package/dist/plugin/serializeCodec.d.ts +6 -0
- package/dist/plugin/triggers.d.ts +33 -0
- package/dist/plugin/types.d.ts +188 -0
- package/dist/plugins/add-block/index.d.ts +17 -0
- package/dist/plugins/calendar/index.d.ts +7 -0
- package/dist/plugins/calendar/view.d.ts +29 -0
- package/dist/plugins/cells/codecs.d.ts +6 -0
- package/dist/plugins/cells/commands.d.ts +5 -0
- package/dist/plugins/cells/controls.d.ts +3 -0
- package/dist/plugins/cells/htmlCodec.d.ts +6 -0
- package/dist/plugins/cells/index.d.ts +3 -0
- package/dist/plugins/cells/views.d.ts +27 -0
- package/dist/plugins/drag-handle/index.d.ts +6 -0
- package/dist/plugins/infinite-scroll/index.d.ts +44 -0
- package/dist/plugins/md-shortcuts/index.d.ts +2 -0
- package/dist/plugins/search/engine.d.ts +33 -0
- package/dist/plugins/search/highlight.d.ts +13 -0
- package/dist/plugins/search/index.d.ts +5 -0
- package/dist/plugins/search/navigate.d.ts +15 -0
- package/dist/plugins/search/styles.d.ts +1 -0
- package/dist/plugins/search/types.d.ts +73 -0
- package/dist/plugins/search/ui.d.ts +2 -0
- package/dist/plugins/slash/index.d.ts +12 -0
- package/dist/plugins/slash/items.d.ts +21 -0
- package/dist/plugins/slash/menu.d.ts +17 -0
- package/dist/plugins/styles.css +233 -0
- package/dist/render/DocView.d.ts +7 -0
- package/dist/render/InlineRunsView.d.ts +7 -0
- package/dist/render/blocks/CodeBlockView.d.ts +7 -0
- package/dist/render/blocks/HeadingView.d.ts +7 -0
- package/dist/render/blocks/ImageView.d.ts +8 -0
- package/dist/render/blocks/ListItemView.d.ts +7 -0
- package/dist/render/blocks/ParagraphView.d.ts +7 -0
- package/dist/virtual/VirtualDoc.d.ts +28 -0
- package/dist/virtual/heightIndex.d.ts +36 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nik (xnimorz)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# creo-edit
|
|
2
|
+
|
|
3
|
+
A text editor framework, based on [Creo](https://github.com/xnim/creo). Row-based rich-text editing on a controlled `contentEditable`. Mountable inside any framework that gives you a DOM ref (React, Vue, Svelte, Solid) — see [Hosting inside other frameworks](#hosting-inside-other-frameworks).
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- **Cursor lives outside the document state** — typing into a block doesn't dirty selection subscribers, and vice versa.
|
|
8
|
+
- **CRDT-friendly row ordering** via base-62 fractional indexing — insert-between is O(log n), no renumber.
|
|
9
|
+
- **Controlled `contentEditable`** — native browser selection and IME, with every `beforeinput` intercepted and translated into a command. The model is the source of truth.
|
|
10
|
+
- **Per-keystroke render cost is O(1) blocks** — block immutability + `shouldUpdate` identity checks make the keyed reconciler skip every untouched block.
|
|
11
|
+
- **Optional virtualization** — only blocks intersecting the viewport are mounted; documents with hundreds of thousands of blocks remain responsive.
|
|
12
|
+
- **First-class mobile support** — native long-press OS menu, native selection handles, IME composition reconciled into a single undo step, `visualViewport`-aware caret-keeping.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add creo creo-edit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createApp, HtmlRender } from "creo";
|
|
24
|
+
import { createEditor } from "creo-edit";
|
|
25
|
+
|
|
26
|
+
const editor = createEditor({
|
|
27
|
+
initial: {
|
|
28
|
+
blocks: [
|
|
29
|
+
{ type: "h1", runs: [{ text: "Hello" }] },
|
|
30
|
+
{ type: "p", runs: [{ text: "Type here." }] },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
createApp(
|
|
36
|
+
() => editor.EditorView(),
|
|
37
|
+
new HtmlRender(document.querySelector("#app")!),
|
|
38
|
+
).mount();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Hosting inside other frameworks
|
|
42
|
+
|
|
43
|
+
The editor renders through Creo's `HtmlRender`, which mounts into any DOM element. Embed it inside a host framework by giving Creo a container element managed by that framework. There is no React/Vue/Svelte wrapper package — you write the ten-line bridge once.
|
|
44
|
+
|
|
45
|
+
**React**
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { useEffect, useRef } from "react";
|
|
49
|
+
import { createApp, HtmlRender } from "creo";
|
|
50
|
+
import { createEditor } from "creo-edit";
|
|
51
|
+
|
|
52
|
+
export function CreoEdit() {
|
|
53
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!ref.current) return;
|
|
56
|
+
const editor = createEditor();
|
|
57
|
+
const app = createApp(
|
|
58
|
+
() => editor.EditorView(),
|
|
59
|
+
new HtmlRender(ref.current),
|
|
60
|
+
).mount();
|
|
61
|
+
return () => app.unmount?.();
|
|
62
|
+
}, []);
|
|
63
|
+
return <div ref={ref} />;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Vue 3**
|
|
68
|
+
|
|
69
|
+
```vue
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { onMounted, onBeforeUnmount, ref } from "vue";
|
|
72
|
+
import { createApp as creoApp, HtmlRender } from "creo";
|
|
73
|
+
import { createEditor } from "creo-edit";
|
|
74
|
+
|
|
75
|
+
const host = ref<HTMLElement | null>(null);
|
|
76
|
+
let app: ReturnType<typeof creoApp> | null = null;
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
const editor = createEditor();
|
|
79
|
+
app = creoApp(() => editor.EditorView(), new HtmlRender(host.value!)).mount();
|
|
80
|
+
});
|
|
81
|
+
onBeforeUnmount(() => app?.unmount?.());
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div ref="host" />
|
|
86
|
+
</template>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Svelte 5 / Solid / anything with a DOM ref**: same pattern — wait for the container to be mounted, call `createApp(...).mount()`, call `app.unmount()` on teardown. The `editor` handle (`createEditor()` return value) is plain JS; exposing `editor.dispatch`, `editor.toJSON`, `editor.docStore`, etc. from inside a hook / composable is straightforward.
|
|
90
|
+
|
|
91
|
+
## Editor API
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
type Editor = {
|
|
95
|
+
docStore: Store<DocState>;
|
|
96
|
+
selStore: Store<Selection>;
|
|
97
|
+
dispatch(cmd: Command): void;
|
|
98
|
+
undo(): void;
|
|
99
|
+
redo(): void;
|
|
100
|
+
EditorView: PublicView<EditorViewProps, void>;
|
|
101
|
+
setDocFromHTML(html: string): void;
|
|
102
|
+
toJSON(): SerializedDoc;
|
|
103
|
+
focus(): void;
|
|
104
|
+
blur(): void;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Commands
|
|
109
|
+
|
|
110
|
+
- `insertText`, `deleteBackward`, `deleteForward` — text editing
|
|
111
|
+
- `splitBlock`, `mergeBackward`, `mergeForward` — structural
|
|
112
|
+
- `setBlockType` — promote/demote between paragraph, headings, list items
|
|
113
|
+
- `toggleMark` — bold, italic, underline, strikethrough, code
|
|
114
|
+
- `toggleList`, `indentList`, `outdentList`
|
|
115
|
+
- `insertImage`, `insertTable`
|
|
116
|
+
- `tableInsertRow/Col`, `tableRemoveRow/Col`
|
|
117
|
+
- `moveCursor`
|
|
118
|
+
|
|
119
|
+
### Default keybindings
|
|
120
|
+
|
|
121
|
+
| Chord | Action |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `Cmd/Ctrl+B` / `+I` / `+U` | Toggle bold / italic / underline |
|
|
124
|
+
| `Cmd/Ctrl+Shift+S` | Strikethrough |
|
|
125
|
+
| `Cmd/Ctrl+Z` / `+Shift+Z` | Undo / redo |
|
|
126
|
+
| `Cmd/Ctrl+Alt+1..6` | Heading levels |
|
|
127
|
+
| `Cmd/Ctrl+Alt+0` | Paragraph |
|
|
128
|
+
| `Tab` / `Shift+Tab` | List indent / outdent (or table cell nav) |
|
|
129
|
+
| `Enter` / `Backspace` / `Delete` | Split / merge blocks |
|
|
130
|
+
| Arrows / `Home` / `End` | Caret navigation (extend with `Shift`) |
|
|
131
|
+
|
|
132
|
+
### Virtualization (large documents)
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const editor = createEditor({
|
|
136
|
+
initial: { blocks: [...] },
|
|
137
|
+
virtualized: true,
|
|
138
|
+
virtualEstimatedHeight: 32,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Only blocks intersecting `[scrollTop − overscan, scrollTop + viewport + overscan]` are mounted, with measured per-block heights stored in a Fenwick tree for O(log n) viewport resolution.
|
|
143
|
+
|
|
144
|
+
### Image upload
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const editor = createEditor({
|
|
148
|
+
uploadImage: async (file) => {
|
|
149
|
+
const fd = new FormData();
|
|
150
|
+
fd.append("file", file);
|
|
151
|
+
const res = await fetch("/upload", { method: "POST", body: fd });
|
|
152
|
+
return (await res.json()).url;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Without `uploadImage`, dropped/pasted images use `URL.createObjectURL`.
|
|
158
|
+
|
|
159
|
+
### Mobile
|
|
160
|
+
|
|
161
|
+
`creo-edit` ships first-class mobile support. The editor root is a `contentEditable`, so the OS long-press menu, native selection handles, IME composition, and autocorrect work out of the box. `visualViewport` tracking exposes `--creo-vv-height` and `--creo-vv-top` as CSS custom properties so host pages can position floating UI above the soft keyboard, and scrolls the caret into the upper third of visible space when the keyboard opens.
|
|
162
|
+
|
|
163
|
+
## Architecture
|
|
164
|
+
|
|
165
|
+
See `AGENTS.md` in the repo root for engine-level notes. The editor is a pure consumer of Creo's public API — `view`, `use`, `store`, primitive functions — and uses raw DOM listeners only for events Creo's event map doesn't expose (composition, drag/drop, clipboard, visualViewport).
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import { type UploadFn } from "../commands/imageCommands";
|
|
3
|
+
import type { DocState, Selection } from "../model/types";
|
|
4
|
+
export type DropStores = {
|
|
5
|
+
docStore: Store<DocState>;
|
|
6
|
+
selStore: Store<Selection>;
|
|
7
|
+
};
|
|
8
|
+
export type DropHandle = {
|
|
9
|
+
destroy: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Wire dragover + drop on the editor root so dragged image files become
|
|
13
|
+
* image blocks. We only swallow the drop when at least one image file is
|
|
14
|
+
* present; other drops fall through to the browser's default behaviour
|
|
15
|
+
* (so dragging text from elsewhere keeps working).
|
|
16
|
+
*/
|
|
17
|
+
export declare function attachDrop(root: HTMLElement, stores: DropStores, upload?: UploadFn): DropHandle;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BlockSpec } from "../model/types";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a fragment of HTML into a sanitized list of BlockSpec.
|
|
4
|
+
*
|
|
5
|
+
* Sanitization rules:
|
|
6
|
+
* - `<script>`, `<style>`, `<link>`, `<meta>` content is dropped entirely.
|
|
7
|
+
* - All `on*` attributes are stripped.
|
|
8
|
+
* - `javascript:` URLs become "#".
|
|
9
|
+
* - Unknown elements become inline (their text content is preserved, marks
|
|
10
|
+
* of any ancestor mark elements still apply).
|
|
11
|
+
* - Block elements outside our model (article, section, etc.) become
|
|
12
|
+
* paragraphs (recursing into their children when those are themselves
|
|
13
|
+
* block-level).
|
|
14
|
+
*
|
|
15
|
+
* Falls back to a single paragraph if no block-level structure is present.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseHTML(html: string): BlockSpec[];
|
|
18
|
+
export declare function parsePlainText(text: string): BlockSpec[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DocState, Selection } from "../model/types";
|
|
2
|
+
/** Whole-doc serialization (used by toJSON-ish helpers and copy-all). */
|
|
3
|
+
export declare function docToHtml(doc: DocState): string;
|
|
4
|
+
export declare function docToPlain(doc: DocState): string;
|
|
5
|
+
export type ClipboardPayload = {
|
|
6
|
+
html: string;
|
|
7
|
+
plain: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function selectionToClipboard(doc: DocState, sel: Selection): ClipboardPayload;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { DocState, Selection } from "../model/types";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
export type UploadFn = (file: File) => Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Pick an image source for a File. If `upload` is provided, await its URL;
|
|
10
|
+
* otherwise fall back to `URL.createObjectURL`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fileToImageSrc(file: File, upload?: UploadFn): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Drop / paste a single File into the editor as an image block. Async
|
|
15
|
+
* (must await an upload when configured).
|
|
16
|
+
*/
|
|
17
|
+
export declare function insertImageFile(stores: Stores, file: File, upload?: UploadFn): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Process a FileList (from paste or drop). Each image becomes its own block;
|
|
20
|
+
* non-image files are ignored.
|
|
21
|
+
*/
|
|
22
|
+
export declare function insertImageFiles(stores: Stores, files: FileList | File[], upload?: UploadFn): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Delete the atomic block currently under the caret. Backspace / Delete
|
|
25
|
+
* routes here whenever the caret sits on any atomic block (image, calendar,
|
|
26
|
+
* or any plugin block flagged `isAtomic`). The caret lands at the start of
|
|
27
|
+
* whatever block survives in that slot — preferring the next sibling, then
|
|
28
|
+
* the previous, then the first remaining block.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deleteSelectedAtomic(stores: Stores): boolean;
|
|
31
|
+
/** Backwards-compatible alias — img used to be the only atomic block. */
|
|
32
|
+
export declare const deleteSelectedImage: typeof deleteSelectedAtomic;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { BlockSpec, DocState, Selection } from "../model/types";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Insert a list of pre-parsed blocks at the current caret. If the selection
|
|
9
|
+
* is a range, it is collapsed first.
|
|
10
|
+
*
|
|
11
|
+
* Splice rules (matches every consumer rich-text editor I'm aware of):
|
|
12
|
+
* - Single text-bearing block: inline-merge into the current block.
|
|
13
|
+
* - Multiple blocks: split the current block at the caret; the first
|
|
14
|
+
* pasted block's runs join the LEFT half, the last pasted block's runs
|
|
15
|
+
* are followed by the RIGHT half (in a new block of the last one's
|
|
16
|
+
* type), and any blocks in between are inserted as new blocks.
|
|
17
|
+
* - Non-text blocks (img / table): always become a separate block, with
|
|
18
|
+
* the surrounding paragraph split if needed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function insertBlocks(stores: Stores, blocks: BlockSpec[]): boolean;
|
|
21
|
+
export declare function insertImage(stores: Stores, payload: {
|
|
22
|
+
src: string;
|
|
23
|
+
alt?: string;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
}): boolean;
|
|
27
|
+
export declare function insertColumns(stores: Stores, payload: {
|
|
28
|
+
cols: number;
|
|
29
|
+
}): boolean;
|
|
30
|
+
export declare function insertTable(stores: Stores, payload: {
|
|
31
|
+
rows: number;
|
|
32
|
+
cols: number;
|
|
33
|
+
}): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { DocState, Selection } from "../model/types";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Toggle whether the touched blocks are list items of the given `ordered`
|
|
9
|
+
* kind.
|
|
10
|
+
* - If every touched text block is already a list item with that ordering,
|
|
11
|
+
* convert them back to paragraphs (and reset depth to 0).
|
|
12
|
+
* - Otherwise convert them to `li ordered=...` (preserving depth where
|
|
13
|
+
* possible, defaulting to 0).
|
|
14
|
+
*/
|
|
15
|
+
export declare function toggleList({ docStore, selStore }: Stores, ordered: boolean): boolean;
|
|
16
|
+
/** Tab — increase list depth (max 3). No-op for non-list blocks. */
|
|
17
|
+
export declare function indentList({ docStore, selStore }: Stores): boolean;
|
|
18
|
+
/** Shift+Tab — decrease list depth. At depth 0, convert back to paragraph. */
|
|
19
|
+
export declare function outdentList({ docStore, selStore }: Stores): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { DocState, Mark, Selection } from "../model/types";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Toggle a mark over the current selection.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour:
|
|
11
|
+
* - Caret-only: no-op (real editors track a "pending mark" that biases the
|
|
12
|
+
* next character; we don't ship that in M6 to keep the API minimal).
|
|
13
|
+
* - Single-block range: if every character in [start, end) already has the
|
|
14
|
+
* mark, REMOVE it; otherwise ADD it everywhere in the range.
|
|
15
|
+
* - Cross-block range: same rule applied per-block to the slice that
|
|
16
|
+
* intersects the range.
|
|
17
|
+
*
|
|
18
|
+
* Run merging happens via `normalizeRuns`, so toggling repeatedly never
|
|
19
|
+
* fragments runs unbounded.
|
|
20
|
+
*/
|
|
21
|
+
export declare function toggleMark({ docStore, selStore }: Stores, mark: Mark): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import { blockAbove, blockBelow, endOfBlock, homeOfBlock, nextAnchor, nextWord, prevAnchor, prevWord } from "../controller/navigation";
|
|
3
|
+
import type { Anchor, DocState, Selection } from "../model/types";
|
|
4
|
+
export type NavStores = {
|
|
5
|
+
docStore: Store<DocState>;
|
|
6
|
+
selStore: Store<Selection>;
|
|
7
|
+
};
|
|
8
|
+
export type AnchorStep = (doc: DocState, a: Anchor) => Anchor;
|
|
9
|
+
/**
|
|
10
|
+
* Move the cursor by applying `step` to the current focus side.
|
|
11
|
+
* - `extend === false` → collapse to the new anchor.
|
|
12
|
+
* - `extend === true` → keep the original anchor, move the focus.
|
|
13
|
+
*/
|
|
14
|
+
export declare function moveBy({ docStore, selStore }: NavStores, step: AnchorStep, extend: boolean): void;
|
|
15
|
+
export declare function moveTo({ selStore }: NavStores, anchor: Anchor, extend: boolean): void;
|
|
16
|
+
export declare const STEP: {
|
|
17
|
+
next: typeof nextAnchor;
|
|
18
|
+
prev: typeof prevAnchor;
|
|
19
|
+
nextWord: typeof nextWord;
|
|
20
|
+
prevWord: typeof prevWord;
|
|
21
|
+
up: typeof blockAbove;
|
|
22
|
+
down: typeof blockBelow;
|
|
23
|
+
home: typeof homeOfBlock;
|
|
24
|
+
end: typeof endOfBlock;
|
|
25
|
+
docHome: (doc: DocState, _a: Anchor) => Anchor;
|
|
26
|
+
docEnd: (doc: DocState, _a: Anchor) => Anchor;
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { BlockType, DocState, Selection } from "../model/types";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Split the current block at the caret. The right half becomes a NEW block
|
|
9
|
+
* placed immediately after the current one. Headings split into a paragraph
|
|
10
|
+
* (the convention every consumer-grade editor uses — pressing Enter at the
|
|
11
|
+
* end of a heading shouldn't create another heading by default). List items
|
|
12
|
+
* split into another list item of the same kind & depth.
|
|
13
|
+
*/
|
|
14
|
+
export declare function splitBlock({ docStore, selStore }: Stores): boolean;
|
|
15
|
+
export declare function mergeBackward({ docStore, selStore }: Stores): boolean;
|
|
16
|
+
export declare function mergeForward({ docStore, selStore }: Stores): boolean;
|
|
17
|
+
export type SetBlockTypePayload = {
|
|
18
|
+
type: BlockType;
|
|
19
|
+
ordered?: boolean;
|
|
20
|
+
depth?: 0 | 1 | 2 | 3;
|
|
21
|
+
};
|
|
22
|
+
export declare function setBlockType({ docStore, selStore }: Stores, payload: SetBlockTypePayload): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DocState, Selection } from "../model/types";
|
|
2
|
+
import type { Store } from "creo";
|
|
3
|
+
export type Stores = {
|
|
4
|
+
docStore: Store<DocState>;
|
|
5
|
+
selStore: Store<Selection>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Insert plain text at the current selection. If the selection is a range,
|
|
9
|
+
* the range is deleted first and the text is inserted at the start.
|
|
10
|
+
*
|
|
11
|
+
* Cross-block ranges are deferred to mergeBackward (M5) — this only handles
|
|
12
|
+
* the single-cell / single-block case.
|
|
13
|
+
*/
|
|
14
|
+
export declare function insertText({ docStore, selStore }: Stores, text: string): boolean;
|
|
15
|
+
/** Delete one character backward, or collapse a range. */
|
|
16
|
+
export declare function deleteBackward({ docStore, selStore }: Stores): boolean;
|
|
17
|
+
/** Delete one character forward, or collapse a range. */
|
|
18
|
+
export declare function deleteForward({ docStore, selStore }: Stores): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Store } from "creo";
|
|
2
|
+
import type { DocState, Selection } from "../model/types";
|
|
3
|
+
/**
|
|
4
|
+
* Snapshot-based undo/redo. We don't synthesize inverse commands — we just
|
|
5
|
+
* stash the previous (doc, sel) before each mutation and let undo restore
|
|
6
|
+
* it.
|
|
7
|
+
*
|
|
8
|
+
* Coalescing rule: consecutive `insertText` / `deleteBackward` ops within
|
|
9
|
+
* 500ms collapse into a single undo entry. This matches Notion / Google
|
|
10
|
+
* Docs UX — typing a sentence then hitting Cmd+Z removes the whole sentence,
|
|
11
|
+
* not the last character.
|
|
12
|
+
*/
|
|
13
|
+
export type HistoryEntry = {
|
|
14
|
+
doc: DocState;
|
|
15
|
+
sel: Selection;
|
|
16
|
+
tag: string;
|
|
17
|
+
ts: number;
|
|
18
|
+
};
|
|
19
|
+
export type HistoryStores = {
|
|
20
|
+
docStore: Store<DocState>;
|
|
21
|
+
selStore: Store<Selection>;
|
|
22
|
+
};
|
|
23
|
+
export declare const COALESCE_MS = 500;
|
|
24
|
+
export declare const HISTORY_CAP = 200;
|
|
25
|
+
export declare function createHistory(stores: HistoryStores): {
|
|
26
|
+
record: (tag: string) => void;
|
|
27
|
+
undo: () => boolean;
|
|
28
|
+
redo: () => boolean;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
};
|
|
31
|
+
export type History = ReturnType<typeof createHistory>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Anchor, DocState } from "../model/types";
|
|
2
|
+
/**
|
|
3
|
+
* Move one character to the right. Crosses block boundaries: from the end of
|
|
4
|
+
* block i to the start of block i+1. At end-of-doc, returns the same anchor.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nextAnchor(doc: DocState, a: Anchor): Anchor;
|
|
7
|
+
/**
|
|
8
|
+
* Move one character to the left. Crosses block boundaries the same way.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prevAnchor(doc: DocState, a: Anchor): Anchor;
|
|
11
|
+
export declare function homeOfBlock(doc: DocState, a: Anchor): Anchor;
|
|
12
|
+
export declare function endOfBlock(doc: DocState, a: Anchor): Anchor;
|
|
13
|
+
export declare function homeOfDoc(doc: DocState): Anchor;
|
|
14
|
+
export declare function endOfDocAnchor(doc: DocState): Anchor;
|
|
15
|
+
export declare function blockAbove(doc: DocState, a: Anchor): Anchor;
|
|
16
|
+
export declare function blockBelow(doc: DocState, a: Anchor): Anchor;
|
|
17
|
+
export declare function nextWord(doc: DocState, a: Anchor): Anchor;
|
|
18
|
+
export declare function prevWord(doc: DocState, a: Anchor): Anchor;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Anchor, BlockId, DocState, Selection } from "../model/types";
|
|
2
|
+
export declare function caretAt(blockId: BlockId, offset: number): Anchor;
|
|
3
|
+
export declare function caret(at: Anchor): Selection;
|
|
4
|
+
export declare function range(anchor: Anchor, focus: Anchor): Selection;
|
|
5
|
+
export declare function anchorOffset(a: Anchor): number;
|
|
6
|
+
export declare function withCharOffset(a: Anchor, offset: number): Anchor;
|
|
7
|
+
/** Returns true when the selection is a collapsed caret. */
|
|
8
|
+
export declare function isCaret(s: Selection): s is {
|
|
9
|
+
kind: "caret";
|
|
10
|
+
at: Anchor;
|
|
11
|
+
};
|
|
12
|
+
/** Same anchor & focus → effectively a caret. */
|
|
13
|
+
export declare function selectionStart(s: Selection): Anchor;
|
|
14
|
+
export declare function selectionEnd(s: Selection): Anchor;
|
|
15
|
+
/** Compares two anchors that point into the same doc. Returns -1/0/+1. */
|
|
16
|
+
export declare function compareAnchors(doc: DocState, a: Anchor, b: Anchor): number;
|
|
17
|
+
/** Anchor in document-order start..end ascending. */
|
|
18
|
+
export declare function orderedRange(doc: DocState, s: Selection): {
|
|
19
|
+
start: Anchor;
|
|
20
|
+
end: Anchor;
|
|
21
|
+
};
|
|
22
|
+
/** Clamp an anchor so it points at a valid offset inside its block. */
|
|
23
|
+
export declare function clampAnchor(doc: DocState, a: Anchor): Anchor;
|
|
24
|
+
export declare function clampSelection(doc: DocState, s: Selection): Selection;
|
|
25
|
+
export declare function endOfDoc(doc: DocState): Anchor;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word-boundary helpers — compute "next/previous word" offsets within a
|
|
3
|
+
* single string. Used by Cmd/Ctrl+Arrow navigation and double-click word
|
|
4
|
+
* selection.
|
|
5
|
+
*
|
|
6
|
+
* Definition of a word — same as the OS conventions every consumer editor
|
|
7
|
+
* follows:
|
|
8
|
+
* - A word run is one-or-more "word chars" (Unicode letters + digits +
|
|
9
|
+
* underscore + apostrophes embedded inside a run).
|
|
10
|
+
* - Whitespace and punctuation are treated as separators.
|
|
11
|
+
* - "Next word from here" = scan forward over (separators), then over
|
|
12
|
+
* (word chars), and land on the first non-word-char.
|
|
13
|
+
* - "Prev word from here" = scan backward over (separators), then over
|
|
14
|
+
* (word chars), and land on the first word-char.
|
|
15
|
+
*/
|
|
16
|
+
/** Offset of the start of the next word at or after `offset` in `text`. */
|
|
17
|
+
export declare function nextWordOffset(text: string, offset: number): number;
|
|
18
|
+
/** Offset of the start of the previous word strictly before `offset`. */
|
|
19
|
+
export declare function prevWordOffset(text: string, offset: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Find the word that contains `offset` in `text`. Returns [start, end).
|
|
22
|
+
* If `offset` is on a non-word char, returns [offset, offset+1) so callers
|
|
23
|
+
* still get a usable selection (single char).
|
|
24
|
+
*/
|
|
25
|
+
export declare function wordRangeAt(text: string, offset: number): [number, number];
|