fabricjs-document-engine 1.2.0 → 1.3.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.
Files changed (81) hide show
  1. package/README.md +31 -0
  2. package/README.zh-CN.md +32 -0
  3. package/dist/commands/text-edit.cjs +223 -0
  4. package/dist/commands/text-edit.d.cts +23 -0
  5. package/dist/commands/text-edit.d.ts +23 -0
  6. package/dist/commands/text-edit.js +218 -0
  7. package/dist/fabric/object-registry.cjs +7 -1
  8. package/dist/fabric/object-registry.js +7 -1
  9. package/dist/filters/filter-worker.cjs +281 -0
  10. package/dist/filters/filter-worker.d.cts +26 -0
  11. package/dist/filters/filter-worker.d.ts +26 -0
  12. package/dist/filters/filter-worker.js +281 -0
  13. package/dist/filters/pipeline.cjs +91 -0
  14. package/dist/filters/pipeline.d.cts +35 -0
  15. package/dist/filters/pipeline.d.ts +35 -0
  16. package/dist/filters/pipeline.js +87 -0
  17. package/dist/filters/worker-entry.cjs +78 -0
  18. package/dist/filters/worker-entry.d.cts +29 -0
  19. package/dist/filters/worker-entry.d.ts +29 -0
  20. package/dist/filters/worker-entry.js +78 -0
  21. package/dist/filters.cjs +9 -0
  22. package/dist/filters.d.cts +3 -0
  23. package/dist/filters.d.ts +3 -0
  24. package/dist/filters.js +3 -0
  25. package/dist/import/svg-import.cjs +102 -0
  26. package/dist/import/svg-import.d.cts +1 -0
  27. package/dist/import/svg-import.d.ts +1 -0
  28. package/dist/import/svg-import.js +103 -1
  29. package/dist/index.cjs +7 -0
  30. package/dist/index.d.cts +2 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +2 -1
  33. package/dist/performance/batch.cjs +31 -0
  34. package/dist/performance/batch.d.cts +4 -0
  35. package/dist/performance/batch.d.ts +4 -0
  36. package/dist/performance/batch.js +31 -0
  37. package/dist/performance/dirty-regions.cjs +378 -0
  38. package/dist/performance/dirty-regions.d.cts +23 -0
  39. package/dist/performance/dirty-regions.d.ts +23 -0
  40. package/dist/performance/dirty-regions.js +378 -0
  41. package/dist/performance/monitor.cjs +60 -0
  42. package/dist/performance/monitor.d.cts +20 -0
  43. package/dist/performance/monitor.d.ts +20 -0
  44. package/dist/performance/monitor.js +60 -0
  45. package/dist/performance/spatial-index.cjs +78 -0
  46. package/dist/performance/spatial-index.d.cts +21 -0
  47. package/dist/performance/spatial-index.d.ts +21 -0
  48. package/dist/performance/spatial-index.js +78 -0
  49. package/dist/performance.cjs +9 -0
  50. package/dist/performance.d.cts +5 -0
  51. package/dist/performance.d.ts +5 -0
  52. package/dist/performance.js +5 -0
  53. package/dist/text/bounded-textbox.cjs +306 -0
  54. package/dist/text/bounded-textbox.d.cts +53 -0
  55. package/dist/text/bounded-textbox.d.ts +53 -0
  56. package/dist/text/bounded-textbox.js +306 -0
  57. package/dist/text/mobile-input.cjs +146 -0
  58. package/dist/text/mobile-input.d.cts +19 -0
  59. package/dist/text/mobile-input.d.ts +19 -0
  60. package/dist/text/mobile-input.js +145 -0
  61. package/dist/text/register.cjs +17 -0
  62. package/dist/text/register.d.cts +5 -0
  63. package/dist/text/register.d.ts +5 -0
  64. package/dist/text/register.js +16 -0
  65. package/dist/text/shaping.cjs +125 -0
  66. package/dist/text/shaping.d.cts +28 -0
  67. package/dist/text/shaping.d.ts +28 -0
  68. package/dist/text/shaping.js +121 -0
  69. package/dist/text/vertical-orientation.cjs +192 -0
  70. package/dist/text/vertical-orientation.d.cts +10 -0
  71. package/dist/text/vertical-orientation.d.ts +10 -0
  72. package/dist/text/vertical-orientation.js +190 -0
  73. package/dist/text/vertical-text.cjs +336 -0
  74. package/dist/text/vertical-text.d.cts +54 -0
  75. package/dist/text/vertical-text.d.ts +54 -0
  76. package/dist/text/vertical-text.js +336 -0
  77. package/dist/text.cjs +21 -0
  78. package/dist/text.d.cts +7 -0
  79. package/dist/text.d.ts +7 -0
  80. package/dist/text.js +7 -0
  81. package/package.json +31 -1
