@shadow-garden/bapbong-input-bridge 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 +35 -0
- package/dist/index.cjs +300 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +268 -0
- package/dist/lib/input-bridge.d.ts +120 -0
- package/dist/lib/input-bridge.d.ts.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Le Phuoc Minh
|
|
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,35 @@
|
|
|
1
|
+
# @shadow-garden/bapbong-input-bridge
|
|
2
|
+
|
|
3
|
+
The canvas can't receive text input, so this package wraps a **hidden
|
|
4
|
+
ProseMirror `EditorView`** (a real, visually-invisible contenteditable) as the
|
|
5
|
+
input sink. The browser delivers keyboard and **IME composition** (Vietnamese,
|
|
6
|
+
CJK, …) to it; ProseMirror owns the document, undo history and clipboard. Every
|
|
7
|
+
transaction fires `onUpdate`, where the host re-runs layout and repaints.
|
|
8
|
+
|
|
9
|
+
- **Scope:** `scope:input`
|
|
10
|
+
- **Depends on:** `@shadow-garden/bapbong-model`, `prosemirror-{state,view,model,commands,keymap,history,transform}`
|
|
11
|
+
|
|
12
|
+
## What it provides
|
|
13
|
+
|
|
14
|
+
- **`InputBridge`** — the hidden editor: `state`, `dispatch(tr)`, `setSelection`,
|
|
15
|
+
`selectWordAt`, `place(x, y, h)` (anchors the IME popup at the caret), `focus`.
|
|
16
|
+
- **Layout-aware commands:** `moveCaretCommand`, `splitListItem`, `wordRangeAt`.
|
|
17
|
+
- **Comment authoring** (undoable transactions on `doc.attrs.comments`):
|
|
18
|
+
`addCommentTr`, `replyCommentTr`, `resolveCommentTr`, `editCommentTr`,
|
|
19
|
+
`deleteCommentTr`.
|
|
20
|
+
- **`CommentComposer`** — a small standalone PM editor for comment bodies, with an
|
|
21
|
+
optional **@mention** suggestion plugin (`MentionHandlers` / `MentionUser` /
|
|
22
|
+
`MentionCoords`), a `placeholder`, and `onEnter` / `onEscape` hooks.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const bridge = new InputBridge({ doc, keys: { ArrowUp, ArrowDown }, onUpdate: repaint });
|
|
26
|
+
container.appendChild(bridge.dom);
|
|
27
|
+
bridge.setSelection(pos); bridge.focus(); bridge.place(x, y, h);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Build / test
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm nx build @shadow-garden/bapbong-input-bridge
|
|
34
|
+
pnpm nx test @shadow-garden/bapbong-input-bridge
|
|
35
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/input-bridge/src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CommentComposer: () => CommentComposer,
|
|
24
|
+
InputBridge: () => InputBridge,
|
|
25
|
+
createEditingState: () => createEditingState,
|
|
26
|
+
moveCaretCommand: () => moveCaretCommand,
|
|
27
|
+
splitListItem: () => splitListItem,
|
|
28
|
+
wordRangeAt: () => wordRangeAt
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// packages/input-bridge/src/lib/input-bridge.ts
|
|
33
|
+
var import_prosemirror_commands = require("prosemirror-commands");
|
|
34
|
+
var import_prosemirror_history = require("prosemirror-history");
|
|
35
|
+
var import_prosemirror_keymap = require("prosemirror-keymap");
|
|
36
|
+
var import_prosemirror_state = require("prosemirror-state");
|
|
37
|
+
var import_prosemirror_transform = require("prosemirror-transform");
|
|
38
|
+
var import_prosemirror_view = require("prosemirror-view");
|
|
39
|
+
function moveCaretCommand(compute, extend = false) {
|
|
40
|
+
return (state, dispatch) => {
|
|
41
|
+
const pos = compute(state);
|
|
42
|
+
if (pos == null) return false;
|
|
43
|
+
const sel = extend ? import_prosemirror_state.TextSelection.create(state.doc, state.selection.anchor, pos) : import_prosemirror_state.TextSelection.create(state.doc, pos);
|
|
44
|
+
dispatch?.(state.tr.setSelection(sel));
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
var splitListItem = (state, dispatch) => {
|
|
49
|
+
const { $from, $to } = state.selection;
|
|
50
|
+
const parent = $from.parent;
|
|
51
|
+
if (!parent.isTextblock || !parent.attrs["list"]) return false;
|
|
52
|
+
if ($from.parent !== $to.parent) return false;
|
|
53
|
+
if (parent.content.size === 0) {
|
|
54
|
+
dispatch?.(
|
|
55
|
+
state.tr.setNodeMarkup($from.before(), null, { ...parent.attrs, list: null })
|
|
56
|
+
);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const tr = state.tr.deleteSelection();
|
|
60
|
+
const pos = tr.selection.from;
|
|
61
|
+
if (!(0, import_prosemirror_transform.canSplit)(tr.doc, pos)) return false;
|
|
62
|
+
tr.split(pos, 1, [{ type: parent.type, attrs: parent.attrs }]);
|
|
63
|
+
dispatch?.(tr);
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
var WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
67
|
+
function wordRangeAt(doc, pos) {
|
|
68
|
+
if (pos < 0 || pos > doc.content.size) return null;
|
|
69
|
+
const $pos = doc.resolve(pos);
|
|
70
|
+
const parent = $pos.parent;
|
|
71
|
+
if (!parent.isTextblock) return null;
|
|
72
|
+
const text = parent.textBetween(0, parent.content.size, void 0, "\uFFFC");
|
|
73
|
+
const offset = pos - $pos.start();
|
|
74
|
+
let from = offset;
|
|
75
|
+
let to = offset;
|
|
76
|
+
while (from > 0 && WORD_CHAR.test(text[from - 1])) from--;
|
|
77
|
+
while (to < text.length && WORD_CHAR.test(text[to])) to++;
|
|
78
|
+
if (from === to) return null;
|
|
79
|
+
return { from: $pos.start() + from, to: $pos.start() + to };
|
|
80
|
+
}
|
|
81
|
+
function createEditingState(doc, keys = {}) {
|
|
82
|
+
return import_prosemirror_state.EditorState.create({
|
|
83
|
+
doc,
|
|
84
|
+
plugins: [
|
|
85
|
+
(0, import_prosemirror_history.history)(),
|
|
86
|
+
(0, import_prosemirror_keymap.keymap)({ "Mod-z": import_prosemirror_history.undo, "Shift-Mod-z": import_prosemirror_history.redo, "Mod-y": import_prosemirror_history.redo }),
|
|
87
|
+
(0, import_prosemirror_keymap.keymap)(keys),
|
|
88
|
+
(0, import_prosemirror_keymap.keymap)(import_prosemirror_commands.baseKeymap)
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
var InputBridge = class {
|
|
93
|
+
/** Clip layer appended near the canvas: it fills the positioned ancestor
|
|
94
|
+
* and `overflow: hidden`s the 800px-wide host riding the caret inside —
|
|
95
|
+
* otherwise that tail pokes past the right page edge and gifts the
|
|
96
|
+
* scroll viewport a horizontal scrollbar. */
|
|
97
|
+
dom;
|
|
98
|
+
/** The hidden editor's host — the element `place()` actually moves. */
|
|
99
|
+
host;
|
|
100
|
+
view;
|
|
101
|
+
constructor(options) {
|
|
102
|
+
this.dom = document.createElement("div");
|
|
103
|
+
this.dom.className = "bapbong-input-bridge";
|
|
104
|
+
Object.assign(this.dom.style, {
|
|
105
|
+
position: "absolute",
|
|
106
|
+
inset: "0",
|
|
107
|
+
overflow: "hidden",
|
|
108
|
+
pointerEvents: "none",
|
|
109
|
+
zIndex: "-1"
|
|
110
|
+
});
|
|
111
|
+
this.host = document.createElement("div");
|
|
112
|
+
Object.assign(this.host.style, {
|
|
113
|
+
position: "absolute",
|
|
114
|
+
top: "0",
|
|
115
|
+
left: "0",
|
|
116
|
+
// MUST be wide: a 1px host makes the hidden document wrap one character
|
|
117
|
+
// per line (tens of thousands of line boxes), and every DOM-selection
|
|
118
|
+
// placement then re-runs that layout — ~500ms PER CLICK on multi-page
|
|
119
|
+
// documents. At a page-ish width the same layout is ~10ms and stays
|
|
120
|
+
// warm. The host is invisible either way (opacity 0, clipped height).
|
|
121
|
+
width: "800px",
|
|
122
|
+
height: "1em",
|
|
123
|
+
overflow: "hidden",
|
|
124
|
+
opacity: "0",
|
|
125
|
+
// Keep it focusable but out of the way of canvas pointer events.
|
|
126
|
+
pointerEvents: "none"
|
|
127
|
+
});
|
|
128
|
+
this.dom.appendChild(this.host);
|
|
129
|
+
this.view = new import_prosemirror_view.EditorView(this.host, {
|
|
130
|
+
state: createEditingState(options.doc, options.keys),
|
|
131
|
+
dispatchTransaction: (tr) => {
|
|
132
|
+
const state = this.view.state.apply(tr);
|
|
133
|
+
this.view.updateState(state);
|
|
134
|
+
options.onUpdate(state, tr);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
get state() {
|
|
139
|
+
return this.view.state;
|
|
140
|
+
}
|
|
141
|
+
/** Dispatch a transaction (e.g. a comment authoring command) through the
|
|
142
|
+
* editor, so it routes through onUpdate + history like any edit. */
|
|
143
|
+
dispatch(tr) {
|
|
144
|
+
this.view.dispatch(tr);
|
|
145
|
+
}
|
|
146
|
+
focus() {
|
|
147
|
+
this.view.focus();
|
|
148
|
+
}
|
|
149
|
+
/** Set a text selection (collapsed caret or anchor→head range). Positions
|
|
150
|
+
* are clamped to the nearest valid text slots. */
|
|
151
|
+
setSelection(anchor, head = anchor) {
|
|
152
|
+
const { doc } = this.view.state;
|
|
153
|
+
const clamp = (p) => Math.max(0, Math.min(p, doc.content.size));
|
|
154
|
+
const sel = import_prosemirror_state.TextSelection.between(doc.resolve(clamp(anchor)), doc.resolve(clamp(head)));
|
|
155
|
+
this.view.dispatch(this.view.state.tr.setSelection(sel));
|
|
156
|
+
}
|
|
157
|
+
/** Select the word around `pos` (double-click); falls back to a collapsed
|
|
158
|
+
* caret when there is no word at that position. */
|
|
159
|
+
selectWordAt(pos) {
|
|
160
|
+
const range = wordRangeAt(this.view.state.doc, pos);
|
|
161
|
+
if (range) this.setSelection(range.from, range.to);
|
|
162
|
+
else this.setSelection(pos);
|
|
163
|
+
}
|
|
164
|
+
/** Move the hidden editor to the painted caret (CSS px, relative to the
|
|
165
|
+
* positioned ancestor) so IME popups anchor in the right place. */
|
|
166
|
+
place(x, y, height) {
|
|
167
|
+
this.host.style.transform = `translate(${x}px, ${y}px)`;
|
|
168
|
+
this.host.style.height = `${height}px`;
|
|
169
|
+
}
|
|
170
|
+
destroy() {
|
|
171
|
+
this.view.destroy();
|
|
172
|
+
this.dom.remove();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var mentionKey = new import_prosemirror_state.PluginKey("mention");
|
|
176
|
+
var INACTIVE = { active: false, from: 0, query: "" };
|
|
177
|
+
var MENTION_RE = /(?:^|\s)@([\p{L}\p{N}_]*)$/u;
|
|
178
|
+
function placeholderPlugin(text) {
|
|
179
|
+
return new import_prosemirror_state.Plugin({
|
|
180
|
+
props: {
|
|
181
|
+
decorations(state) {
|
|
182
|
+
const doc = state.doc;
|
|
183
|
+
const first = doc.firstChild;
|
|
184
|
+
if (doc.childCount === 1 && first?.isTextblock && first.content.size === 0) {
|
|
185
|
+
return import_prosemirror_view.DecorationSet.create(doc, [
|
|
186
|
+
import_prosemirror_view.Decoration.node(0, first.nodeSize, { class: "is-empty", "data-placeholder": text })
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function mentionPlugin(handlers) {
|
|
195
|
+
return new import_prosemirror_state.Plugin({
|
|
196
|
+
key: mentionKey,
|
|
197
|
+
state: {
|
|
198
|
+
init: () => INACTIVE,
|
|
199
|
+
apply(tr) {
|
|
200
|
+
const sel = tr.selection;
|
|
201
|
+
if (!sel.empty) return INACTIVE;
|
|
202
|
+
const $from = sel.$from;
|
|
203
|
+
const textBefore = $from.parent.textBetween(0, $from.parentOffset, "\n", "\uFFFC");
|
|
204
|
+
const m = MENTION_RE.exec(textBefore);
|
|
205
|
+
if (!m) return INACTIVE;
|
|
206
|
+
const query = m[1];
|
|
207
|
+
return { active: true, from: $from.pos - query.length - 1, query };
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
view: (editorView) => {
|
|
211
|
+
const emit = (view) => {
|
|
212
|
+
const st = mentionKey.getState(view.state);
|
|
213
|
+
if (!st?.active) return handlers.query(null);
|
|
214
|
+
const c = view.coordsAtPos(view.state.selection.from);
|
|
215
|
+
handlers.query({ query: st.query, coords: { left: c.left, top: c.top, bottom: c.bottom } });
|
|
216
|
+
};
|
|
217
|
+
emit(editorView);
|
|
218
|
+
return { update: emit, destroy: () => handlers.query(null) };
|
|
219
|
+
},
|
|
220
|
+
props: {
|
|
221
|
+
handleKeyDown(view, event) {
|
|
222
|
+
if (!mentionKey.getState(view.state)?.active) return false;
|
|
223
|
+
const map = {
|
|
224
|
+
ArrowUp: "up",
|
|
225
|
+
ArrowDown: "down",
|
|
226
|
+
Enter: "enter",
|
|
227
|
+
Tab: "enter",
|
|
228
|
+
Escape: "esc"
|
|
229
|
+
};
|
|
230
|
+
const k = map[event.key];
|
|
231
|
+
if (k && handlers.key(k)) {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
var CommentComposer = class {
|
|
241
|
+
constructor(schema, mount, initialDoc, mention, opts) {
|
|
242
|
+
this.schema = schema;
|
|
243
|
+
const doc = initialDoc ? schema.nodeFromJSON(initialDoc) : void 0;
|
|
244
|
+
const plugins = [(0, import_prosemirror_history.history)()];
|
|
245
|
+
if (opts?.placeholder) plugins.push(placeholderPlugin(opts.placeholder));
|
|
246
|
+
if (mention && schema.nodes["mention"]) plugins.push(mentionPlugin(mention));
|
|
247
|
+
const keys = {};
|
|
248
|
+
if (opts?.onEnter) {
|
|
249
|
+
keys["Enter"] = () => (opts.onEnter?.(), true);
|
|
250
|
+
keys["Shift-Enter"] = import_prosemirror_commands.baseKeymap["Enter"];
|
|
251
|
+
}
|
|
252
|
+
if (opts?.onEscape) keys["Escape"] = () => (opts.onEscape?.(), true);
|
|
253
|
+
if (Object.keys(keys).length) plugins.push((0, import_prosemirror_keymap.keymap)(keys));
|
|
254
|
+
plugins.push((0, import_prosemirror_keymap.keymap)({ "Mod-z": import_prosemirror_history.undo, "Shift-Mod-z": import_prosemirror_history.redo }), (0, import_prosemirror_keymap.keymap)(import_prosemirror_commands.baseKeymap));
|
|
255
|
+
this.view = new import_prosemirror_view.EditorView(mount, {
|
|
256
|
+
state: import_prosemirror_state.EditorState.create({ ...doc ? { doc } : { schema }, plugins })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
schema;
|
|
260
|
+
view;
|
|
261
|
+
/** Replace the active "@query" with a mention node + trailing space. */
|
|
262
|
+
applyMention(user) {
|
|
263
|
+
const st = mentionKey.getState(this.view.state);
|
|
264
|
+
if (!st?.active) return;
|
|
265
|
+
const to = this.view.state.selection.from;
|
|
266
|
+
const node = this.schema.nodes["mention"].create({ id: user.id, label: user.label });
|
|
267
|
+
const tr = this.view.state.tr.replaceWith(st.from, to, [node, this.schema.text(" ")]);
|
|
268
|
+
tr.setSelection(import_prosemirror_state.TextSelection.create(tr.doc, st.from + 2));
|
|
269
|
+
this.view.dispatch(tr);
|
|
270
|
+
this.view.focus();
|
|
271
|
+
}
|
|
272
|
+
/** Whether the composer has no visible text. */
|
|
273
|
+
isEmpty() {
|
|
274
|
+
return this.view.state.doc.textContent.trim().length === 0;
|
|
275
|
+
}
|
|
276
|
+
/** The composed body as commentSchema doc JSON. */
|
|
277
|
+
getJSON() {
|
|
278
|
+
return this.view.state.doc.toJSON();
|
|
279
|
+
}
|
|
280
|
+
/** Reset to an empty document. */
|
|
281
|
+
clear() {
|
|
282
|
+
const empty = import_prosemirror_state.EditorState.create({ schema: this.schema, plugins: this.view.state.plugins });
|
|
283
|
+
this.view.updateState(empty);
|
|
284
|
+
}
|
|
285
|
+
focus() {
|
|
286
|
+
this.view.focus();
|
|
287
|
+
}
|
|
288
|
+
destroy() {
|
|
289
|
+
this.view.destroy();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
293
|
+
0 && (module.exports = {
|
|
294
|
+
CommentComposer,
|
|
295
|
+
InputBridge,
|
|
296
|
+
createEditingState,
|
|
297
|
+
moveCaretCommand,
|
|
298
|
+
splitListItem,
|
|
299
|
+
wordRangeAt
|
|
300
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// packages/input-bridge/src/lib/input-bridge.ts
|
|
2
|
+
import { baseKeymap } from "prosemirror-commands";
|
|
3
|
+
import { history, redo, undo } from "prosemirror-history";
|
|
4
|
+
import { keymap } from "prosemirror-keymap";
|
|
5
|
+
import { EditorState, Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
|
6
|
+
import { canSplit } from "prosemirror-transform";
|
|
7
|
+
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
8
|
+
function moveCaretCommand(compute, extend = false) {
|
|
9
|
+
return (state, dispatch) => {
|
|
10
|
+
const pos = compute(state);
|
|
11
|
+
if (pos == null) return false;
|
|
12
|
+
const sel = extend ? TextSelection.create(state.doc, state.selection.anchor, pos) : TextSelection.create(state.doc, pos);
|
|
13
|
+
dispatch?.(state.tr.setSelection(sel));
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
var splitListItem = (state, dispatch) => {
|
|
18
|
+
const { $from, $to } = state.selection;
|
|
19
|
+
const parent = $from.parent;
|
|
20
|
+
if (!parent.isTextblock || !parent.attrs["list"]) return false;
|
|
21
|
+
if ($from.parent !== $to.parent) return false;
|
|
22
|
+
if (parent.content.size === 0) {
|
|
23
|
+
dispatch?.(
|
|
24
|
+
state.tr.setNodeMarkup($from.before(), null, { ...parent.attrs, list: null })
|
|
25
|
+
);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const tr = state.tr.deleteSelection();
|
|
29
|
+
const pos = tr.selection.from;
|
|
30
|
+
if (!canSplit(tr.doc, pos)) return false;
|
|
31
|
+
tr.split(pos, 1, [{ type: parent.type, attrs: parent.attrs }]);
|
|
32
|
+
dispatch?.(tr);
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
var WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
36
|
+
function wordRangeAt(doc, pos) {
|
|
37
|
+
if (pos < 0 || pos > doc.content.size) return null;
|
|
38
|
+
const $pos = doc.resolve(pos);
|
|
39
|
+
const parent = $pos.parent;
|
|
40
|
+
if (!parent.isTextblock) return null;
|
|
41
|
+
const text = parent.textBetween(0, parent.content.size, void 0, "\uFFFC");
|
|
42
|
+
const offset = pos - $pos.start();
|
|
43
|
+
let from = offset;
|
|
44
|
+
let to = offset;
|
|
45
|
+
while (from > 0 && WORD_CHAR.test(text[from - 1])) from--;
|
|
46
|
+
while (to < text.length && WORD_CHAR.test(text[to])) to++;
|
|
47
|
+
if (from === to) return null;
|
|
48
|
+
return { from: $pos.start() + from, to: $pos.start() + to };
|
|
49
|
+
}
|
|
50
|
+
function createEditingState(doc, keys = {}) {
|
|
51
|
+
return EditorState.create({
|
|
52
|
+
doc,
|
|
53
|
+
plugins: [
|
|
54
|
+
history(),
|
|
55
|
+
keymap({ "Mod-z": undo, "Shift-Mod-z": redo, "Mod-y": redo }),
|
|
56
|
+
keymap(keys),
|
|
57
|
+
keymap(baseKeymap)
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
var InputBridge = class {
|
|
62
|
+
/** Clip layer appended near the canvas: it fills the positioned ancestor
|
|
63
|
+
* and `overflow: hidden`s the 800px-wide host riding the caret inside —
|
|
64
|
+
* otherwise that tail pokes past the right page edge and gifts the
|
|
65
|
+
* scroll viewport a horizontal scrollbar. */
|
|
66
|
+
dom;
|
|
67
|
+
/** The hidden editor's host — the element `place()` actually moves. */
|
|
68
|
+
host;
|
|
69
|
+
view;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.dom = document.createElement("div");
|
|
72
|
+
this.dom.className = "bapbong-input-bridge";
|
|
73
|
+
Object.assign(this.dom.style, {
|
|
74
|
+
position: "absolute",
|
|
75
|
+
inset: "0",
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
pointerEvents: "none",
|
|
78
|
+
zIndex: "-1"
|
|
79
|
+
});
|
|
80
|
+
this.host = document.createElement("div");
|
|
81
|
+
Object.assign(this.host.style, {
|
|
82
|
+
position: "absolute",
|
|
83
|
+
top: "0",
|
|
84
|
+
left: "0",
|
|
85
|
+
// MUST be wide: a 1px host makes the hidden document wrap one character
|
|
86
|
+
// per line (tens of thousands of line boxes), and every DOM-selection
|
|
87
|
+
// placement then re-runs that layout — ~500ms PER CLICK on multi-page
|
|
88
|
+
// documents. At a page-ish width the same layout is ~10ms and stays
|
|
89
|
+
// warm. The host is invisible either way (opacity 0, clipped height).
|
|
90
|
+
width: "800px",
|
|
91
|
+
height: "1em",
|
|
92
|
+
overflow: "hidden",
|
|
93
|
+
opacity: "0",
|
|
94
|
+
// Keep it focusable but out of the way of canvas pointer events.
|
|
95
|
+
pointerEvents: "none"
|
|
96
|
+
});
|
|
97
|
+
this.dom.appendChild(this.host);
|
|
98
|
+
this.view = new EditorView(this.host, {
|
|
99
|
+
state: createEditingState(options.doc, options.keys),
|
|
100
|
+
dispatchTransaction: (tr) => {
|
|
101
|
+
const state = this.view.state.apply(tr);
|
|
102
|
+
this.view.updateState(state);
|
|
103
|
+
options.onUpdate(state, tr);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
get state() {
|
|
108
|
+
return this.view.state;
|
|
109
|
+
}
|
|
110
|
+
/** Dispatch a transaction (e.g. a comment authoring command) through the
|
|
111
|
+
* editor, so it routes through onUpdate + history like any edit. */
|
|
112
|
+
dispatch(tr) {
|
|
113
|
+
this.view.dispatch(tr);
|
|
114
|
+
}
|
|
115
|
+
focus() {
|
|
116
|
+
this.view.focus();
|
|
117
|
+
}
|
|
118
|
+
/** Set a text selection (collapsed caret or anchor→head range). Positions
|
|
119
|
+
* are clamped to the nearest valid text slots. */
|
|
120
|
+
setSelection(anchor, head = anchor) {
|
|
121
|
+
const { doc } = this.view.state;
|
|
122
|
+
const clamp = (p) => Math.max(0, Math.min(p, doc.content.size));
|
|
123
|
+
const sel = TextSelection.between(doc.resolve(clamp(anchor)), doc.resolve(clamp(head)));
|
|
124
|
+
this.view.dispatch(this.view.state.tr.setSelection(sel));
|
|
125
|
+
}
|
|
126
|
+
/** Select the word around `pos` (double-click); falls back to a collapsed
|
|
127
|
+
* caret when there is no word at that position. */
|
|
128
|
+
selectWordAt(pos) {
|
|
129
|
+
const range = wordRangeAt(this.view.state.doc, pos);
|
|
130
|
+
if (range) this.setSelection(range.from, range.to);
|
|
131
|
+
else this.setSelection(pos);
|
|
132
|
+
}
|
|
133
|
+
/** Move the hidden editor to the painted caret (CSS px, relative to the
|
|
134
|
+
* positioned ancestor) so IME popups anchor in the right place. */
|
|
135
|
+
place(x, y, height) {
|
|
136
|
+
this.host.style.transform = `translate(${x}px, ${y}px)`;
|
|
137
|
+
this.host.style.height = `${height}px`;
|
|
138
|
+
}
|
|
139
|
+
destroy() {
|
|
140
|
+
this.view.destroy();
|
|
141
|
+
this.dom.remove();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var mentionKey = new PluginKey("mention");
|
|
145
|
+
var INACTIVE = { active: false, from: 0, query: "" };
|
|
146
|
+
var MENTION_RE = /(?:^|\s)@([\p{L}\p{N}_]*)$/u;
|
|
147
|
+
function placeholderPlugin(text) {
|
|
148
|
+
return new Plugin({
|
|
149
|
+
props: {
|
|
150
|
+
decorations(state) {
|
|
151
|
+
const doc = state.doc;
|
|
152
|
+
const first = doc.firstChild;
|
|
153
|
+
if (doc.childCount === 1 && first?.isTextblock && first.content.size === 0) {
|
|
154
|
+
return DecorationSet.create(doc, [
|
|
155
|
+
Decoration.node(0, first.nodeSize, { class: "is-empty", "data-placeholder": text })
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function mentionPlugin(handlers) {
|
|
164
|
+
return new Plugin({
|
|
165
|
+
key: mentionKey,
|
|
166
|
+
state: {
|
|
167
|
+
init: () => INACTIVE,
|
|
168
|
+
apply(tr) {
|
|
169
|
+
const sel = tr.selection;
|
|
170
|
+
if (!sel.empty) return INACTIVE;
|
|
171
|
+
const $from = sel.$from;
|
|
172
|
+
const textBefore = $from.parent.textBetween(0, $from.parentOffset, "\n", "\uFFFC");
|
|
173
|
+
const m = MENTION_RE.exec(textBefore);
|
|
174
|
+
if (!m) return INACTIVE;
|
|
175
|
+
const query = m[1];
|
|
176
|
+
return { active: true, from: $from.pos - query.length - 1, query };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
view: (editorView) => {
|
|
180
|
+
const emit = (view) => {
|
|
181
|
+
const st = mentionKey.getState(view.state);
|
|
182
|
+
if (!st?.active) return handlers.query(null);
|
|
183
|
+
const c = view.coordsAtPos(view.state.selection.from);
|
|
184
|
+
handlers.query({ query: st.query, coords: { left: c.left, top: c.top, bottom: c.bottom } });
|
|
185
|
+
};
|
|
186
|
+
emit(editorView);
|
|
187
|
+
return { update: emit, destroy: () => handlers.query(null) };
|
|
188
|
+
},
|
|
189
|
+
props: {
|
|
190
|
+
handleKeyDown(view, event) {
|
|
191
|
+
if (!mentionKey.getState(view.state)?.active) return false;
|
|
192
|
+
const map = {
|
|
193
|
+
ArrowUp: "up",
|
|
194
|
+
ArrowDown: "down",
|
|
195
|
+
Enter: "enter",
|
|
196
|
+
Tab: "enter",
|
|
197
|
+
Escape: "esc"
|
|
198
|
+
};
|
|
199
|
+
const k = map[event.key];
|
|
200
|
+
if (k && handlers.key(k)) {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
var CommentComposer = class {
|
|
210
|
+
constructor(schema, mount, initialDoc, mention, opts) {
|
|
211
|
+
this.schema = schema;
|
|
212
|
+
const doc = initialDoc ? schema.nodeFromJSON(initialDoc) : void 0;
|
|
213
|
+
const plugins = [history()];
|
|
214
|
+
if (opts?.placeholder) plugins.push(placeholderPlugin(opts.placeholder));
|
|
215
|
+
if (mention && schema.nodes["mention"]) plugins.push(mentionPlugin(mention));
|
|
216
|
+
const keys = {};
|
|
217
|
+
if (opts?.onEnter) {
|
|
218
|
+
keys["Enter"] = () => (opts.onEnter?.(), true);
|
|
219
|
+
keys["Shift-Enter"] = baseKeymap["Enter"];
|
|
220
|
+
}
|
|
221
|
+
if (opts?.onEscape) keys["Escape"] = () => (opts.onEscape?.(), true);
|
|
222
|
+
if (Object.keys(keys).length) plugins.push(keymap(keys));
|
|
223
|
+
plugins.push(keymap({ "Mod-z": undo, "Shift-Mod-z": redo }), keymap(baseKeymap));
|
|
224
|
+
this.view = new EditorView(mount, {
|
|
225
|
+
state: EditorState.create({ ...doc ? { doc } : { schema }, plugins })
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
schema;
|
|
229
|
+
view;
|
|
230
|
+
/** Replace the active "@query" with a mention node + trailing space. */
|
|
231
|
+
applyMention(user) {
|
|
232
|
+
const st = mentionKey.getState(this.view.state);
|
|
233
|
+
if (!st?.active) return;
|
|
234
|
+
const to = this.view.state.selection.from;
|
|
235
|
+
const node = this.schema.nodes["mention"].create({ id: user.id, label: user.label });
|
|
236
|
+
const tr = this.view.state.tr.replaceWith(st.from, to, [node, this.schema.text(" ")]);
|
|
237
|
+
tr.setSelection(TextSelection.create(tr.doc, st.from + 2));
|
|
238
|
+
this.view.dispatch(tr);
|
|
239
|
+
this.view.focus();
|
|
240
|
+
}
|
|
241
|
+
/** Whether the composer has no visible text. */
|
|
242
|
+
isEmpty() {
|
|
243
|
+
return this.view.state.doc.textContent.trim().length === 0;
|
|
244
|
+
}
|
|
245
|
+
/** The composed body as commentSchema doc JSON. */
|
|
246
|
+
getJSON() {
|
|
247
|
+
return this.view.state.doc.toJSON();
|
|
248
|
+
}
|
|
249
|
+
/** Reset to an empty document. */
|
|
250
|
+
clear() {
|
|
251
|
+
const empty = EditorState.create({ schema: this.schema, plugins: this.view.state.plugins });
|
|
252
|
+
this.view.updateState(empty);
|
|
253
|
+
}
|
|
254
|
+
focus() {
|
|
255
|
+
this.view.focus();
|
|
256
|
+
}
|
|
257
|
+
destroy() {
|
|
258
|
+
this.view.destroy();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
export {
|
|
262
|
+
CommentComposer,
|
|
263
|
+
InputBridge,
|
|
264
|
+
createEditingState,
|
|
265
|
+
moveCaretCommand,
|
|
266
|
+
splitListItem,
|
|
267
|
+
wordRangeAt
|
|
268
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Node as PMNode, Schema } from 'prosemirror-model';
|
|
2
|
+
import { EditorState, type Command, type Transaction } from 'prosemirror-state';
|
|
3
|
+
import { EditorView } from 'prosemirror-view';
|
|
4
|
+
export type { Command, EditorState, Transaction } from 'prosemirror-state';
|
|
5
|
+
/** A command that moves the caret to the position computed by `compute`
|
|
6
|
+
* (e.g. layout-aware ArrowUp/ArrowDown from bapbong-selection). With `extend`,
|
|
7
|
+
* the anchor stays put and only the head moves (Shift+arrow selection).
|
|
8
|
+
* Returns false — leaving the key to the next handler — when `compute`
|
|
9
|
+
* yields null. */
|
|
10
|
+
export declare function moveCaretCommand(compute: (state: EditorState) => number | null, extend?: boolean): Command;
|
|
11
|
+
/** Enter inside a list item: split the paragraph KEEPING its list attrs (the
|
|
12
|
+
* layout engine recounts markers, so the new item numbers itself and
|
|
13
|
+
* everything below renumbers). Enter on an EMPTY item exits the list, like
|
|
14
|
+
* Word. Returns false outside lists so the base keymap takes over. */
|
|
15
|
+
export declare const splitListItem: Command;
|
|
16
|
+
/** The word range around `pos` (for double-click selection), or null when the
|
|
17
|
+
* position isn't inside a textblock or doesn't touch a word character.
|
|
18
|
+
* Unicode-aware: Vietnamese diacritics count as word characters. */
|
|
19
|
+
export declare function wordRangeAt(doc: PMNode, pos: number): {
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
} | null;
|
|
23
|
+
export interface InputBridgeOptions {
|
|
24
|
+
/** The initial document (its schema drives the editor). */
|
|
25
|
+
doc: PMNode;
|
|
26
|
+
/** Extra bindings, checked before the base keymap — e.g. ArrowUp/ArrowDown
|
|
27
|
+
* wired to layout-aware caret motion from bapbong-selection. */
|
|
28
|
+
keys?: Record<string, Command>;
|
|
29
|
+
/** Called after every dispatched transaction (typing, IME composition
|
|
30
|
+
* steps, undo, selection changes). Re-layout + repaint here. */
|
|
31
|
+
onUpdate: (state: EditorState, tr: Transaction) => void;
|
|
32
|
+
}
|
|
33
|
+
/** Editing state with history + base keymap; exported for headless tests. */
|
|
34
|
+
export declare function createEditingState(doc: PMNode, keys?: Record<string, Command>): EditorState;
|
|
35
|
+
/**
|
|
36
|
+
* Hidden ProseMirror editor acting as the canvas's input sink.
|
|
37
|
+
*
|
|
38
|
+
* The browser routes keyboard and IME composition into a real (but invisible)
|
|
39
|
+
* contenteditable; ProseMirror keeps the model, history and clipboard. The
|
|
40
|
+
* host positions `dom` at the canvas caret via `place()` so IME candidate
|
|
41
|
+
* popups appear next to the visible (painted) caret.
|
|
42
|
+
*/
|
|
43
|
+
export declare class InputBridge {
|
|
44
|
+
/** Clip layer appended near the canvas: it fills the positioned ancestor
|
|
45
|
+
* and `overflow: hidden`s the 800px-wide host riding the caret inside —
|
|
46
|
+
* otherwise that tail pokes past the right page edge and gifts the
|
|
47
|
+
* scroll viewport a horizontal scrollbar. */
|
|
48
|
+
readonly dom: HTMLElement;
|
|
49
|
+
/** The hidden editor's host — the element `place()` actually moves. */
|
|
50
|
+
private readonly host;
|
|
51
|
+
readonly view: EditorView;
|
|
52
|
+
constructor(options: InputBridgeOptions);
|
|
53
|
+
get state(): EditorState;
|
|
54
|
+
/** Dispatch a transaction (e.g. a comment authoring command) through the
|
|
55
|
+
* editor, so it routes through onUpdate + history like any edit. */
|
|
56
|
+
dispatch(tr: Transaction): void;
|
|
57
|
+
focus(): void;
|
|
58
|
+
/** Set a text selection (collapsed caret or anchor→head range). Positions
|
|
59
|
+
* are clamped to the nearest valid text slots. */
|
|
60
|
+
setSelection(anchor: number, head?: number): void;
|
|
61
|
+
/** Select the word around `pos` (double-click); falls back to a collapsed
|
|
62
|
+
* caret when there is no word at that position. */
|
|
63
|
+
selectWordAt(pos: number): void;
|
|
64
|
+
/** Move the hidden editor to the painted caret (CSS px, relative to the
|
|
65
|
+
* positioned ancestor) so IME popups anchor in the right place. */
|
|
66
|
+
place(x: number, y: number, height: number): void;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
}
|
|
69
|
+
/** A user the composer can @mention. */
|
|
70
|
+
export interface MentionUser {
|
|
71
|
+
id: string;
|
|
72
|
+
label: string;
|
|
73
|
+
}
|
|
74
|
+
/** Where the @query popup should sit (client coords of the caret). */
|
|
75
|
+
export interface MentionCoords {
|
|
76
|
+
left: number;
|
|
77
|
+
top: number;
|
|
78
|
+
bottom: number;
|
|
79
|
+
}
|
|
80
|
+
/** Host hooks for the @mention popup. The composer owns detection + insertion;
|
|
81
|
+
* the host owns rendering the popup and choosing the user. */
|
|
82
|
+
export interface MentionHandlers {
|
|
83
|
+
/** Popup should show/move (state) or hide (null). */
|
|
84
|
+
query(state: {
|
|
85
|
+
query: string;
|
|
86
|
+
coords: MentionCoords;
|
|
87
|
+
} | null): void;
|
|
88
|
+
/** A nav key fired while the popup is open; return true if the host consumed
|
|
89
|
+
* it (then the composer swallows it instead of editing text). */
|
|
90
|
+
key(key: 'up' | 'down' | 'enter' | 'esc'): boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A small standalone ProseMirror editor for composing a comment body. The host
|
|
94
|
+
* passes the body schema (bapbong-model's commentSchema) so input-bridge stays
|
|
95
|
+
* schema-agnostic; `getJSON()` returns the composed doc to store on the thread.
|
|
96
|
+
* Pass `mention` handlers to enable @-mentions (the host renders the popup).
|
|
97
|
+
* `opts.onEnter` makes Enter submit (Shift-Enter newlines) — used by the inline
|
|
98
|
+
* reply box; `opts.onEscape` cancels. The mention plugin is ordered first so it
|
|
99
|
+
* gets Enter/Esc while its popup is open, before these keymaps.
|
|
100
|
+
*/
|
|
101
|
+
export declare class CommentComposer {
|
|
102
|
+
private readonly schema;
|
|
103
|
+
readonly view: EditorView;
|
|
104
|
+
constructor(schema: Schema, mount: HTMLElement, initialDoc?: unknown, mention?: MentionHandlers, opts?: {
|
|
105
|
+
onEnter?: () => void;
|
|
106
|
+
onEscape?: () => void;
|
|
107
|
+
placeholder?: string;
|
|
108
|
+
});
|
|
109
|
+
/** Replace the active "@query" with a mention node + trailing space. */
|
|
110
|
+
applyMention(user: MentionUser): void;
|
|
111
|
+
/** Whether the composer has no visible text. */
|
|
112
|
+
isEmpty(): boolean;
|
|
113
|
+
/** The composed body as commentSchema doc JSON. */
|
|
114
|
+
getJSON(): unknown;
|
|
115
|
+
/** Reset to an empty document. */
|
|
116
|
+
clear(): void;
|
|
117
|
+
focus(): void;
|
|
118
|
+
destroy(): void;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=input-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-bridge.d.ts","sourceRoot":"","sources":["../../src/lib/input-bridge.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shadow-garden/bapbong-input-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "bapbong — hidden ProseMirror editor as the input/IME sink for the canvas",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shadowgarden-app/bapbong.git",
|
|
9
|
+
"directory": "packages/input-bridge"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./package.json": "./package.json",
|
|
17
|
+
".": {
|
|
18
|
+
"@shadow-garden/source": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!**/*.tsbuildinfo"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"nx": {
|
|
33
|
+
"tags": [
|
|
34
|
+
"scope:input"
|
|
35
|
+
],
|
|
36
|
+
"targets": {
|
|
37
|
+
"build": {
|
|
38
|
+
"executor": "@nx/esbuild:esbuild",
|
|
39
|
+
"outputs": [
|
|
40
|
+
"{options.outputPath}"
|
|
41
|
+
],
|
|
42
|
+
"options": {
|
|
43
|
+
"outputPath": "packages/input-bridge/dist",
|
|
44
|
+
"main": "packages/input-bridge/src/index.ts",
|
|
45
|
+
"tsConfig": "packages/input-bridge/tsconfig.lib.json",
|
|
46
|
+
"format": [
|
|
47
|
+
"esm",
|
|
48
|
+
"cjs"
|
|
49
|
+
],
|
|
50
|
+
"declarationRootDir": "packages/input-bridge/src"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"prosemirror-commands": "^1.7.1",
|
|
57
|
+
"prosemirror-history": "^1.4.1",
|
|
58
|
+
"prosemirror-keymap": "^1.2.3",
|
|
59
|
+
"prosemirror-model": "^1.25.7",
|
|
60
|
+
"prosemirror-state": "^1.4.4",
|
|
61
|
+
"prosemirror-transform": "^1.10.0",
|
|
62
|
+
"prosemirror-view": "^1.41.3"
|
|
63
|
+
}
|
|
64
|
+
}
|