@b9g/revise 0.1.2 → 0.1.4
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 +7 -0
- package/README.md +147 -58
- package/package.json +59 -31
- package/{_subseq.d.ts → src/_subseq.d.ts} +18 -18
- package/src/contentarea.cjs +649 -0
- package/{contentarea.d.ts → src/contentarea.d.ts} +74 -57
- package/src/contentarea.js +624 -0
- package/src/edit.cjs +767 -0
- package/{edit.d.ts → src/edit.d.ts} +93 -99
- package/src/edit.js +741 -0
- package/src/history.cjs +100 -0
- package/{history.d.ts → src/history.d.ts} +13 -13
- package/src/history.js +76 -0
- package/src/keyer.cjs +93 -0
- package/{keyer.d.ts → src/keyer.d.ts} +8 -8
- package/src/keyer.js +69 -0
- package/src/state.cjs +142 -0
- package/src/state.d.ts +31 -0
- package/src/state.js +117 -0
- package/contentarea.cjs +0 -597
- package/contentarea.cjs.map +0 -1
- package/contentarea.js +0 -593
- package/contentarea.js.map +0 -1
- package/edit.cjs +0 -618
- package/edit.cjs.map +0 -1
- package/edit.js +0 -615
- package/edit.js.map +0 -1
- package/history.cjs +0 -81
- package/history.cjs.map +0 -1
- package/history.js +0 -78
- package/history.js.map +0 -1
- package/keyer.cjs +0 -42
- package/keyer.cjs.map +0 -1
- package/keyer.js +0 -39
- package/keyer.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2021 Brian Kim
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,70 +1,159 @@
|
|
|
1
1
|
# Revise.js
|
|
2
2
|
|
|
3
|
-
Revise is a JavaScript library for
|
|
4
|
-
[`contenteditable
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
to be framework-agnostic, though currently it is only being tested with
|
|
9
|
-
[Crank](crank.js.org).
|
|
3
|
+
Revise is a JavaScript library for building rich-text editors on top of
|
|
4
|
+
[`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
|
|
5
|
+
It provides low-level primitives — a custom element, an edit data structure,
|
|
6
|
+
a keyed reconciler, and a state coordinator — so that any framework can build
|
|
7
|
+
editing experiences without fighting the DOM.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
who aren’t afraid of stepping through DOM code in a debugger.
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
TKTKTKT
|
|
19
|
-
|
|
20
|
-
## API
|
|
21
|
-
|
|
22
|
-
### `ContentAreaElement`
|
|
23
|
-
|
|
24
|
-
A custom element class with an API similar to that of the JavaScript API for
|
|
25
|
-
the [`<textarea>`
|
|
26
|
-
element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement).
|
|
27
|
-
This class provides special properties and methods for reading the current
|
|
28
|
-
value of the element’s contents and manipulating its selection.
|
|
29
|
-
|
|
30
|
-
- `value`
|
|
31
|
-
|
|
32
|
-
- `selectionStart`
|
|
33
|
-
|
|
34
|
-
- `selectionEnd`
|
|
9
|
+
```
|
|
10
|
+
npm install @b9g/revise
|
|
11
|
+
```
|
|
35
12
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `setSelectionRange(selectionStart: number, selectionEnd: number, selectionDirection?: SelectionDirection): void;`
|
|
39
|
-
|
|
40
|
-
- `indexAt(node: Node | null, offset: number): number`
|
|
41
|
-
|
|
42
|
-
- `nodeOffsetAt(index: number): [Node | null, number]
|
|
43
|
-
|
|
44
|
-
- `source(name: string): boolean`
|
|
45
|
-
|
|
46
|
-
- `ContentEvent`
|
|
47
|
-
|
|
48
|
-
### `Edit`
|
|
49
|
-
|
|
50
|
-
- `operations(): Array<Operation>`
|
|
51
|
-
|
|
52
|
-
- `apply(text: string): string`
|
|
53
|
-
|
|
54
|
-
- `compose(that: Edit): Edit`
|
|
13
|
+
## Status
|
|
55
14
|
|
|
56
|
-
|
|
15
|
+
Early beta. The API is stabilizing but may still change. Recommended for
|
|
16
|
+
developers who aren’t afraid of stepping through DOM code in a debugger.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Register the custom element, create an `EditableState`, and render keyed lines
|
|
21
|
+
inside a `<content-area>`:
|
|
57
22
|
|
|
58
|
-
|
|
23
|
+
```js
|
|
24
|
+
import {ContentAreaElement} from "@b9g/revise/contentarea.js";
|
|
25
|
+
import {EditableState} from "@b9g/revise/state.js";
|
|
59
26
|
|
|
60
|
-
-
|
|
27
|
+
customElements.define("content-area", ContentAreaElement);
|
|
61
28
|
|
|
62
|
-
|
|
29
|
+
const state = new EditableState({value: "Hello World!\n"});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When `<content-area>` detects a user edit, it fires a `contentchange` event
|
|
33
|
+
with an `Edit` object describing the change. Prevent the default DOM mutation,
|
|
34
|
+
apply the edit to state, then re-render:
|
|
63
35
|
|
|
64
|
-
|
|
36
|
+
```js
|
|
37
|
+
area.addEventListener("contentchange", (ev) => {
|
|
38
|
+
const {edit, source} = ev.detail;
|
|
39
|
+
if (source === "render") return; // ignore our own re-renders
|
|
65
40
|
|
|
66
|
-
|
|
41
|
+
const selection = area.getSelectionRange();
|
|
42
|
+
ev.preventDefault();
|
|
43
|
+
state.applyEdit(edit);
|
|
44
|
+
// Re-render your UI from state.value, then restore selection
|
|
45
|
+
});
|
|
46
|
+
```
|
|
67
47
|
|
|
68
|
-
|
|
48
|
+
After re-rendering, mark the DOM update so the next `contentchange` is tagged
|
|
49
|
+
as a render (not a user edit), and restore the cursor:
|
|
69
50
|
|
|
70
|
-
|
|
51
|
+
```js
|
|
52
|
+
area.source("render");
|
|
53
|
+
area.setSelectionRange(sel.start, sel.end, sel.direction);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `state.keyer.keyAt(index)` to get stable keys for each line so your
|
|
57
|
+
framework can reconcile DOM nodes efficiently:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const lines = state.value.split("\n");
|
|
61
|
+
let cursor = 0;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const key = state.keyer.keyAt(cursor);
|
|
64
|
+
cursor += line.length + 1;
|
|
65
|
+
// render <div key={key}>{line || <br/>}</div>
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Undo/redo is built in:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
state.undo(); // returns true if there was something to undo
|
|
73
|
+
state.redo();
|
|
74
|
+
state.checkpoint(); // break the current edit group (e.g. on cursor move)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See the [live demos](https://bikeshaving.github.io/revise/) for complete
|
|
78
|
+
examples with syntax highlighting, rainbow text, and social highlighting.
|
|
79
|
+
|
|
80
|
+
## Modules
|
|
81
|
+
|
|
82
|
+
### `@b9g/revise/contentarea.js`
|
|
83
|
+
|
|
84
|
+
**`ContentAreaElement`** — A custom element (`<content-area>`) with an API
|
|
85
|
+
modeled after
|
|
86
|
+
[`HTMLTextAreaElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement).
|
|
87
|
+
Wraps a `contenteditable` element and translates DOM mutations into `Edit`
|
|
88
|
+
objects.
|
|
89
|
+
|
|
90
|
+
- `value: string` — The text content of the element.
|
|
91
|
+
- `selectionStart: number`
|
|
92
|
+
- `selectionEnd: number`
|
|
93
|
+
- `selectionDirection: SelectionDirection`
|
|
94
|
+
- `getSelectionRange(): SelectionRange`
|
|
95
|
+
- `setSelectionRange(start, end, direction?): void`
|
|
96
|
+
- `indexAt(node, offset): number` — Convert a DOM position to a text index.
|
|
97
|
+
- `nodeOffsetAt(index): [Node | null, number]` — Convert a text index to a DOM position.
|
|
98
|
+
- `source(name): boolean` — Tag the next DOM mutation cycle so `contentchange` events include a `source` property. Returns `true` if content changed.
|
|
99
|
+
|
|
100
|
+
**`ContentEvent`** — The event dispatched on `contentchange`. `event.detail`
|
|
101
|
+
contains `{edit: Edit, source: string | null}`. Call `event.preventDefault()`
|
|
102
|
+
to revert the DOM mutation and apply the edit to your own state instead.
|
|
103
|
+
|
|
104
|
+
### `@b9g/revise/edit.js`
|
|
105
|
+
|
|
106
|
+
**`Edit`** — A compact, composable data structure for representing changes to
|
|
107
|
+
strings.
|
|
108
|
+
|
|
109
|
+
- `apply(text): string` — Apply the edit to a string.
|
|
110
|
+
- `invert(): Edit` — Return the inverse edit (for undo).
|
|
111
|
+
- `compose(that): Edit` — Compose two sequential edits into one.
|
|
112
|
+
- `normalize(): Edit` — Normalize the edit (remove no-ops).
|
|
113
|
+
- `operations(): Array<Operation>` — Get the list of retain/insert/delete operations.
|
|
114
|
+
- `hasChangesBetween(start, end): boolean`
|
|
115
|
+
- `Edit.diff(text1, text2, hint?): Edit` — Compute an edit from two strings.
|
|
116
|
+
- `Edit.createBuilder(value): EditBuilder` — Create a builder for constructing edits operation-by-operation.
|
|
117
|
+
|
|
118
|
+
**`EditBuilder`** — Fluent builder for constructing edits:
|
|
119
|
+
`insert(value)`, `retain(length)`, `delete(length)`, `concat(edit)`, `build()`.
|
|
120
|
+
|
|
121
|
+
### `@b9g/revise/history.js`
|
|
122
|
+
|
|
123
|
+
**`EditHistory`** — Undo/redo stack that automatically composes consecutive
|
|
124
|
+
simple edits into groups.
|
|
125
|
+
|
|
126
|
+
- `append(edit): void` — Record an edit. Consecutive simple edits are composed; complex edits (multiple operations) trigger a checkpoint.
|
|
127
|
+
- `checkpoint(): void` — Break the current edit group.
|
|
128
|
+
- `undo(): Edit | undefined` — Pop the undo stack and return the inverted edit.
|
|
129
|
+
- `redo(): Edit | undefined` — Pop the redo stack and return the edit.
|
|
130
|
+
- `canUndo(): boolean`
|
|
131
|
+
- `canRedo(): boolean`
|
|
132
|
+
|
|
133
|
+
### `@b9g/revise/keyer.js`
|
|
134
|
+
|
|
135
|
+
**`Keyer`** — Assigns stable integer keys to text positions and keeps them in
|
|
136
|
+
sync as edits are applied. Use `keyAt(index)` to get a stable key for the
|
|
137
|
+
character at a given index, then call `transform(edit)` after each edit to
|
|
138
|
+
update all key positions.
|
|
139
|
+
|
|
140
|
+
- `keyAt(index): number` — Get or create a stable key for a text position.
|
|
141
|
+
- `transform(edit): void` — Update all key positions after an edit.
|
|
142
|
+
|
|
143
|
+
### `@b9g/revise/state.js`
|
|
144
|
+
|
|
145
|
+
**`EditableState`** — A framework-agnostic state coordinator that ties
|
|
146
|
+
together value, keyer, history, and selection.
|
|
147
|
+
|
|
148
|
+
- `value: string` — The current text.
|
|
149
|
+
- `keyer: Keyer` — The keyer for stable line keys.
|
|
150
|
+
- `history: EditHistory` — The undo/redo history.
|
|
151
|
+
- `selection: SelectionRange | undefined` — Selection computed from the last edit.
|
|
152
|
+
- `applyEdit(edit, options?): void` — Apply an edit, updating value/keyer/history/selection.
|
|
153
|
+
- `setValue(newValue, options?): void` — Set a new value by diffing against the current value.
|
|
154
|
+
- `undo(): boolean` — Undo the last edit group.
|
|
155
|
+
- `redo(): boolean` — Redo the last undone edit group.
|
|
156
|
+
- `canUndo(): boolean`
|
|
157
|
+
- `canRedo(): boolean`
|
|
158
|
+
- `checkpoint(): void` — Break the current edit group.
|
|
159
|
+
- `reset(value?): void` — Reset all state.
|
package/package.json
CHANGED
|
@@ -1,44 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/revise",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@b9g/libuild": "file:../libuild",
|
|
7
|
+
"playwright-test": "^8.1.1",
|
|
8
|
+
"typescript": "^5.0.0"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
11
|
+
"types": "src/contentarea.d.ts",
|
|
12
|
+
"main": "src/contentarea.cjs",
|
|
13
|
+
"module": "src/contentarea.js",
|
|
6
14
|
"exports": {
|
|
7
15
|
"./edit.js": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
16
|
+
"types": "./src/edit.d.ts",
|
|
17
|
+
"import": "./src/edit.js",
|
|
18
|
+
"require": "./src/edit.cjs"
|
|
10
19
|
},
|
|
11
20
|
"./contentarea.js": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
21
|
+
"types": "./src/contentarea.d.ts",
|
|
22
|
+
"import": "./src/contentarea.js",
|
|
23
|
+
"require": "./src/contentarea.cjs"
|
|
14
24
|
},
|
|
15
25
|
"./keyer.js": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
26
|
+
"types": "./src/keyer.d.ts",
|
|
27
|
+
"import": "./src/keyer.js",
|
|
28
|
+
"require": "./src/keyer.cjs"
|
|
18
29
|
},
|
|
19
30
|
"./history.js": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
"types": "./src/history.d.ts",
|
|
32
|
+
"import": "./src/history.js",
|
|
33
|
+
"require": "./src/history.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./state.js": {
|
|
36
|
+
"types": "./src/state.d.ts",
|
|
37
|
+
"import": "./src/state.js",
|
|
38
|
+
"require": "./src/state.cjs"
|
|
39
|
+
},
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./src/contentarea.d.ts",
|
|
42
|
+
"import": "./src/contentarea.js",
|
|
43
|
+
"require": "./src/contentarea.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./contentarea": {
|
|
46
|
+
"types": "./src/contentarea.d.ts",
|
|
47
|
+
"import": "./src/contentarea.js",
|
|
48
|
+
"require": "./src/contentarea.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./edit": {
|
|
51
|
+
"types": "./src/edit.d.ts",
|
|
52
|
+
"import": "./src/edit.js",
|
|
53
|
+
"require": "./src/edit.cjs"
|
|
54
|
+
},
|
|
55
|
+
"./history": {
|
|
56
|
+
"types": "./src/history.d.ts",
|
|
57
|
+
"import": "./src/history.js",
|
|
58
|
+
"require": "./src/history.cjs"
|
|
59
|
+
},
|
|
60
|
+
"./keyer": {
|
|
61
|
+
"types": "./src/keyer.d.ts",
|
|
62
|
+
"import": "./src/keyer.js",
|
|
63
|
+
"require": "./src/keyer.cjs"
|
|
64
|
+
},
|
|
65
|
+
"./state": {
|
|
66
|
+
"types": "./src/state.d.ts",
|
|
67
|
+
"import": "./src/state.js",
|
|
68
|
+
"require": "./src/state.cjs"
|
|
69
|
+
},
|
|
70
|
+
"./package.json": "./package.json"
|
|
43
71
|
}
|
|
44
|
-
}
|
|
72
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export type Subseq = Array<number>;
|
|
2
|
-
export declare function measure(subseq: Subseq): {
|
|
3
|
-
length: number;
|
|
4
|
-
includedLength: number;
|
|
5
|
-
excludedLength: number;
|
|
6
|
-
};
|
|
7
|
-
export declare function pushSegment(subseq: Subseq, length: number, included: boolean): void;
|
|
8
|
-
export declare function contains(subseq: Subseq, index: number): boolean;
|
|
9
|
-
export declare function clear(subseq: Subseq): Subseq;
|
|
10
|
-
export declare function fill(subseq: Subseq): Subseq;
|
|
11
|
-
export declare function complement(subseq: Subseq): Subseq;
|
|
12
|
-
export declare function align(subseq1: Subseq, subseq2: Subseq): Array<[number, boolean, boolean]>;
|
|
13
|
-
export declare function union(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
14
|
-
export declare function intersection(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
15
|
-
export declare function difference(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
16
|
-
export declare function shrink(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
17
|
-
export declare function expand(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
18
|
-
export declare function interleave(subseq1: Subseq, subseq2: Subseq): [Subseq, Subseq];
|
|
1
|
+
export type Subseq = Array<number>;
|
|
2
|
+
export declare function measure(subseq: Subseq): {
|
|
3
|
+
length: number;
|
|
4
|
+
includedLength: number;
|
|
5
|
+
excludedLength: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function pushSegment(subseq: Subseq, length: number, included: boolean): void;
|
|
8
|
+
export declare function contains(subseq: Subseq, index: number): boolean;
|
|
9
|
+
export declare function clear(subseq: Subseq): Subseq;
|
|
10
|
+
export declare function fill(subseq: Subseq): Subseq;
|
|
11
|
+
export declare function complement(subseq: Subseq): Subseq;
|
|
12
|
+
export declare function align(subseq1: Subseq, subseq2: Subseq): Array<[number, boolean, boolean]>;
|
|
13
|
+
export declare function union(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
14
|
+
export declare function intersection(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
15
|
+
export declare function difference(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
16
|
+
export declare function shrink(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
17
|
+
export declare function expand(subseq1: Subseq, subseq2: Subseq): Subseq;
|
|
18
|
+
export declare function interleave(subseq1: Subseq, subseq2: Subseq): [Subseq, Subseq];
|