@@ -0,0 +1,306 @@
1
+ import { shapeLine, shapeWord } from "./shaping.js";
2
+ import { Textbox } from "fabric";
3
+ //#region src/text/bounded-textbox.ts
4
+ const BOUNDED_PROPERTIES = [
5
+ "breakWords",
6
+ "maxHeight",
7
+ "overflow",
8
+ "fit",
9
+ "minFontSize",
10
+ "shaping"
11
+ ];
12
+ const ELLIPSIS = "…";
13
+ let clipCounter = 0;
14
+ function internals(text) {
15
+ return text;
16
+ }
17
+ var BoundedTextbox = class BoundedTextbox extends Textbox {
18
+ static getDefaults() {
19
+ return {
20
+ ...super.getDefaults(),
21
+ ...BoundedTextbox.boundedDefaults
22
+ };
23
+ }
24
+ constructor(text, options = {}) {
25
+ super(text, {
26
+ ...BoundedTextbox.boundedDefaults,
27
+ ...options
28
+ });
29
+ }
30
+ getBaseFontSize() {
31
+ return this.baseFontSize;
32
+ }
33
+ initDimensions() {
34
+ if (!internals(this).initialized) return;
35
+ this.midWordBreaks ?? (this.midWordBreaks = /* @__PURE__ */ new Set());
36
+ this.overflowCache = void 0;
37
+ this.fitScale = 1;
38
+ super.initDimensions();
39
+ if (this.fit === "shrink" && this.maxHeight !== void 0 && this.calcTextHeight() > this.maxHeight) this.shrinkToFit();
40
+ if (this.maxHeight !== void 0 && this.overflow !== "visible") this.height = Math.min(this.height, this.maxHeight);
41
+ }
42
+ shrinkToFit() {
43
+ const base = this.baseFontSize;
44
+ const maxHeight = this.maxHeight;
45
+ let low = Math.max(1, Math.ceil(Math.min(this.minFontSize, base) * 2));
46
+ let high = Math.floor(base * 2) - 1;
47
+ let best = low;
48
+ const layOut = (halfPoints) => {
49
+ this.fitScale = halfPoints / 2 / base;
50
+ Textbox.prototype.initDimensions.call(this);
51
+ return this.calcTextHeight();
52
+ };
53
+ while (low <= high) {
54
+ const middle = low + high >> 1;
55
+ if (layOut(middle) <= maxHeight) {
56
+ best = middle;
57
+ low = middle + 1;
58
+ } else high = middle - 1;
59
+ }
60
+ if (this.fitScale !== best / 2 / base) layOut(best);
61
+ }
62
+ getValueOfPropertyAt(lineIndex, charIndex, property) {
63
+ const value = super.getValueOfPropertyAt(lineIndex, charIndex, property);
64
+ if (property === "fontSize" && this.fitScale !== 1 && this.hasOwnFontSize(lineIndex, charIndex)) return value * this.fitScale;
65
+ return value;
66
+ }
67
+ getCompleteStyleDeclaration(lineIndex, charIndex) {
68
+ const style = super.getCompleteStyleDeclaration(lineIndex, charIndex);
69
+ if (this.fitScale !== 1 && this.hasOwnFontSize(lineIndex, charIndex)) style.fontSize *= this.fitScale;
70
+ return style;
71
+ }
72
+ hasOwnFontSize(lineIndex, charIndex) {
73
+ return internals(this)._getStyleDeclaration(lineIndex, charIndex)?.fontSize !== void 0;
74
+ }
75
+ _wrapText(lines, desiredWidth) {
76
+ const self = internals(this);
77
+ this.midWordBreaks = /* @__PURE__ */ new Set();
78
+ self.isWrapping = true;
79
+ const data = self.getGraphemeDataForRender(lines);
80
+ const wrapped = [];
81
+ for (let index = 0; index < data.wordsData.length; index += 1) {
82
+ this.lineBase = wrapped.length;
83
+ wrapped.push(...this._wrapLine(index, desiredWidth, data));
84
+ }
85
+ self.isWrapping = false;
86
+ return wrapped;
87
+ }
88
+ _wrapLine(lineIndex, desiredWidth, data, reservedSpace = 0) {
89
+ if (this.breakWords === "never" || this.splitByGrapheme) return super._wrapLine(lineIndex, desiredWidth, data, reservedSpace);
90
+ const self = internals(this);
91
+ const { wordsData } = data;
92
+ const additionalSpace = self._getWidthOfCharSpacing();
93
+ const available = desiredWidth - reservedSpace;
94
+ const lines = [];
95
+ let line = [];
96
+ let lineWidth = 0;
97
+ let offset = 0;
98
+ let infixWidth = 0;
99
+ let lineJustStarted = true;
100
+ let widestGrapheme = 0;
101
+ const words = wordsData[lineIndex] ?? [];
102
+ for (const { word, width: wordWidth } of words) {
103
+ const wordStart = offset;
104
+ offset += word.length;
105
+ if (wordWidth - additionalSpace > available && word.length > 1) {
106
+ if (!lineJustStarted) lines.push(line);
107
+ line = [];
108
+ lineWidth = 0;
109
+ let previous;
110
+ for (let index = 0; index < word.length; index += 1) {
111
+ const grapheme = word[index];
112
+ const graphemeWidth = self._getGraphemeBox(grapheme, lineIndex, wordStart + index, previous, true).kernedWidth;
113
+ widestGrapheme = Math.max(widestGrapheme, graphemeWidth - additionalSpace);
114
+ if (line.length > 0 && lineWidth + graphemeWidth - additionalSpace > available) {
115
+ this.midWordBreaks.add(this.lineBase + lines.length);
116
+ lines.push(line);
117
+ line = [];
118
+ lineWidth = 0;
119
+ }
120
+ line.push(grapheme);
121
+ lineWidth += graphemeWidth;
122
+ previous = grapheme;
123
+ }
124
+ } else {
125
+ if (word.length === 1) widestGrapheme = Math.max(widestGrapheme, wordWidth - additionalSpace);
126
+ lineWidth += infixWidth + wordWidth - additionalSpace;
127
+ if (lineWidth > available && !lineJustStarted) {
128
+ lines.push(line);
129
+ line = [];
130
+ lineWidth = wordWidth;
131
+ lineJustStarted = true;
132
+ } else lineWidth += additionalSpace;
133
+ if (!lineJustStarted) line.push(" ");
134
+ line = line.concat(word);
135
+ }
136
+ infixWidth = self._measureWord([" "], lineIndex, offset);
137
+ offset += 1;
138
+ lineJustStarted = false;
139
+ }
140
+ if (words.length > 0) lines.push(line);
141
+ self.dynamicMinWidth = Math.max(self.dynamicMinWidth, widestGrapheme);
142
+ return lines;
143
+ }
144
+ _measureLine(lineIndex) {
145
+ const measured = super._measureLine(lineIndex);
146
+ return this.shaping ? shapeLine(this, lineIndex, measured) : measured;
147
+ }
148
+ _measureWord(word, lineIndex, charOffset = 0) {
149
+ const width = super._measureWord(word, lineIndex, charOffset);
150
+ return this.shaping ? shapeWord(this, word, lineIndex, charOffset, width) : width;
151
+ }
152
+ missingNewlineOffset(lineIndex, skipWrapping) {
153
+ if (!skipWrapping && this.midWordBreaks?.has(lineIndex)) return 0;
154
+ return super.missingNewlineOffset(lineIndex, skipWrapping);
155
+ }
156
+ overflowLayout() {
157
+ if (this.overflowCache !== void 0) return this.overflowCache;
158
+ const maxHeight = this.maxHeight;
159
+ if (maxHeight === void 0 || this.overflow === "visible") return this.overflowCache = null;
160
+ const lineCount = internals(this)._textLines.length;
161
+ let visibleLines = 0;
162
+ let top = 0;
163
+ for (let index = 0; index < lineCount; index += 1) {
164
+ const lineHeight = this.getHeightOfLine(index);
165
+ if (top + lineHeight / this.lineHeight > maxHeight + .5) break;
166
+ visibleLines = index + 1;
167
+ top += lineHeight;
168
+ }
169
+ const clipped = visibleLines < lineCount;
170
+ const layout = {
171
+ visibleLines,
172
+ clipped,
173
+ visibleHeight: top
174
+ };
175
+ if (clipped && visibleLines > 0 && this.overflow === "ellipsis" && !this.isEditing && this.direction !== "rtl") layout.ellipsis = this.placeEllipsis(visibleLines - 1, top - this.getHeightOfLine(visibleLines - 1));
176
+ return this.overflowCache = layout;
177
+ }
178
+ placeEllipsis(line, lineTop) {
179
+ const self = internals(this);
180
+ this.getLineWidth(line);
181
+ const bounds = self.__charBounds[line] ?? [];
182
+ const graphemes = self._textLines[line] ?? [];
183
+ const lineLeft = self._getLineLeftOffset(line);
184
+ const styleAt = (index) => this.getCompleteStyleDeclaration(line, Math.max(0, Math.min(index, graphemes.length - 1)));
185
+ const ellipsisWidth = self._measureChar(ELLIPSIS, styleAt(graphemes.length - 1)).width;
186
+ let low = 0;
187
+ let high = graphemes.length;
188
+ while (low < high) {
189
+ const middle = low + high + 1 >> 1;
190
+ if (lineLeft + (bounds[middle]?.left ?? 0) + ellipsisWidth <= this.width) low = middle;
191
+ else high = middle - 1;
192
+ }
193
+ let cut = low;
194
+ while (cut > 0 && /\s/.test(graphemes[cut - 1])) cut -= 1;
195
+ const lineHeight = this.getHeightOfLine(line) / this.lineHeight;
196
+ return {
197
+ line,
198
+ cut,
199
+ x: self._getLeftOffset() + lineLeft + (bounds[cut]?.left ?? 0),
200
+ baseline: self._getTopOffset() + lineTop + lineHeight * (1 - self._fontSizeFraction),
201
+ style: styleAt(cut - 1)
202
+ };
203
+ }
204
+ clipRects(layout) {
205
+ const self = internals(this);
206
+ const left = self._getLeftOffset();
207
+ const top = self._getTopOffset();
208
+ const wide = this.width + this.fontSize * 2;
209
+ const x = left - this.fontSize;
210
+ if (!layout.ellipsis) {
211
+ const bottom = this.overflow === "clip" || this.isEditing ? this.maxHeight : layout.visibleHeight;
212
+ return [[
213
+ x,
214
+ top - this.fontSize,
215
+ wide,
216
+ bottom + this.fontSize
217
+ ]];
218
+ }
219
+ const { line, x: cutX } = layout.ellipsis;
220
+ const lineTop = layout.visibleHeight - this.getHeightOfLine(line);
221
+ const rects = [];
222
+ if (lineTop > 0) rects.push([
223
+ x,
224
+ top - this.fontSize,
225
+ wide,
226
+ lineTop + this.fontSize
227
+ ]);
228
+ rects.push([
229
+ x,
230
+ top + lineTop,
231
+ cutX - x,
232
+ this.getHeightOfLine(line)
233
+ ]);
234
+ return rects;
235
+ }
236
+ _render(ctx) {
237
+ const layout = this.overflowLayout();
238
+ if (!layout || !layout.clipped && this.overflow !== "clip") {
239
+ super._render(ctx);
240
+ return;
241
+ }
242
+ ctx.save();
243
+ ctx.beginPath();
244
+ for (const [x, y, width, height] of this.clipRects(layout)) ctx.rect(x, y, width, height);
245
+ ctx.clip();
246
+ super._render(ctx);
247
+ ctx.restore();
248
+ const ellipsis = layout.ellipsis;
249
+ if (!ellipsis) return;
250
+ ctx.save();
251
+ ctx.font = internals(this)._getFontDeclaration(ellipsis.style);
252
+ ctx.textBaseline = "alphabetic";
253
+ ctx.textAlign = "left";
254
+ ctx.fillStyle = typeof ellipsis.style.fill === "string" ? ellipsis.style.fill : typeof this.fill === "string" ? this.fill : "#000";
255
+ ctx.fillText(ELLIPSIS, ellipsis.x, ellipsis.baseline - (ellipsis.style.deltaY ?? 0));
256
+ ctx.restore();
257
+ }
258
+ _toSVG() {
259
+ const markup = super._toSVG();
260
+ const layout = this.overflowLayout();
261
+ if (!layout || !layout.clipped && this.overflow !== "clip") return markup;
262
+ clipCounter += 1;
263
+ const id = `fde-bounded-text-${clipCounter}`;
264
+ const result = [
265
+ `<clipPath id="${id}">${this.clipRects(layout).map(([x, y, width, height]) => `<rect x="${x}" y="${y}" width="${width}" height="${height}" />`).join("")}</clipPath>\n<g clip-path="url(#${id})">\n`,
266
+ ...markup,
267
+ "</g>\n"
268
+ ];
269
+ const ellipsis = layout.ellipsis;
270
+ if (ellipsis) {
271
+ const style = ellipsis.style;
272
+ const fill = typeof style.fill === "string" ? style.fill : typeof this.fill === "string" ? this.fill : "#000";
273
+ const family = String(style.fontFamily ?? this.fontFamily).replace(/"/g, "'");
274
+ result.push(`<text xml:space="preserve" x="${ellipsis.x}" y="${ellipsis.baseline - (style.deltaY ?? 0)}" font-family="${family}" font-size="${style.fontSize}" font-style="${style.fontStyle ?? "normal"}" font-weight="${style.fontWeight ?? "normal"}" fill="${fill}">${ELLIPSIS}</text>\n`);
275
+ }
276
+ return result;
277
+ }
278
+ toObject(propertiesToInclude = []) {
279
+ const object = super.toObject([...BOUNDED_PROPERTIES, ...propertiesToInclude]);
280
+ if ("fontSize" in object) object.fontSize = this.baseFontSize;
281
+ return object;
282
+ }
283
+ };
284
+ BoundedTextbox.type = "BoundedTextbox";
285
+ BoundedTextbox.textLayoutProperties = [...Textbox.textLayoutProperties, ...BOUNDED_PROPERTIES];
286
+ BoundedTextbox.cacheProperties = [...Textbox.cacheProperties, ...BOUNDED_PROPERTIES];
287
+ BoundedTextbox.boundedDefaults = {
288
+ breakWords: "anywhere",
289
+ maxHeight: void 0,
290
+ overflow: "visible",
291
+ fit: "none",
292
+ minFontSize: 6,
293
+ shaping: false
294
+ };
295
+ Object.defineProperty(BoundedTextbox.prototype, "fontSize", {
296
+ configurable: true,
297
+ enumerable: true,
298
+ get() {
299
+ return this.baseFontSize * (this.fitScale ?? 1);
300
+ },
301
+ set(value) {
302
+ this.baseFontSize = value;
303
+ }
304
+ });
305
+ //#endregion
306
+ export { BoundedTextbox };
@@ -0,0 +1,146 @@
1
+ const require_text_edit = require("../commands/text-edit.cjs");
2
+ let fabric = require("fabric");
3
+ //#region src/text/mobile-input.ts
4
+ const composing = (text) => Boolean(text.inCompositionMode);
5
+ function findTextEdit(previous, next, cursor) {
6
+ const shorter = Math.min(previous.length, next.length);
7
+ let sharedStart = 0;
8
+ while (sharedStart < shorter && previous[sharedStart] === next[sharedStart]) sharedStart += 1;
9
+ let sharedEnd = 0;
10
+ while (sharedEnd < shorter && previous[previous.length - 1 - sharedEnd] === next[next.length - 1 - sharedEnd]) sharedEnd += 1;
11
+ let from = sharedStart;
12
+ let end = sharedEnd;
13
+ if (sharedStart + sharedEnd > shorter) {
14
+ const grown = next.length - shorter;
15
+ from = Math.min(Math.max(cursor - grown, shorter - sharedEnd), sharedStart);
16
+ end = shorter - from;
17
+ }
18
+ const oldTo = previous.length - end;
19
+ const newTo = next.length - end;
20
+ if (from === oldTo && from === newTo) return void 0;
21
+ return {
22
+ from,
23
+ oldTo,
24
+ newTo
25
+ };
26
+ }
27
+ function anchorOf(text) {
28
+ if (!text.textAlign.startsWith("justify")) return text.textAlign.replace("justify-", "");
29
+ return text.direction === "rtl" ? "right" : "left";
30
+ }
31
+ function applyFromTextarea(event) {
32
+ const textarea = this.hiddenTextarea;
33
+ if (!textarea) return;
34
+ if (this.fromPaste) {
35
+ fabric.IText.prototype.onInput.call(this, event);
36
+ return;
37
+ }
38
+ event?.stopPropagation();
39
+ if (!this.isEditing) return;
40
+ const { value } = textarea;
41
+ const next = this.graphemeSplit(value);
42
+ const selection = this.fromStringToGraphemeSelection(textarea.selectionStart, textarea.selectionEnd, value);
43
+ const edit = findTextEdit(this._text, next, selection.selectionEnd);
44
+ if (edit) {
45
+ const anchored = typeof this.getPositionByOrigin === "function";
46
+ const anchor = anchorOf(this);
47
+ const position = anchored ? this.getPositionByOrigin(anchor, "top") : void 0;
48
+ require_text_edit.replaceTextRange(this, edit.from, edit.oldTo, next.slice(edit.from, edit.newTo).join(""));
49
+ if (position) this.setPositionByOrigin(position, anchor, "top");
50
+ this.setCoords();
51
+ }
52
+ this.selectionStart = selection.selectionStart;
53
+ this.selectionEnd = selection.selectionEnd;
54
+ this.cursorOffsetCache = {};
55
+ if (!composing(this)) this.updateTextareaPosition();
56
+ this.canvas?.requestRenderAll();
57
+ }
58
+ const OWN_HANDLERS = ["onInput"];
59
+ function isText(object) {
60
+ return object instanceof fabric.IText;
61
+ }
62
+ function attachMobileTextInput(canvas, options = {}) {
63
+ const patched = /* @__PURE__ */ new Set();
64
+ const attributes = {
65
+ inputmode: options.inputMode ?? "text",
66
+ enterkeyhint: options.enterKeyHint ?? "enter",
67
+ autocapitalize: options.autocapitalize ?? "sentences",
68
+ autocorrect: options.autocorrect === false ? "off" : "on",
69
+ spellcheck: String(options.spellcheck ?? false)
70
+ };
71
+ const patch = (object) => {
72
+ if (!isText(object) || patched.has(object)) return;
73
+ object.onInput = applyFromTextarea;
74
+ patched.add(object);
75
+ };
76
+ const unpatch = (object) => {
77
+ for (const name of OWN_HANDLERS) delete object[name];
78
+ };
79
+ canvas.getObjects().forEach(patch);
80
+ const editing = () => {
81
+ const active = canvas.getActiveObject();
82
+ return isText(active) && active.isEditing ? active : void 0;
83
+ };
84
+ const onAdded = ({ target }) => patch(target);
85
+ const onEditingEntered = ({ target }) => {
86
+ if (!isText(target)) return;
87
+ patch(target);
88
+ const textarea = target.hiddenTextarea;
89
+ if (!textarea) return;
90
+ for (const [name, value] of Object.entries(attributes)) textarea.setAttribute(name, value);
91
+ textarea.style.fontSize = "16px";
92
+ };
93
+ const onSelectionChange = () => {
94
+ const text = editing();
95
+ const textarea = text?.hiddenTextarea;
96
+ if (!text || !textarea || textarea.ownerDocument.activeElement !== textarea || composing(text)) return;
97
+ const selection = text.fromStringToGraphemeSelection(textarea.selectionStart, textarea.selectionEnd, textarea.value);
98
+ if (selection.selectionStart === text.selectionStart && selection.selectionEnd === text.selectionEnd) return;
99
+ text.selectionStart = selection.selectionStart;
100
+ text.selectionEnd = selection.selectionEnd;
101
+ text.cursorOffsetCache = {};
102
+ text.updateTextareaPosition();
103
+ canvas.clearContext(canvas.contextTop);
104
+ text.renderCursorOrSelection();
105
+ };
106
+ const onTouchEnd = () => {
107
+ const textarea = editing()?.hiddenTextarea;
108
+ if (textarea && textarea.ownerDocument.activeElement !== textarea) textarea.focus({ preventScroll: true });
109
+ };
110
+ let frame = 0;
111
+ const onViewportChange = () => {
112
+ if (frame) return;
113
+ frame = requestAnimationFrame(() => {
114
+ frame = 0;
115
+ const text = editing();
116
+ if (!text) return;
117
+ canvas.calcOffset();
118
+ text.updateTextareaPosition();
119
+ });
120
+ };
121
+ const element = canvas.upperCanvasEl;
122
+ const ownerDocument = element.ownerDocument;
123
+ const viewport = ownerDocument.defaultView?.visualViewport;
124
+ canvas.on("object:added", onAdded);
125
+ canvas.on("text:editing:entered", onEditingEntered);
126
+ ownerDocument.addEventListener("selectionchange", onSelectionChange);
127
+ element.addEventListener("touchend", onTouchEnd, { passive: true });
128
+ viewport?.addEventListener("resize", onViewportChange);
129
+ viewport?.addEventListener("scroll", onViewportChange);
130
+ const active = editing();
131
+ if (active) onEditingEntered({ target: active });
132
+ return { detach() {
133
+ canvas.off("object:added", onAdded);
134
+ canvas.off("text:editing:entered", onEditingEntered);
135
+ ownerDocument.removeEventListener("selectionchange", onSelectionChange);
136
+ element.removeEventListener("touchend", onTouchEnd);
137
+ viewport?.removeEventListener("resize", onViewportChange);
138
+ viewport?.removeEventListener("scroll", onViewportChange);
139
+ if (frame) cancelAnimationFrame(frame);
140
+ patched.forEach(unpatch);
141
+ patched.clear();
142
+ } };
143
+ }
144
+ //#endregion
145
+ exports.attachMobileTextInput = attachMobileTextInput;
146
+ exports.findTextEdit = findTextEdit;
@@ -0,0 +1,19 @@
1
+ import { Canvas } from "fabric";
2
+ //#region src/text/mobile-input.d.ts
3
+ export interface MobileTextInputOptions {
4
+ inputMode?: string;
5
+ enterKeyHint?: string;
6
+ autocapitalize?: string;
7
+ autocorrect?: boolean;
8
+ spellcheck?: boolean;
9
+ }
10
+ export interface MobileTextInput {
11
+ detach(): void;
12
+ }
13
+ export declare function findTextEdit(previous: readonly string[], next: readonly string[], cursor: number): {
14
+ from: number;
15
+ oldTo: number;
16
+ newTo: number;
17
+ } | undefined;
18
+ export declare function attachMobileTextInput(canvas: Canvas, options?: MobileTextInputOptions): MobileTextInput;
19
+ //#endregion
@@ -0,0 +1,19 @@
1
+ import { Canvas } from "fabric";
2
+ //#region src/text/mobile-input.d.ts
3
+ export interface MobileTextInputOptions {
4
+ inputMode?: string;
5
+ enterKeyHint?: string;
6
+ autocapitalize?: string;
7
+ autocorrect?: boolean;
8
+ spellcheck?: boolean;
9
+ }
10
+ export interface MobileTextInput {
11
+ detach(): void;
12
+ }
13
+ export declare function findTextEdit(previous: readonly string[], next: readonly string[], cursor: number): {
14
+ from: number;
15
+ oldTo: number;
16
+ newTo: number;
17
+ } | undefined;
18
+ export declare function attachMobileTextInput(canvas: Canvas, options?: MobileTextInputOptions): MobileTextInput;
19
+ //#endregion
@@ -0,0 +1,145 @@
1
+ import { replaceTextRange } from "../commands/text-edit.js";
2
+ import { IText } from "fabric";
3
+ //#region src/text/mobile-input.ts
4
+ const composing = (text) => Boolean(text.inCompositionMode);
5
+ function findTextEdit(previous, next, cursor) {
6
+ const shorter = Math.min(previous.length, next.length);
7
+ let sharedStart = 0;
8
+ while (sharedStart < shorter && previous[sharedStart] === next[sharedStart]) sharedStart += 1;
9
+ let sharedEnd = 0;
10
+ while (sharedEnd < shorter && previous[previous.length - 1 - sharedEnd] === next[next.length - 1 - sharedEnd]) sharedEnd += 1;
11
+ let from = sharedStart;
12
+ let end = sharedEnd;
13
+ if (sharedStart + sharedEnd > shorter) {
14
+ const grown = next.length - shorter;
15
+ from = Math.min(Math.max(cursor - grown, shorter - sharedEnd), sharedStart);
16
+ end = shorter - from;
17
+ }
18
+ const oldTo = previous.length - end;
19
+ const newTo = next.length - end;
20
+ if (from === oldTo && from === newTo) return void 0;
21
+ return {
22
+ from,
23
+ oldTo,
24
+ newTo
25
+ };
26
+ }
27
+ function anchorOf(text) {
28
+ if (!text.textAlign.startsWith("justify")) return text.textAlign.replace("justify-", "");
29
+ return text.direction === "rtl" ? "right" : "left";
30
+ }
31
+ function applyFromTextarea(event) {
32
+ const textarea = this.hiddenTextarea;
33
+ if (!textarea) return;
34
+ if (this.fromPaste) {
35
+ IText.prototype.onInput.call(this, event);
36
+ return;
37
+ }
38
+ event?.stopPropagation();
39
+ if (!this.isEditing) return;
40
+ const { value } = textarea;
41
+ const next = this.graphemeSplit(value);
42
+ const selection = this.fromStringToGraphemeSelection(textarea.selectionStart, textarea.selectionEnd, value);
43
+ const edit = findTextEdit(this._text, next, selection.selectionEnd);
44
+ if (edit) {
45
+ const anchored = typeof this.getPositionByOrigin === "function";
46
+ const anchor = anchorOf(this);
47
+ const position = anchored ? this.getPositionByOrigin(anchor, "top") : void 0;
48
+ replaceTextRange(this, edit.from, edit.oldTo, next.slice(edit.from, edit.newTo).join(""));
49
+ if (position) this.setPositionByOrigin(position, anchor, "top");
50
+ this.setCoords();
51
+ }
52
+ this.selectionStart = selection.selectionStart;
53
+ this.selectionEnd = selection.selectionEnd;
54
+ this.cursorOffsetCache = {};
55
+ if (!composing(this)) this.updateTextareaPosition();
56
+ this.canvas?.requestRenderAll();
57
+ }
58
+ const OWN_HANDLERS = ["onInput"];
59
+ function isText(object) {
60
+ return object instanceof IText;
61
+ }
62
+ function attachMobileTextInput(canvas, options = {}) {
63
+ const patched = /* @__PURE__ */ new Set();
64
+ const attributes = {
65
+ inputmode: options.inputMode ?? "text",
66
+ enterkeyhint: options.enterKeyHint ?? "enter",
67
+ autocapitalize: options.autocapitalize ?? "sentences",
68
+ autocorrect: options.autocorrect === false ? "off" : "on",
69
+ spellcheck: String(options.spellcheck ?? false)
70
+ };
71
+ const patch = (object) => {
72
+ if (!isText(object) || patched.has(object)) return;
73
+ object.onInput = applyFromTextarea;
74
+ patched.add(object);
75
+ };
76
+ const unpatch = (object) => {
77
+ for (const name of OWN_HANDLERS) delete object[name];
78
+ };
79
+ canvas.getObjects().forEach(patch);
80
+ const editing = () => {
81
+ const active = canvas.getActiveObject();
82
+ return isText(active) && active.isEditing ? active : void 0;
83
+ };
84
+ const onAdded = ({ target }) => patch(target);
85
+ const onEditingEntered = ({ target }) => {
86
+ if (!isText(target)) return;
87
+ patch(target);
88
+ const textarea = target.hiddenTextarea;
89
+ if (!textarea) return;
90
+ for (const [name, value] of Object.entries(attributes)) textarea.setAttribute(name, value);
91
+ textarea.style.fontSize = "16px";
92
+ };
93
+ const onSelectionChange = () => {
94
+ const text = editing();
95
+ const textarea = text?.hiddenTextarea;
96
+ if (!text || !textarea || textarea.ownerDocument.activeElement !== textarea || composing(text)) return;
97
+ const selection = text.fromStringToGraphemeSelection(textarea.selectionStart, textarea.selectionEnd, textarea.value);
98
+ if (selection.selectionStart === text.selectionStart && selection.selectionEnd === text.selectionEnd) return;
99
+ text.selectionStart = selection.selectionStart;
100
+ text.selectionEnd = selection.selectionEnd;
101
+ text.cursorOffsetCache = {};
102
+ text.updateTextareaPosition();
103
+ canvas.clearContext(canvas.contextTop);
104
+ text.renderCursorOrSelection();
105
+ };
106
+ const onTouchEnd = () => {
107
+ const textarea = editing()?.hiddenTextarea;
108
+ if (textarea && textarea.ownerDocument.activeElement !== textarea) textarea.focus({ preventScroll: true });
109
+ };
110
+ let frame = 0;
111
+ const onViewportChange = () => {
112
+ if (frame) return;
113
+ frame = requestAnimationFrame(() => {
114
+ frame = 0;
115
+ const text = editing();
116
+ if (!text) return;
117
+ canvas.calcOffset();
118
+ text.updateTextareaPosition();
119
+ });
120
+ };
121
+ const element = canvas.upperCanvasEl;
122
+ const ownerDocument = element.ownerDocument;
123
+ const viewport = ownerDocument.defaultView?.visualViewport;
124
+ canvas.on("object:added", onAdded);
125
+ canvas.on("text:editing:entered", onEditingEntered);
126
+ ownerDocument.addEventListener("selectionchange", onSelectionChange);
127
+ element.addEventListener("touchend", onTouchEnd, { passive: true });
128
+ viewport?.addEventListener("resize", onViewportChange);
129
+ viewport?.addEventListener("scroll", onViewportChange);
130
+ const active = editing();
131
+ if (active) onEditingEntered({ target: active });
132
+ return { detach() {
133
+ canvas.off("object:added", onAdded);
134
+ canvas.off("text:editing:entered", onEditingEntered);
135
+ ownerDocument.removeEventListener("selectionchange", onSelectionChange);
136
+ element.removeEventListener("touchend", onTouchEnd);
137
+ viewport?.removeEventListener("resize", onViewportChange);
138
+ viewport?.removeEventListener("scroll", onViewportChange);
139
+ if (frame) cancelAnimationFrame(frame);
140
+ patched.forEach(unpatch);
141
+ patched.clear();
142
+ } };
143
+ }
144
+ //#endregion
145
+ export { attachMobileTextInput, findTextEdit };
@@ -0,0 +1,17 @@
1
+ const require_shaping = require("./shaping.cjs");
2
+ const require_bounded_textbox = require("./bounded-textbox.cjs");
3
+ const require_vertical_text = require("./vertical-text.cjs");
4
+ let fabric = require("fabric");
5
+ //#region src/text/register.ts
6
+ const textObjects = [
7
+ { fabricClass: require_bounded_textbox.BoundedTextbox },
8
+ { fabricClass: require_shaping.ShapedIText },
9
+ { fabricClass: require_shaping.ShapedTextbox },
10
+ { fabricClass: require_vertical_text.VerticalText }
11
+ ];
12
+ function registerTextObjects() {
13
+ for (const { fabricClass } of textObjects) fabric.classRegistry.setClass(fabricClass);
14
+ }
15
+ //#endregion
16
+ exports.registerTextObjects = registerTextObjects;
17
+ exports.textObjects = textObjects;
@@ -0,0 +1,5 @@
1
+ import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
2
+ //#region src/text/register.d.ts
3
+ export declare const textObjects: CustomObjectDefinition[];
4
+ export declare function registerTextObjects(): void;
5
+ //#endregion
@@ -0,0 +1,5 @@
1
+ import { CustomObjectDefinition } from "../fabric/object-registry.js";
2
+ //#region src/text/register.d.ts
3
+ export declare const textObjects: CustomObjectDefinition[];
4
+ export declare function registerTextObjects(): void;
5
+ //#endregion
@@ -0,0 +1,16 @@
1
+ import { ShapedIText, ShapedTextbox } from "./shaping.js";
2
+ import { BoundedTextbox } from "./bounded-textbox.js";
3
+ import { VerticalText } from "./vertical-text.js";
4
+ import { classRegistry } from "fabric";
5
+ //#region src/text/register.ts
6
+ const textObjects = [
7
+ { fabricClass: BoundedTextbox },
8
+ { fabricClass: ShapedIText },
9
+ { fabricClass: ShapedTextbox },
10
+ { fabricClass: VerticalText }
11
+ ];
12
+ function registerTextObjects() {
13
+ for (const { fabricClass } of textObjects) classRegistry.setClass(fabricClass);
14
+ }
15
+ //#endregion
16
+ export { registerTextObjects, textObjects };