@sofereditor/react 0.6.0 → 0.7.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/dist/index.cjs +306 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +195 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -264,6 +264,15 @@ interface UseEditorResult {
|
|
|
264
264
|
cellIndex?: number;
|
|
265
265
|
embed: ImageEmbed;
|
|
266
266
|
} | null;
|
|
267
|
+
/**
|
|
268
|
+
* Passthrough of `UseEditorOptions.uploadImage` — `undefined` when not
|
|
269
|
+
* configured. Exposed so callers with the editor instance but not the
|
|
270
|
+
* original options (the `<Editor>` component's paste handler, notably) can
|
|
271
|
+
* upload an image without going through `insertImageFromFile` (which also
|
|
272
|
+
* inserts it — not always what's wanted, e.g. resolving `data:` embeds
|
|
273
|
+
* already inside a pasted slice before `insertSlice`).
|
|
274
|
+
*/
|
|
275
|
+
uploadImage?: (file: File) => Promise<string>;
|
|
267
276
|
}
|
|
268
277
|
declare function useEditor(opts?: UseEditorOptions): UseEditorResult;
|
|
269
278
|
|
package/dist/index.d.ts
CHANGED
|
@@ -264,6 +264,15 @@ interface UseEditorResult {
|
|
|
264
264
|
cellIndex?: number;
|
|
265
265
|
embed: ImageEmbed;
|
|
266
266
|
} | null;
|
|
267
|
+
/**
|
|
268
|
+
* Passthrough of `UseEditorOptions.uploadImage` — `undefined` when not
|
|
269
|
+
* configured. Exposed so callers with the editor instance but not the
|
|
270
|
+
* original options (the `<Editor>` component's paste handler, notably) can
|
|
271
|
+
* upload an image without going through `insertImageFromFile` (which also
|
|
272
|
+
* inserts it — not always what's wanted, e.g. resolving `data:` embeds
|
|
273
|
+
* already inside a pasted slice before `insertSlice`).
|
|
274
|
+
*/
|
|
275
|
+
uploadImage?: (file: File) => Promise<string>;
|
|
267
276
|
}
|
|
268
277
|
declare function useEditor(opts?: UseEditorOptions): UseEditorResult;
|
|
269
278
|
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useState as useState10
|
|
9
9
|
} from "react";
|
|
10
10
|
import {
|
|
11
|
+
collapsedSelection as collapsedSelection2,
|
|
11
12
|
deleteBackward,
|
|
12
13
|
deleteForward,
|
|
13
14
|
deleteSelection,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
insertParagraph,
|
|
16
17
|
insertSlice,
|
|
17
18
|
insertText,
|
|
19
|
+
isEmbedAdjacentToCaret,
|
|
18
20
|
serializeSelection,
|
|
19
21
|
sliceToText,
|
|
20
22
|
SOFER_MIME as SOFER_MIME2
|
|
@@ -297,7 +299,12 @@ function applyDomSelection(root, modelSel) {
|
|
|
297
299
|
import { SOFER_MIME } from "@sofereditor/core";
|
|
298
300
|
|
|
299
301
|
// src/htmlToSlice.ts
|
|
300
|
-
import { MAX_LIST_LEVEL } from "@sofereditor/core";
|
|
302
|
+
import { isImageEmbed, MAX_LIST_LEVEL } from "@sofereditor/core";
|
|
303
|
+
|
|
304
|
+
// src/imageConstraints.ts
|
|
305
|
+
var MAX_INSERT_WIDTH = 600;
|
|
306
|
+
|
|
307
|
+
// src/htmlToSlice.ts
|
|
301
308
|
function htmlToSlice(html) {
|
|
302
309
|
if (!html || html.trim().length === 0) return null;
|
|
303
310
|
let doc;
|
|
@@ -316,8 +323,10 @@ function htmlToSlice(html) {
|
|
|
316
323
|
}
|
|
317
324
|
if (blocks.length === 0) return null;
|
|
318
325
|
normalizeListLevels(blocks);
|
|
319
|
-
const
|
|
320
|
-
|
|
326
|
+
const temConteudo = blocks.some(
|
|
327
|
+
(b) => b.text.trim().length > 0 || b.delta.some((op) => isImageEmbed(op.insert))
|
|
328
|
+
);
|
|
329
|
+
if (!temConteudo) return null;
|
|
321
330
|
const blockLevel = blocks.length > 1 || blocks[0].type !== "paragraph";
|
|
322
331
|
return { blocks, openStart: false, openEnd: false, ...blockLevel ? { blockLevel: true } : {} };
|
|
323
332
|
}
|
|
@@ -428,6 +437,7 @@ function walkBlockLevel(container, marks, blocks) {
|
|
|
428
437
|
break;
|
|
429
438
|
}
|
|
430
439
|
case "img":
|
|
440
|
+
loose.push(node);
|
|
431
441
|
break;
|
|
432
442
|
case "br":
|
|
433
443
|
loose.push(node);
|
|
@@ -620,7 +630,11 @@ function walkInlineNode(node, marks, ops) {
|
|
|
620
630
|
const el = node;
|
|
621
631
|
const tag = el.tagName.toLowerCase();
|
|
622
632
|
if (IGNORED_TAGS.has(tag) || isNamespacedTag(tag)) return;
|
|
623
|
-
if (tag === "img")
|
|
633
|
+
if (tag === "img") {
|
|
634
|
+
const op = imageEmbedFromElement(el);
|
|
635
|
+
if (op) ops.push(op);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
624
638
|
if (tag === "br") {
|
|
625
639
|
ops.push({ insert: " " });
|
|
626
640
|
return;
|
|
@@ -635,6 +649,73 @@ function walkInlineNode(node, marks, ops) {
|
|
|
635
649
|
}
|
|
636
650
|
for (const child of Array.from(el.childNodes)) walkInlineNode(child, nextMarks, ops);
|
|
637
651
|
}
|
|
652
|
+
var MIN_EMBED_BYTES = 1024;
|
|
653
|
+
function decodedDataUrlByteLength(dataUrl) {
|
|
654
|
+
const commaIndex = dataUrl.indexOf(",");
|
|
655
|
+
if (commaIndex === -1) return 0;
|
|
656
|
+
const meta = dataUrl.slice(0, commaIndex);
|
|
657
|
+
const payload = dataUrl.slice(commaIndex + 1);
|
|
658
|
+
if (/;base64$/i.test(meta)) {
|
|
659
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
660
|
+
return Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
return decodeURIComponent(payload).length;
|
|
664
|
+
} catch {
|
|
665
|
+
return payload.length;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function imageEmbedFromElement(el) {
|
|
669
|
+
const src = el.getAttribute("src");
|
|
670
|
+
if (!src || !src.startsWith("data:image/")) return null;
|
|
671
|
+
if (decodedDataUrlByteLength(src) < MIN_EMBED_BYTES) return null;
|
|
672
|
+
const style = parseStyle(el.getAttribute("style"));
|
|
673
|
+
const width = readImageDimension(el, "width", style);
|
|
674
|
+
const height = readImageDimension(el, "height", style);
|
|
675
|
+
if (width == null || height == null) return null;
|
|
676
|
+
let w = width;
|
|
677
|
+
let h = height;
|
|
678
|
+
if (w > MAX_INSERT_WIDTH) {
|
|
679
|
+
h = h * MAX_INSERT_WIDTH / w;
|
|
680
|
+
w = MAX_INSERT_WIDTH;
|
|
681
|
+
}
|
|
682
|
+
return { insert: { type: "image", src, width: Math.round(w), height: Math.round(h) } };
|
|
683
|
+
}
|
|
684
|
+
function readImageDimension(el, attr, style) {
|
|
685
|
+
const raw = el.getAttribute(attr);
|
|
686
|
+
if (raw && /^\d+(\.\d+)?$/.test(raw.trim())) {
|
|
687
|
+
const n = Number.parseFloat(raw);
|
|
688
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
689
|
+
}
|
|
690
|
+
const fromStyle = style[attr];
|
|
691
|
+
if (fromStyle) {
|
|
692
|
+
const n = cssLengthToPx(fromStyle);
|
|
693
|
+
if (n != null) return n;
|
|
694
|
+
}
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
697
|
+
function cssLengthToPx(raw) {
|
|
698
|
+
const v = raw.trim();
|
|
699
|
+
const m = /^(-?[\d.]+)\s*(px|pt|in|cm|mm)?$/i.exec(v);
|
|
700
|
+
if (!m) return void 0;
|
|
701
|
+
const num = Number.parseFloat(m[1]);
|
|
702
|
+
if (!Number.isFinite(num) || num <= 0) return void 0;
|
|
703
|
+
const unit = (m[2] ?? "px").toLowerCase();
|
|
704
|
+
switch (unit) {
|
|
705
|
+
case "px":
|
|
706
|
+
return num;
|
|
707
|
+
case "pt":
|
|
708
|
+
return num * (96 / 72);
|
|
709
|
+
case "in":
|
|
710
|
+
return num * 96;
|
|
711
|
+
case "cm":
|
|
712
|
+
return num * (96 / 2.54);
|
|
713
|
+
case "mm":
|
|
714
|
+
return num * (96 / 25.4);
|
|
715
|
+
default:
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
638
719
|
function applyTagMarks(tag, marks) {
|
|
639
720
|
switch (tag) {
|
|
640
721
|
case "b":
|
|
@@ -902,6 +983,58 @@ function planPaste(cd) {
|
|
|
902
983
|
return { kind: "none" };
|
|
903
984
|
}
|
|
904
985
|
|
|
986
|
+
// src/resolvePastedImages.ts
|
|
987
|
+
import { isImageEmbed as isImageEmbed2 } from "@sofereditor/core";
|
|
988
|
+
function sliceHasDataImageEmbeds(slice) {
|
|
989
|
+
return slice.blocks.some(
|
|
990
|
+
(b) => b.delta.some((op) => isImageEmbed2(op.insert) && op.insert.src.startsWith("data:image/"))
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
async function resolvePastedImageUploads(slice, upload) {
|
|
994
|
+
if (!upload) return slice;
|
|
995
|
+
const blocks = await Promise.all(
|
|
996
|
+
slice.blocks.map(async (block) => {
|
|
997
|
+
let changed = false;
|
|
998
|
+
const delta = await Promise.all(
|
|
999
|
+
block.delta.map(async (op) => {
|
|
1000
|
+
if (!isImageEmbed2(op.insert) || !op.insert.src.startsWith("data:image/")) return op;
|
|
1001
|
+
try {
|
|
1002
|
+
const file = await dataUrlToFile(op.insert.src);
|
|
1003
|
+
const newSrc = await upload(file);
|
|
1004
|
+
changed = true;
|
|
1005
|
+
return { ...op, insert: { ...op.insert, src: newSrc } };
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
console.error(
|
|
1008
|
+
"[sofereditor] falha ao subir imagem colada para o storage \u2014 mantendo a imagem embutida (data:) nesta colagem",
|
|
1009
|
+
err
|
|
1010
|
+
);
|
|
1011
|
+
return op;
|
|
1012
|
+
}
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
return changed ? { ...block, delta } : block;
|
|
1016
|
+
})
|
|
1017
|
+
);
|
|
1018
|
+
return { ...slice, blocks };
|
|
1019
|
+
}
|
|
1020
|
+
async function dataUrlToFile(dataUrl) {
|
|
1021
|
+
const blob = await fetch(dataUrl).then((r) => r.blob());
|
|
1022
|
+
const mime = blob.type;
|
|
1023
|
+
return new File([blob], `imagem-colada-${Date.now()}.${extensionForMime(mime)}`, { type: mime });
|
|
1024
|
+
}
|
|
1025
|
+
var MIME_EXTENSIONS = {
|
|
1026
|
+
"image/png": "png",
|
|
1027
|
+
"image/jpeg": "jpg",
|
|
1028
|
+
"image/jpg": "jpg",
|
|
1029
|
+
"image/gif": "gif",
|
|
1030
|
+
"image/webp": "webp",
|
|
1031
|
+
"image/bmp": "bmp",
|
|
1032
|
+
"image/svg+xml": "svg"
|
|
1033
|
+
};
|
|
1034
|
+
function extensionForMime(mime) {
|
|
1035
|
+
return MIME_EXTENSIONS[mime.toLowerCase()] ?? "png";
|
|
1036
|
+
}
|
|
1037
|
+
|
|
905
1038
|
// src/BehindImageSelectAffordance.tsx
|
|
906
1039
|
import {
|
|
907
1040
|
useCallback,
|
|
@@ -2327,7 +2460,7 @@ import {
|
|
|
2327
2460
|
import { Fragment as Fragment3, memo } from "react";
|
|
2328
2461
|
import {
|
|
2329
2462
|
BLANK_STYLE,
|
|
2330
|
-
isImageEmbed,
|
|
2463
|
+
isImageEmbed as isImageEmbed3,
|
|
2331
2464
|
splitUnderscoreRuns
|
|
2332
2465
|
} from "@sofereditor/core";
|
|
2333
2466
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
@@ -2345,7 +2478,7 @@ function renderInline(delta, keyPrefix) {
|
|
|
2345
2478
|
const key = `${keyPrefix}-${offset}`;
|
|
2346
2479
|
body.push(/* @__PURE__ */ jsx9(Fragment3, { children: wrap(text, op.attributes) }, key));
|
|
2347
2480
|
offset += text.length;
|
|
2348
|
-
} else if (
|
|
2481
|
+
} else if (isImageEmbed3(op.insert)) {
|
|
2349
2482
|
const key = `${keyPrefix}-${offset}`;
|
|
2350
2483
|
const layout = op.insert.layout ?? "inline";
|
|
2351
2484
|
const commentMarkId = op.attributes?.comment?.markId;
|
|
@@ -3460,7 +3593,7 @@ import {
|
|
|
3460
3593
|
insertTable as cmdInsertTable,
|
|
3461
3594
|
insertTableColumn as cmdInsertColumn,
|
|
3462
3595
|
insertTableRow as cmdInsertRow,
|
|
3463
|
-
isImageEmbed as
|
|
3596
|
+
isImageEmbed as isImageEmbed4,
|
|
3464
3597
|
isMarkUniformInRange,
|
|
3465
3598
|
moveToNextCell as cmdMoveNextCell,
|
|
3466
3599
|
moveToPrevCell as cmdMovePrevCell,
|
|
@@ -3475,7 +3608,6 @@ import {
|
|
|
3475
3608
|
toggleList as cmdToggleList,
|
|
3476
3609
|
toggleMark as cmdToggleMark
|
|
3477
3610
|
} from "@sofereditor/core";
|
|
3478
|
-
var MAX_INSERT_WIDTH = 600;
|
|
3479
3611
|
function useEditor(opts = {}) {
|
|
3480
3612
|
const doc = useMemo3(() => opts.document ?? new EditorDocument(), [opts.document]);
|
|
3481
3613
|
const history = useMemo3(() => new EditorHistory(doc), [doc]);
|
|
@@ -3862,7 +3994,7 @@ function useEditor(opts = {}) {
|
|
|
3862
3994
|
cursor += op.insert.length;
|
|
3863
3995
|
continue;
|
|
3864
3996
|
}
|
|
3865
|
-
if (cursor === start &&
|
|
3997
|
+
if (cursor === start && isImageEmbed4(op.insert)) {
|
|
3866
3998
|
return {
|
|
3867
3999
|
blockIndex: sel.anchor.blockIndex,
|
|
3868
4000
|
cellIndex: sel.anchor.cellIndex,
|
|
@@ -3995,7 +4127,8 @@ function useEditor(opts = {}) {
|
|
|
3995
4127
|
resolvePageConfigRequest,
|
|
3996
4128
|
imageCaptionRequest,
|
|
3997
4129
|
requestImageCaption,
|
|
3998
|
-
resolveImageCaptionRequest
|
|
4130
|
+
resolveImageCaptionRequest,
|
|
4131
|
+
uploadImage: opts.uploadImage
|
|
3999
4132
|
};
|
|
4000
4133
|
}
|
|
4001
4134
|
function readAsDataURL(file) {
|
|
@@ -4042,6 +4175,7 @@ function Editor({
|
|
|
4042
4175
|
const { doc, history, snapshot, getSelection, setSelection } = editor;
|
|
4043
4176
|
const rootRef = useRef11(null);
|
|
4044
4177
|
const isComposingRef = useRef11(false);
|
|
4178
|
+
const pasteInFlightRef = useRef11(false);
|
|
4045
4179
|
const [hoveredBehind, setHoveredBehind] = useState10(null);
|
|
4046
4180
|
const hoveredBehindRef = useRef11(null);
|
|
4047
4181
|
const [hoveredLink, setHoveredLink] = useState10(null);
|
|
@@ -4484,9 +4618,48 @@ function Editor({
|
|
|
4484
4618
|
e.preventDefault();
|
|
4485
4619
|
switch (plan.kind) {
|
|
4486
4620
|
case "sofer":
|
|
4487
|
-
case "html":
|
|
4488
4621
|
insertSlice(ctx, plan.slice);
|
|
4489
4622
|
return;
|
|
4623
|
+
case "html": {
|
|
4624
|
+
if (!sliceHasDataImageEmbeds(plan.slice)) {
|
|
4625
|
+
insertSlice(ctx, plan.slice);
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
if (pasteInFlightRef.current) {
|
|
4629
|
+
console.warn(
|
|
4630
|
+
"[sofereditor] colagem com imagem ainda subindo \u2014 descartando esta colagem concorrente para n\xE3o corromper a posi\xE7\xE3o de inser\xE7\xE3o"
|
|
4631
|
+
);
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
pasteInFlightRef.current = true;
|
|
4635
|
+
const capturedSelection = ctx.getSelection();
|
|
4636
|
+
void (async () => {
|
|
4637
|
+
try {
|
|
4638
|
+
const resolved = await resolvePastedImageUploads(plan.slice, editorRef.current.uploadImage);
|
|
4639
|
+
const liveSelection = ctxRef.current.getSelection();
|
|
4640
|
+
const userMovedAway = !selectionsEqual(liveSelection, capturedSelection);
|
|
4641
|
+
const { blockIndex, cellIndex } = capturedSelection.focus;
|
|
4642
|
+
const targetText = ctxRef.current.doc.textAt(blockIndex, cellIndex);
|
|
4643
|
+
let insertAt = capturedSelection;
|
|
4644
|
+
if (!targetText) {
|
|
4645
|
+
console.error(
|
|
4646
|
+
"[sofereditor] bloco de destino da colagem sumiu durante o upload da imagem \u2014 inserindo no fim do documento em vez de descartar a colagem"
|
|
4647
|
+
);
|
|
4648
|
+
const lastIndex = Math.max(0, ctxRef.current.doc.blockCount() - 1);
|
|
4649
|
+
const lastText = ctxRef.current.doc.getBlockText(lastIndex);
|
|
4650
|
+
insertAt = collapsedSelection2({ blockIndex: lastIndex, offset: lastText ? lastText.length : 0 });
|
|
4651
|
+
}
|
|
4652
|
+
ctxRef.current.setSelection(insertAt);
|
|
4653
|
+
insertSlice(ctxRef.current, resolved);
|
|
4654
|
+
if (userMovedAway) {
|
|
4655
|
+
ctxRef.current.setSelection(liveSelection);
|
|
4656
|
+
}
|
|
4657
|
+
} finally {
|
|
4658
|
+
pasteInFlightRef.current = false;
|
|
4659
|
+
}
|
|
4660
|
+
})();
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4490
4663
|
case "images":
|
|
4491
4664
|
void (async () => {
|
|
4492
4665
|
for (const f of plan.files) await editorRef.current.insertImageFromFile(f);
|
|
@@ -4539,6 +4712,17 @@ function Editor({
|
|
|
4539
4712
|
}
|
|
4540
4713
|
return;
|
|
4541
4714
|
}
|
|
4715
|
+
if (!mod && (e.key === "Backspace" || e.key === "Delete")) {
|
|
4716
|
+
if (isComposingRef.current) return;
|
|
4717
|
+
const direction = e.key === "Backspace" ? "backward" : "forward";
|
|
4718
|
+
const shouldIntercept = ed.getSelectedEmbed() != null || isEmbedAdjacentToCaret(ctxRef.current, direction);
|
|
4719
|
+
if (shouldIntercept) {
|
|
4720
|
+
e.preventDefault();
|
|
4721
|
+
if (direction === "backward") deleteBackward(ctxRef.current);
|
|
4722
|
+
else deleteForward(ctxRef.current);
|
|
4723
|
+
}
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4542
4726
|
if (!mod) return;
|
|
4543
4727
|
const key = e.key.toLowerCase();
|
|
4544
4728
|
if (!e.shiftKey && !e.altKey && key === "z") {
|