tosijs-styled-editor 0.4.2

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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Selection management for tosijs-styled-editor.
3
+ *
4
+ * Replaces browser selection behavior entirely. Selections are tracked
5
+ * using marker elements (.sel-start, .sel-end) and the .selected class.
6
+ *
7
+ * Key concept: "spanification" — wrapping each character (or word) in a
8
+ * <span> so we can determine exact screen positions without browser
9
+ * selection APIs.
10
+ */
11
+ /**
12
+ * Wrap text nodes inside `element` into individual character spans
13
+ * (or word spans if byWord is true). When `make` is false, unwraps
14
+ * existing spanification.
15
+ */
16
+ export declare function spanify(element: Element, make: boolean, byWord?: boolean): void;
17
+ /**
18
+ * Selectable manages custom text selection within a root element.
19
+ * It replaces browser selection with DOM-based markers.
20
+ */
21
+ export declare class Selectable {
22
+ root: HTMLElement;
23
+ selecting: number | false;
24
+ touchMode: boolean;
25
+ constructor(root: HTMLElement);
26
+ private lastHovered;
27
+ /** A click inside a selection, resolved on mouseup if no drag started */
28
+ private pendingCollapse;
29
+ private setup;
30
+ destroy(): void;
31
+ private handleMouseMove;
32
+ /**
33
+ * Put the caret at the character nearest a point. Used when a click inside a
34
+ * selection turns out not to be a drag, and to show the live drop position
35
+ * while dragging — we own the caret, so the drop indicator IS the caret.
36
+ */
37
+ placeCaretAt(x: number, y: number): void;
38
+ /** Put the bounds around the word containing an offset in a text node. */
39
+ selectWordAt(node: Text, offset: number): void;
40
+ private handleMouseDown;
41
+ private handleMouseUp;
42
+ private handleMouseLeave;
43
+ private handleTouchStart;
44
+ private handleTouchMove;
45
+ private handleTouchEnd;
46
+ /** Trigger selectionchanged event and focus the caret */
47
+ selectionChanged(): void;
48
+ /**
49
+ * Set by the component. The markers in the text are inert spans — a replaced
50
+ * element there breaks the shaping run, which is what tore Arabic words apart
51
+ * around the caret — so the visible caret and the focusable element live
52
+ * OUTSIDE the document, and this is how they learn the bounds moved.
53
+ */
54
+ onBoundsChanged?: () => void;
55
+ /** The focusable element that stands in for the caret */
56
+ focusTarget?: HTMLElement;
57
+ focus(): void;
58
+ /** Create a document fragment with sel-start and sel-end markers */
59
+ createBounds(): DocumentFragment;
60
+ /**
61
+ * Put the bound markers in TEXTUAL order.
62
+ *
63
+ * While a drag is in flight `.sel-end` is the moving bound and `.sel-start`
64
+ * the anchor, so dragging backwards leaves the end marker earlier in the
65
+ * document than the start. That is the right model for dragging but the wrong
66
+ * one for painting: the edges are coloured by which marker they are, so a
67
+ * backwards drag showed the colours swapped. Once the gesture is complete the
68
+ * labels are re-assigned by document order, so start is always textually
69
+ * first — and in RTL that means it paints on the right.
70
+ */
71
+ normalizeBoundsOrder(): void;
72
+ /** Find an element within the root */
73
+ find(selector: string): Element | null;
74
+ /** Find all matching elements within the root */
75
+ findAll(selector: string): Element[];
76
+ /** Mark the selection between the current bounds */
77
+ markBounds(): void;
78
+ /** Remove selection bound markers */
79
+ removeBounds(): void;
80
+ /** Restore bounds to match the current .selected elements */
81
+ resetBounds(): this;
82
+ /** Extend the selection based on the current click mode */
83
+ extendSelection(): void;
84
+ /** Get the top-level child of root that contains `node` */
85
+ private topLevelAncestor;
86
+ /** Clear all selection markers */
87
+ unmark(): void;
88
+ /** Check if a node is a direct child of root (a "block") */
89
+ isBlock(node: Node): boolean;
90
+ /** Mark a range of elements as selected */
91
+ markRange(first: Element, last: Element): void;
92
+ /** The representations a dragged selection offers */
93
+ static DRAG_TYPES: string;
94
+ /** Make a selected element a draggable object */
95
+ private makeDraggable;
96
+ /** Mark a single node as selected */
97
+ private markNode;
98
+ /** Despanify all non-selected blocks */
99
+ despanify(): void;
100
+ normalize(): this;
101
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Table utilities for the grid-based table editor.
3
+ *
4
+ * Tables are `<ul class="editor-table">` with CSS grid layout.
5
+ * Cells are `<li>` elements. Column count is derived from
6
+ * `grid-template-columns`. Header cells have class `table-header`.
7
+ */
8
+ /** Check if a node is inside a table cell */
9
+ export declare function isTableCell(node: Node): boolean;
10
+ /** Find the <li> cell containing a node, or null */
11
+ export declare function cellOf(node: Node): HTMLElement | null;
12
+ /** Find the .editor-table <ul> containing a node, or null */
13
+ export declare function tableOf(node: Node): HTMLElement | null;
14
+ /** Get the number of columns from grid-template-columns */
15
+ export declare function getColumnCount(table: HTMLElement): number;
16
+ /** Get the column widths as an array of strings */
17
+ export declare function getColumnWidths(table: HTMLElement): string[];
18
+ /** Set the column widths from an array of strings */
19
+ export declare function setColumnWidths(table: HTMLElement, widths: string[]): void;
20
+ /** Get the index of a cell among its sibling <li> elements */
21
+ export declare function cellIndex(cell: HTMLElement): number;
22
+ /** Get which row a cell is in (0-based) */
23
+ export declare function rowOfCell(cell: HTMLElement, colCount: number): number;
24
+ /** Get which column a cell is in (0-based) */
25
+ export declare function colOfCell(cell: HTMLElement, colCount: number): number;
26
+ /** Get the cell in the same column one row down, or null */
27
+ export declare function cellBelow(cell: HTMLElement): HTMLElement | null;
28
+ /** Get the cell in the same column one row up, or null */
29
+ export declare function cellAbove(cell: HTMLElement): HTMLElement | null;
30
+ /** Get the next <li> sibling, or null */
31
+ export declare function nextCell(cell: HTMLElement): HTMLElement | null;
32
+ /** Get the previous <li> sibling, or null */
33
+ export declare function prevCell(cell: HTMLElement): HTMLElement | null;
34
+ /** Check if a cell is a header cell */
35
+ export declare function isHeaderCell(cell: HTMLElement): boolean;
36
+ /** Create a single table cell */
37
+ export declare function createCell(isHeader?: boolean): HTMLElement;
38
+ /** Get all cells in a specific row */
39
+ export declare function getCellsInRow(table: HTMLElement, row: number, colCount: number): HTMLElement[];
40
+ /** Get all cells in a specific column */
41
+ export declare function getCellsInCol(table: HTMLElement, col: number, colCount: number): HTMLElement[];
42
+ /** Get the total number of rows */
43
+ export declare function getRowCount(table: HTMLElement): number;
44
+ /**
45
+ * Create a complete table element.
46
+ * @param cols Number of columns
47
+ * @param rows Total number of rows (including header rows)
48
+ * @param headerRows Number of header rows (default 1)
49
+ */
50
+ export declare function createTable(cols: number, rows?: number, headerRows?: number): HTMLElement;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Toolbar factory for tosijs-styled-editor.
3
+ *
4
+ * Provides ready-made toolbar configurations using tosijs-ui icons and menus.
5
+ * Toolbar buttons fire commands via their `value` attribute.
6
+ * Menus use tosi-menu with actions that call doCommand on a provided editor.
7
+ */
8
+ import type { TosijsStyledEditor } from './tosijs-styled-editor';
9
+ /**
10
+ * The class we tag our own menu popups with, so menu styling can be scoped to
11
+ * this editor instead of every `tosi-menu` on the page.
12
+ */
13
+ export declare const MENU_CLASS = "tosijs-styled-editor-menu";
14
+ /**
15
+ * Create a toolbar command button with an icon.
16
+ * The button's `value` attribute holds the command string.
17
+ */
18
+ export declare function commandButton(title: string, command: string, icon: SVGElement, shortcut?: string): HTMLButtonElement;
19
+ /** Create a spacer for visual grouping */
20
+ export declare function toolbarSpacer(width?: string): HTMLElement;
21
+ /** A language picker for the menubar — flag only, no caption */
22
+ export declare function localePickerWidget(): HTMLElement;
23
+ /** Paragraph style menu */
24
+ export declare function paragraphStyleMenu(editor: TosijsStyledEditor): HTMLElement;
25
+ /** Justification menu */
26
+ export declare function justificationMenu(editor: TosijsStyledEditor): HTMLElement;
27
+ /** Font family menu */
28
+ export declare function fontFamilyMenu(editor: TosijsStyledEditor): HTMLElement;
29
+ /** Font size menu */
30
+ export declare function fontSizeMenu(editor: TosijsStyledEditor): HTMLElement;
31
+ /** Line spacing menu */
32
+ export declare function lineSpacingMenu(editor: TosijsStyledEditor): HTMLElement;
33
+ /** Character formatting buttons (toolbar) */
34
+ export declare function characterStyleWidgets(): HTMLElement[];
35
+ /** Paragraph formatting buttons (toolbar) */
36
+ export declare function paragraphStyleWidgets(): HTMLElement[];
37
+ /** Undo/redo buttons (toolbar) */
38
+ export declare function undoRedoWidgets(): HTMLElement[];
39
+ /** Debug toggle (toolbar) */
40
+ export declare function debugWidget(): HTMLElement;
41
+ /** Minimal toolbar: bold/italic/underline + undo/redo */
42
+ export declare function minimalToolbar(): HTMLElement[];
43
+ /** Default (full) toolbar */
44
+ export declare function defaultToolbar(): HTMLElement[];
45
+ /**
46
+ * Insert menu — link, image and footnote.
47
+ *
48
+ * Link and image need a URL, and the component has no dialog of its own yet, so
49
+ * these prompt. A host that wants its own UI can call the commands directly:
50
+ * `editor.doCommand('setLink <url> [target]')`.
51
+ */
52
+ export declare function insertMenu(editor: TosijsStyledEditor): HTMLElement;
53
+ /** Table menu */
54
+ export declare function tableMenu(editor: TosijsStyledEditor): HTMLElement;
55
+ /** Default menubar */
56
+ export declare function defaultMenubar(editor: TosijsStyledEditor): HTMLElement[];
@@ -0,0 +1,247 @@
1
+ import { Component as WebComponent, ElementCreator, PartsMap, type TosiStyleSheet } from 'tosijs';
2
+ import { Selectable } from './selection';
3
+ import { type Command } from './commands';
4
+ interface EditableParts extends PartsMap {
5
+ menubar: HTMLElement;
6
+ toolbar: HTMLElement;
7
+ doc: HTMLElement;
8
+ caret: HTMLElement;
9
+ edgeStart: HTMLElement;
10
+ edgeEnd: HTMLElement;
11
+ }
12
+ export declare class TosijsStyledEditor extends WebComponent<EditableParts> {
13
+ static formAssociated: boolean;
14
+ static preferredTagName: string;
15
+ static initAttributes: {
16
+ widgets: "none" | "minimal" | "default";
17
+ localized: boolean;
18
+ name: string;
19
+ required: boolean;
20
+ };
21
+ static shadowStyleSpec: TosiStyleSheet;
22
+ /**
23
+ * Toolbar buttons and menus are LIGHT DOM (slotted), and `::slotted()` rules
24
+ * lose the cascade to the host page's own `button` styles — which is how the
25
+ * buttons ended up as white chips on the tinted bars. A light stylesheet is
26
+ * scoped by tag name at document level, so it competes on equal terms.
27
+ */
28
+ static lightStyleSpec: TosiStyleSheet;
29
+ selectable: Selectable;
30
+ /** Set from `initAttributes` at runtime; `declare` so no field is emitted over it */
31
+ widgets: 'none' | 'minimal' | 'default';
32
+ /** Show a language picker — the built-in widgets are always translatable */
33
+ localized: boolean;
34
+ active: boolean;
35
+ pastemode: 'merge' | 'remove' | 'preserve' | 'paragraphs';
36
+ private undo;
37
+ private undoDepth;
38
+ private reasonForLastUndo?;
39
+ private lastKey;
40
+ private lastCursorX;
41
+ private isInitialized;
42
+ private touchAffordances;
43
+ private touchHandleStart;
44
+ private touchContextMenu;
45
+ private touchHandleEnd;
46
+ private touchMenuEl;
47
+ private isTouchInteraction;
48
+ private touchDrags;
49
+ private resizeTable;
50
+ private resizeCol;
51
+ private resizeStartX;
52
+ private resizeStartWidths;
53
+ private _value;
54
+ /** Get doc innerHTML excluding UI affordances */
55
+ private get docHTML();
56
+ /** Set doc innerHTML and re-attach UI affordances */
57
+ private set docHTML(value);
58
+ get value(): string;
59
+ set value(html: string);
60
+ /** The editable commands — extend this object to add custom commands */
61
+ commands: Record<string, Command>;
62
+ content: (HTMLDivElement | HTMLSlotElement)[];
63
+ formResetCallback(): void;
64
+ connectedCallback(): void;
65
+ /** Reflect whether a slot has assigned content onto the host, and keep it current */
66
+ private trackBar;
67
+ /**
68
+ * Populate the built-in toolbar/menubar named by the `widgets` attribute.
69
+ * Anything the author slotted themselves wins — this only fills an empty bar.
70
+ */
71
+ private applyWidgets;
72
+ disconnectedCallback(): void;
73
+ /**
74
+ * Position the caret (and, for an expanded selection, its two edges) over the
75
+ * document from the inert markers' rects. Nothing here is in the text, so
76
+ * nothing here can change how the text is shaped.
77
+ */
78
+ /**
79
+ * Where a bound marker is on screen.
80
+ *
81
+ * The markers are styled `display: contents` so they generate NO BOX — an
82
+ * inline box between two characters changed the line it sat in (in WebKit an
83
+ * empty marker with `font-size: 0` grew the paragraph by ~5px and shifted
84
+ * every block after it). With no box their own getBoundingClientRect() is
85
+ * meaningless, so position comes from the caret geometry beside them.
86
+ */
87
+ private markerRect;
88
+ private syncCaret;
89
+ /** Get the editing context for commands */
90
+ private getContext;
91
+ /** Execute a command string */
92
+ doCommand(commandString: string): void;
93
+ focus(): void;
94
+ normalize(): void;
95
+ /** Get all leaf nodes in the current selection */
96
+ selectedLeafNodes(): Node[];
97
+ /** Get the selected blocks */
98
+ selectedBlocks(): Element[];
99
+ /** Get the caret input if it exists in the doc */
100
+ insertionPoint(): HTMLInputElement | null;
101
+ /** Get the top-level block containing a node */
102
+ block(node: Node): Element | null;
103
+ /** Find the table cell containing the caret, if any */
104
+ private caretCell;
105
+ /** Find the list item (<li> in a non-table <ul>/<ol>) containing a node, or null */
106
+ private listItemOf;
107
+ /** Move the caret (bounds) into a cell, selecting all content */
108
+ private moveCaretToCell;
109
+ /** Insert a new row above the current cell and move caret into it */
110
+ private tableMoveUp;
111
+ /** Insert a new row below the current cell and move caret into it */
112
+ private tableMoveDown;
113
+ /** Exit the table, placing the caret in the block before or after it */
114
+ private exitTable;
115
+ /** Split a list item at the caret, creating a new <li> after it.
116
+ * If the item is empty, exit the list instead (convert to <p>). */
117
+ private splitListItem;
118
+ /** Check if a list item is effectively empty (only whitespace/nbsp/bounds) */
119
+ private isListItemEmpty;
120
+ /** Remove a list item, merging with previous <li> or exiting the list */
121
+ private removeListItem;
122
+ /** Backspace inside a list item: delete within the <li>, or merge with previous <li> */
123
+ private listBackspace;
124
+ /** Forward delete inside a list item: delete within the <li>, or merge with next <li> */
125
+ private listForwardDelete;
126
+ /** Group spanified characters by visual line (rounded top value) */
127
+ private groupByLine;
128
+ /** Find the spanified char on a line closest to targetX */
129
+ private closestCharOnLine;
130
+ /** Position bounds at a spanified character, then despanify the block */
131
+ private positionAtSpanChar;
132
+ /** Move caret up or down by visual line */
133
+ private moveVertical;
134
+ /** Move caret into an empty container (no spanifiable content) */
135
+ private moveCaretToEmptyContainer;
136
+ /** Move caret vertically into a sibling block, handling lists and regular blocks */
137
+ private moveVerticalToBlock;
138
+ /** Insert a character at the caret */
139
+ private contentKey;
140
+ /**
141
+ * Typing left-to-right into a right-to-left block (or the reverse) put the
142
+ * caret on the wrong side of the line. The caret is an element, and bidi
143
+ * treats an empty inline as a NEUTRAL — so it resolves to the BLOCK's base
144
+ * direction rather than to the run being typed, and lands at the far end.
145
+ *
146
+ * We own the caret, so we do not have to guess: when the typed character
147
+ * disagrees with the block, put the run AND the caret inside a directional
148
+ * isolate. The caret is then a neutral among characters of its own run and
149
+ * resolves with them, and the run itself renders correctly too — the same
150
+ * bug class as an un-isolated URL inside an RTL paragraph.
151
+ *
152
+ * Returns the isolate in force, or null when none is needed.
153
+ */
154
+ private isolateForTyping;
155
+ /** Delete the character before the caret */
156
+ private backspace;
157
+ /** Forward delete */
158
+ private forwardDelete;
159
+ /** Delete the current selection, return true if something was deleted */
160
+ deleteSelection(): boolean;
161
+ /** Split the current block at the caret position (Enter key) */
162
+ private splitAtCaret;
163
+ /** Move caret left */
164
+ private arrowLeft;
165
+ /** Move caret right */
166
+ private arrowRight;
167
+ /** Move caret left by one word */
168
+ private moveWordLeft;
169
+ /** Move caret right by one word */
170
+ private moveWordRight;
171
+ /** Move bounds to before a target node */
172
+ private moveBoundsBefore;
173
+ /** Move bounds to after a target node */
174
+ private moveBoundsAfter;
175
+ /** Manage undo/redo stack */
176
+ /**
177
+ * The document's CONTENT, with every trace of the selection removed.
178
+ *
179
+ * Undo snapshots are full HTML and the selection lives in the DOM — bound
180
+ * markers are elements, `.selected` is a class and sometimes a wrapper span.
181
+ * So moving the caret changes the snapshot string even though nothing was
182
+ * edited, and every caret move pushed an undo step that undid nothing.
183
+ * Comparing signatures instead means only real edits create steps, while the
184
+ * snapshots themselves keep their markers so undo still restores a selection.
185
+ */
186
+ private signatureOf;
187
+ updateUndo(command?: string, reason?: string): void;
188
+ private updateUndoButtons;
189
+ /**
190
+ * A link inside the document is text you are editing, not navigation — a
191
+ * click there is placing the caret. Ctrl/Cmd-click still follows it, which is
192
+ * the convention every other editor uses.
193
+ */
194
+ private handleDocClick;
195
+ /** Handle shortcuts (ctrl/cmd+key) */
196
+ private handleShortcut;
197
+ private handleKeydown;
198
+ private handleKeypress;
199
+ private handleCopy;
200
+ private handleCut;
201
+ /**
202
+ * Insert transferred content at the caret, honouring `pastemode`.
203
+ *
204
+ * Shared by paste and drop deliberately: dropping and pasting the same
205
+ * content must produce the same document, and that only stays true if there
206
+ * is one implementation.
207
+ */
208
+ private insertTransfer;
209
+ private handlePaste;
210
+ /** True while a drag that STARTED in this editor is in flight */
211
+ private draggingSelection;
212
+ /**
213
+ * Offer the selection as a draggable object in both representations, so the
214
+ * receiver picks: styled markup for a rich target, clean text for a plain one.
215
+ */
216
+ private handleDragStart;
217
+ /**
218
+ * The drop indicator IS the caret — we own it, so there is no separate
219
+ * insertion bar to keep in sync with where the text will actually land.
220
+ */
221
+ private handleDragOver;
222
+ private handleDrop;
223
+ private handleDragEnd;
224
+ private clearDraggable;
225
+ /** Read dropped image files in as data URIs */
226
+ private insertDroppedImages;
227
+ /** Find the cell edge near a pointer position, returns [table, colIndex] or null */
228
+ private cellEdgeAt;
229
+ /** Update position of touch affordance handles based on selection bounds */
230
+ private updateTouchAffordances;
231
+ private positionAffordances;
232
+ private handleAffordanceDragStart;
233
+ private handleAffordanceDragMove;
234
+ private handleAffordanceDragEnd;
235
+ private handleTouchContextMenu;
236
+ private showTouchMenu;
237
+ private touchMenuDismiss;
238
+ private hideTouchMenu;
239
+ private handleResizePointerDown;
240
+ private handleResizePointerMove;
241
+ private handleResizePointerUp;
242
+ private isToolbarEvent;
243
+ private handleToolbarClick;
244
+ private handleToolbarChange;
245
+ }
246
+ export declare const tosijsStyledEditor: ElementCreator<TosijsStyledEditor>;
247
+ export {};
@@ -0,0 +1 @@
1
+ export declare const version = "0.4.2";
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "tosijs-styled-editor",
3
+ "version": "0.4.2",
4
+ "description": "Rich text editor web component — no contentEditable, no browser selection APIs",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/tonioloewald/tosijs-editor.git"
8
+ },
9
+ "homepage": "https://editor.tosijs.net",
10
+ "bugs": {
11
+ "url": "https://github.com/tonioloewald/tosijs-editor/issues"
12
+ },
13
+ "source": "src/index.ts",
14
+ "type": "module",
15
+ "types": "dist/index.d.ts",
16
+ "main": "./dist/module.js",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/module.js",
21
+ "browser": "./dist/index.js",
22
+ "default": "./dist/module.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/*.tsbuildinfo",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "start": "bun bin/site.ts",
33
+ "make": "bun bin/site.ts --build",
34
+ "test": "bun test",
35
+ "lint": "bun tsc --noEmit --noUnusedLocals --noUnusedParameters",
36
+ "format": "bun prettier --write .",
37
+ "haltija": "haltija --name tosijs-editor --port 8702 --server",
38
+ "tls": "bunx tosijs-dev-certs"
39
+ },
40
+ "author": "Tonio Loewald",
41
+ "license": "Apache-2.0",
42
+ "peerDependencies": {
43
+ "tosijs": "^1.10.1",
44
+ "tosijs-ui": "^1.13.0"
45
+ },
46
+ "devDependencies": {
47
+ "bun-types": "^1.3.0",
48
+ "chokidar": "^4.0.3",
49
+ "haltija": "1.12.9",
50
+ "happy-dom": "^20.14.0",
51
+ "prettier": "^2",
52
+ "tjs-lang": "^0.13.1",
53
+ "tosijs": "^1.10.1",
54
+ "tosijs-ui": "^1.13.0",
55
+ "typescript": "^5.9.3"
56
+ },
57
+ "overrides": {
58
+ "ws": "^8.21.0"
59
+ }
60
+ }