@inditextech/weave-sdk 5.7.0-SNAPSHOT.596.1 → 5.7.0-SNAPSHOT.599.1
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/sdk.js +1337 -145
- package/dist/sdk.node.js +1337 -145
- package/dist/sdk.node.stats.html +1 -1
- package/dist/sdk.stats.html +1 -1
- package/dist/types.d.ts +299 -33
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1337 -145
- package/dist/types.js.map +1 -1
- package/dist/types.stats.html +1 -1
- package/package.json +2 -2
package/dist/types.js
CHANGED
|
@@ -4771,12 +4771,39 @@ function registerKeyboardHandlers(ctx) {
|
|
|
4771
4771
|
}
|
|
4772
4772
|
}
|
|
4773
4773
|
if (e.code === "Backspace" || e.code === "Delete") {
|
|
4774
|
-
|
|
4774
|
+
queueMicrotask(() => {
|
|
4775
4775
|
ctx.removeSelectedNodes();
|
|
4776
4776
|
});
|
|
4777
4777
|
return;
|
|
4778
4778
|
}
|
|
4779
4779
|
}, { signal });
|
|
4780
|
+
const isModifier = (e) => e.key === "Control" || e.key === "Meta";
|
|
4781
|
+
const hideHover = () => ctx.getTransformerController().getHoverTransformer().nodes([]);
|
|
4782
|
+
const restoreHover = () => {
|
|
4783
|
+
const pos = stage.getPointerPosition();
|
|
4784
|
+
const shape = pos ? stage.getIntersection(pos) : null;
|
|
4785
|
+
if (!shape) return;
|
|
4786
|
+
shape.fire("pointerover", {
|
|
4787
|
+
evt: {
|
|
4788
|
+
ctrlKey: false,
|
|
4789
|
+
metaKey: false
|
|
4790
|
+
},
|
|
4791
|
+
target: shape
|
|
4792
|
+
}, true);
|
|
4793
|
+
};
|
|
4794
|
+
window.addEventListener("keydown", (e) => {
|
|
4795
|
+
if (isModifier(e)) hideHover();
|
|
4796
|
+
}, {
|
|
4797
|
+
signal,
|
|
4798
|
+
capture: true
|
|
4799
|
+
});
|
|
4800
|
+
window.addEventListener("keyup", (e) => {
|
|
4801
|
+
if (isModifier(e)) restoreHover();
|
|
4802
|
+
}, {
|
|
4803
|
+
signal,
|
|
4804
|
+
capture: true
|
|
4805
|
+
});
|
|
4806
|
+
window.addEventListener("blur", restoreHover, { signal });
|
|
4780
4807
|
stage.container().addEventListener("keyup", (e) => {
|
|
4781
4808
|
if (e.code === "Space") ctx.setSpaceKeyPressed(false);
|
|
4782
4809
|
}, { signal });
|
|
@@ -6532,7 +6559,10 @@ var WeaveNode = class {
|
|
|
6532
6559
|
}
|
|
6533
6560
|
handleMouseOver(e, node) {
|
|
6534
6561
|
const stage = this.instance.getStage();
|
|
6535
|
-
if (e.evt.ctrlKey || e.evt.metaKey)
|
|
6562
|
+
if (e.evt.ctrlKey || e.evt.metaKey) {
|
|
6563
|
+
this.hideHoverState();
|
|
6564
|
+
return false;
|
|
6565
|
+
}
|
|
6536
6566
|
const user = this.instance.getStore().getUser();
|
|
6537
6567
|
const activeAction = this.instance.getActiveAction();
|
|
6538
6568
|
const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled();
|
|
@@ -6579,7 +6609,10 @@ var WeaveNode = class {
|
|
|
6579
6609
|
return cancelBubble;
|
|
6580
6610
|
}
|
|
6581
6611
|
handleMouseout(e, node) {
|
|
6582
|
-
if (e.evt?.ctrlKey || e.evt?.metaKey)
|
|
6612
|
+
if (e.evt?.ctrlKey || e.evt?.metaKey) {
|
|
6613
|
+
this.hideHoverState();
|
|
6614
|
+
return false;
|
|
6615
|
+
}
|
|
6583
6616
|
if (this.instance.getInstanceRecursive(node)) this.hideHoverState();
|
|
6584
6617
|
}
|
|
6585
6618
|
create(key, props) {
|
|
@@ -9502,7 +9535,7 @@ var WeaveRegisterManager = class {
|
|
|
9502
9535
|
};
|
|
9503
9536
|
//#endregion
|
|
9504
9537
|
//#region package.json
|
|
9505
|
-
var version = "5.7.0-SNAPSHOT.
|
|
9538
|
+
var version = "5.7.0-SNAPSHOT.599.1";
|
|
9506
9539
|
//#endregion
|
|
9507
9540
|
//#region src/managers/setup.ts
|
|
9508
9541
|
var WeaveSetupManager = class {
|
|
@@ -13804,7 +13837,7 @@ var WeaveTextNode = class extends WeaveNode {
|
|
|
13804
13837
|
delete cleanedAttrs.dragBoundFunc;
|
|
13805
13838
|
if (cleanedAttrs.link) {
|
|
13806
13839
|
if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
|
|
13807
|
-
|
|
13840
|
+
cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
|
|
13808
13841
|
}
|
|
13809
13842
|
return {
|
|
13810
13843
|
key: attrs.id ?? "",
|
|
@@ -14440,6 +14473,31 @@ var WeaveTextNode = class extends WeaveNode {
|
|
|
14440
14473
|
};
|
|
14441
14474
|
//#endregion
|
|
14442
14475
|
//#region src/nodes/text-v2/model.ts
|
|
14476
|
+
const MIN_LINE_HEIGHT = .5;
|
|
14477
|
+
/** Valid `lineHeight` mark of a run, or undefined (use the node value). */
|
|
14478
|
+
function runLineHeight(m) {
|
|
14479
|
+
const v = m?.lineHeight;
|
|
14480
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(4, Math.max(MIN_LINE_HEIGHT, v)) : void 0;
|
|
14481
|
+
}
|
|
14482
|
+
/** Only absolute http(s) URLs are links (same rule as the legacy node). */
|
|
14483
|
+
function isSafeLink(value) {
|
|
14484
|
+
if (typeof value !== "string") return false;
|
|
14485
|
+
try {
|
|
14486
|
+
return ["http:", "https:"].includes(new URL(value).protocol);
|
|
14487
|
+
} catch {
|
|
14488
|
+
return false;
|
|
14489
|
+
}
|
|
14490
|
+
}
|
|
14491
|
+
/** Block fields that describe structure (not content). */
|
|
14492
|
+
function blockFields(block, withStart) {
|
|
14493
|
+
const out = {};
|
|
14494
|
+
if (!block) return out;
|
|
14495
|
+
if (block.kind) out.kind = block.kind;
|
|
14496
|
+
if (block.depth) out.depth = block.depth;
|
|
14497
|
+
if (block.align) out.align = block.align;
|
|
14498
|
+
if (withStart && block.start !== void 0) out.start = block.start;
|
|
14499
|
+
return out;
|
|
14500
|
+
}
|
|
14443
14501
|
function isEmptyMarks(m) {
|
|
14444
14502
|
return !m || Object.keys(m).length === 0;
|
|
14445
14503
|
}
|
|
@@ -14520,7 +14578,7 @@ function marksAt(runs, offset) {
|
|
|
14520
14578
|
* blocks) with plain `insert`. Marks on untouched text are preserved; the
|
|
14521
14579
|
* inserted text inherits marks of the character before `start`.
|
|
14522
14580
|
*/
|
|
14523
|
-
function replaceRange(blocks, start, end, insert) {
|
|
14581
|
+
function replaceRange(blocks, start, end, insert, insertMarks) {
|
|
14524
14582
|
const total = blocksToText(blocks).length;
|
|
14525
14583
|
start = Math.max(0, Math.min(start, total));
|
|
14526
14584
|
end = Math.max(start, Math.min(end, total));
|
|
@@ -14529,6 +14587,7 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14529
14587
|
const prefixBlocks = [];
|
|
14530
14588
|
const suffixBlocks = [];
|
|
14531
14589
|
let inheritedMarks;
|
|
14590
|
+
let startBlock;
|
|
14532
14591
|
let pos = 0;
|
|
14533
14592
|
blocks.forEach((block) => {
|
|
14534
14593
|
const len = blockText(block).length;
|
|
@@ -14541,11 +14600,13 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14541
14600
|
if (start >= bStart && start <= bEnd) {
|
|
14542
14601
|
const local = start - bStart;
|
|
14543
14602
|
before = sliceRuns(block.runs, 0, local);
|
|
14603
|
+
startBlock = block;
|
|
14544
14604
|
inheritedMarks = marksAt(block.runs, local);
|
|
14545
14605
|
}
|
|
14546
14606
|
if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
|
|
14547
14607
|
}
|
|
14548
14608
|
});
|
|
14609
|
+
if (insertMarks !== void 0) inheritedMarks = insertMarks ?? void 0;
|
|
14549
14610
|
const lines = insert.split("\n");
|
|
14550
14611
|
const mk = (t) => t === "" ? [] : [{
|
|
14551
14612
|
t,
|
|
@@ -14556,7 +14617,10 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14556
14617
|
let runs = mk(line);
|
|
14557
14618
|
if (idx === 0) runs = [...before, ...runs];
|
|
14558
14619
|
if (idx === lines.length - 1) runs = [...runs, ...after];
|
|
14559
|
-
middle.push({
|
|
14620
|
+
middle.push({
|
|
14621
|
+
...blockFields(startBlock, idx === 0),
|
|
14622
|
+
runs: normalizeRuns(runs)
|
|
14623
|
+
});
|
|
14560
14624
|
});
|
|
14561
14625
|
return [
|
|
14562
14626
|
...prefixBlocks,
|
|
@@ -14564,8 +14628,331 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14564
14628
|
...suffixBlocks
|
|
14565
14629
|
];
|
|
14566
14630
|
}
|
|
14631
|
+
function safeBase(base) {
|
|
14632
|
+
return Number.isFinite(base) && base > 0 ? base : 1;
|
|
14633
|
+
}
|
|
14634
|
+
/** px -> stored ratio (4 decimals). */
|
|
14635
|
+
function sizeToRatio(px, base) {
|
|
14636
|
+
return Math.round(px / safeBase(base) * 1e4) / 1e4;
|
|
14637
|
+
}
|
|
14638
|
+
/** stored ratio -> px (rounded to 0.1). Invalid ratios resolve to the base. */
|
|
14639
|
+
function ratioToSize(ratio, base) {
|
|
14640
|
+
const b = safeBase(base);
|
|
14641
|
+
return Math.round((typeof ratio === "number" && ratio > 0 ? ratio : 1) * b * 10) / 10;
|
|
14642
|
+
}
|
|
14643
|
+
/** Effective ratio of a run (invalid or missing -> 1). */
|
|
14644
|
+
function runRatio(m) {
|
|
14645
|
+
const r = m?.size;
|
|
14646
|
+
return typeof r === "number" && Number.isFinite(r) && r > 0 ? r : 1;
|
|
14647
|
+
}
|
|
14648
|
+
function normalizeMarks(m) {
|
|
14649
|
+
const out = {};
|
|
14650
|
+
for (const [k, v] of Object.entries(m)) {
|
|
14651
|
+
if (v === void 0 || v === false || v === null || v === "") continue;
|
|
14652
|
+
if (k === "size" && v === 1) continue;
|
|
14653
|
+
out[k] = v;
|
|
14654
|
+
}
|
|
14655
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
14656
|
+
}
|
|
14657
|
+
/** Normalized marks of a run, or undefined when empty. */
|
|
14658
|
+
function cleanMarks(m) {
|
|
14659
|
+
return m ? normalizeMarks(m) : void 0;
|
|
14660
|
+
}
|
|
14661
|
+
function blockSpans(blocks) {
|
|
14662
|
+
const out = [];
|
|
14663
|
+
let pos = 0;
|
|
14664
|
+
blocks.forEach((b, index) => {
|
|
14665
|
+
const len = blockText(b).length;
|
|
14666
|
+
out.push({
|
|
14667
|
+
index,
|
|
14668
|
+
start: pos,
|
|
14669
|
+
end: pos + len
|
|
14670
|
+
});
|
|
14671
|
+
pos += len + 1;
|
|
14672
|
+
});
|
|
14673
|
+
return out;
|
|
14674
|
+
}
|
|
14675
|
+
/** Indices of blocks touched by [start, end] (collapsed range -> its block). */
|
|
14676
|
+
function blockIndicesInRange(blocks, start, end) {
|
|
14677
|
+
const out = [];
|
|
14678
|
+
for (const s of blockSpans(blocks)) if (s.end >= start && s.start <= end) {
|
|
14679
|
+
if (end > start && s.start === end && s.start !== start) continue;
|
|
14680
|
+
out.push(s.index);
|
|
14681
|
+
}
|
|
14682
|
+
return out;
|
|
14683
|
+
}
|
|
14684
|
+
/** Marks that typing at a collapsed offset would inherit. */
|
|
14685
|
+
function marksAtOffset(blocks, offset) {
|
|
14686
|
+
for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) return cleanMarks(marksAt(blocks[s.index].runs, offset - s.start));
|
|
14687
|
+
}
|
|
14688
|
+
function mapRunsInRange(block, from, to, fn) {
|
|
14689
|
+
const runs = [];
|
|
14690
|
+
let pos = 0;
|
|
14691
|
+
for (const run of block.runs) {
|
|
14692
|
+
const rs = pos;
|
|
14693
|
+
const re = pos + run.t.length;
|
|
14694
|
+
pos = re;
|
|
14695
|
+
const s = Math.max(from, rs);
|
|
14696
|
+
const e = Math.min(to, re);
|
|
14697
|
+
if (s >= e) {
|
|
14698
|
+
runs.push(run);
|
|
14699
|
+
continue;
|
|
14700
|
+
}
|
|
14701
|
+
if (s > rs) runs.push({
|
|
14702
|
+
t: run.t.slice(0, s - rs),
|
|
14703
|
+
m: run.m
|
|
14704
|
+
});
|
|
14705
|
+
runs.push({
|
|
14706
|
+
t: run.t.slice(s - rs, e - rs),
|
|
14707
|
+
m: fn(run.m)
|
|
14708
|
+
});
|
|
14709
|
+
if (e < re) runs.push({
|
|
14710
|
+
t: run.t.slice(e - rs),
|
|
14711
|
+
m: run.m
|
|
14712
|
+
});
|
|
14713
|
+
}
|
|
14714
|
+
return {
|
|
14715
|
+
...block,
|
|
14716
|
+
runs: normalizeRuns(runs)
|
|
14717
|
+
};
|
|
14718
|
+
}
|
|
14719
|
+
/** Set (or remove with `undefined`) one mark over the global range. */
|
|
14720
|
+
function applyMark(blocks, start, end, key, value) {
|
|
14721
|
+
if (start >= end) return blocks;
|
|
14722
|
+
if (key === "lineHeight" && value !== void 0) {
|
|
14723
|
+
const lh = runLineHeight({ lineHeight: value });
|
|
14724
|
+
if (lh === void 0) return blocks;
|
|
14725
|
+
value = lh;
|
|
14726
|
+
}
|
|
14727
|
+
const spans = blockSpans(blocks);
|
|
14728
|
+
return blocks.map((block, i) => {
|
|
14729
|
+
const s = spans[i];
|
|
14730
|
+
if (s.end <= start || s.start >= end) return block;
|
|
14731
|
+
return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
|
|
14732
|
+
...m,
|
|
14733
|
+
[key]: value
|
|
14734
|
+
}));
|
|
14735
|
+
});
|
|
14736
|
+
}
|
|
14737
|
+
/** Query one mark over a range; collapsed ranges report the caret marks. */
|
|
14738
|
+
function queryMark(blocks, start, end, key, stored) {
|
|
14739
|
+
const values = [];
|
|
14740
|
+
if (start >= end) {
|
|
14741
|
+
const v = (stored ?? marksAtOffset(blocks, start))?.[key];
|
|
14742
|
+
return v === void 0 || v === false ? { state: "none" } : {
|
|
14743
|
+
state: "all",
|
|
14744
|
+
value: v
|
|
14745
|
+
};
|
|
14746
|
+
}
|
|
14747
|
+
const spans = blockSpans(blocks);
|
|
14748
|
+
blocks.forEach((block, i) => {
|
|
14749
|
+
const s = spans[i];
|
|
14750
|
+
if (s.end <= start || s.start >= end) return;
|
|
14751
|
+
const from = Math.max(start, s.start) - s.start;
|
|
14752
|
+
const to = Math.min(end, s.end) - s.start;
|
|
14753
|
+
for (const r of sliceRuns(block.runs, from, to)) {
|
|
14754
|
+
const v = r.m?.[key];
|
|
14755
|
+
values.push(v === false ? void 0 : v);
|
|
14756
|
+
}
|
|
14757
|
+
});
|
|
14758
|
+
if (values.length === 0) return { state: "none" };
|
|
14759
|
+
const set = values.filter((v) => v !== void 0);
|
|
14760
|
+
if (set.length === 0) return { state: "none" };
|
|
14761
|
+
if (set.length < values.length) return { state: "some" };
|
|
14762
|
+
return values.every((v) => v === values[0]) ? {
|
|
14763
|
+
state: "all",
|
|
14764
|
+
value: values[0]
|
|
14765
|
+
} : { state: "some" };
|
|
14766
|
+
}
|
|
14767
|
+
/** Toggle a boolean mark: removes when fully set, sets otherwise. */
|
|
14768
|
+
function toggleMark(blocks, start, end, key) {
|
|
14769
|
+
return applyMark(blocks, start, end, key, queryMark(blocks, start, end, key).state === "all" ? void 0 : true);
|
|
14770
|
+
}
|
|
14771
|
+
function mapBlocks(blocks, indices, fn) {
|
|
14772
|
+
const set = new Set(indices);
|
|
14773
|
+
return blocks.map((b, i) => set.has(i) ? fn(b) : b);
|
|
14774
|
+
}
|
|
14775
|
+
function stripList(b) {
|
|
14776
|
+
const { kind: _k, depth: _d, start: _s, ...rest } = b;
|
|
14777
|
+
return rest;
|
|
14778
|
+
}
|
|
14779
|
+
/** Toggle list `kind` on the blocks of a range (all already that kind -> off). */
|
|
14780
|
+
function toggleList(blocks, start, end, kind) {
|
|
14781
|
+
const idx = blockIndicesInRange(blocks, start, end);
|
|
14782
|
+
const allSame = idx.every((i) => blocks[i].kind === kind);
|
|
14783
|
+
return mapBlocks(blocks, idx, (b) => allSame ? stripList(b) : {
|
|
14784
|
+
...b,
|
|
14785
|
+
kind
|
|
14786
|
+
});
|
|
14787
|
+
}
|
|
14788
|
+
/** Set (or clear with `undefined`) the alignment of the blocks in a range. */
|
|
14789
|
+
function setBlockAlign(blocks, start, end, align) {
|
|
14790
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
|
|
14791
|
+
if (align === void 0) {
|
|
14792
|
+
const { align: _a, ...rest } = b;
|
|
14793
|
+
return rest;
|
|
14794
|
+
}
|
|
14795
|
+
return {
|
|
14796
|
+
...b,
|
|
14797
|
+
align
|
|
14798
|
+
};
|
|
14799
|
+
});
|
|
14800
|
+
}
|
|
14801
|
+
/** Increase depth of list blocks in the range (non-list blocks untouched). */
|
|
14802
|
+
function indentBlocks(blocks, start, end) {
|
|
14803
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => b.kind ? {
|
|
14804
|
+
...b,
|
|
14805
|
+
depth: (b.depth ?? 0) + 1
|
|
14806
|
+
} : b);
|
|
14807
|
+
}
|
|
14808
|
+
/** Decrease depth; at depth 0 the list marker is removed. */
|
|
14809
|
+
function outdentBlocks(blocks, start, end) {
|
|
14810
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
|
|
14811
|
+
if (!b.kind) return b;
|
|
14812
|
+
const d = b.depth ?? 0;
|
|
14813
|
+
if (d <= 0) return stripList(b);
|
|
14814
|
+
const next = {
|
|
14815
|
+
...b,
|
|
14816
|
+
depth: d - 1
|
|
14817
|
+
};
|
|
14818
|
+
if (d - 1 === 0) delete next.depth;
|
|
14819
|
+
return next;
|
|
14820
|
+
});
|
|
14821
|
+
}
|
|
14822
|
+
const BULLETS = [
|
|
14823
|
+
"•",
|
|
14824
|
+
"◦",
|
|
14825
|
+
"▪"
|
|
14826
|
+
];
|
|
14827
|
+
function alpha(n) {
|
|
14828
|
+
let s = "";
|
|
14829
|
+
let v = n;
|
|
14830
|
+
while (v > 0) {
|
|
14831
|
+
v--;
|
|
14832
|
+
s = String.fromCharCode(97 + v % 26) + s;
|
|
14833
|
+
v = Math.floor(v / 26);
|
|
14834
|
+
}
|
|
14835
|
+
return s;
|
|
14836
|
+
}
|
|
14837
|
+
function roman(n) {
|
|
14838
|
+
const map = [
|
|
14839
|
+
[1e3, "m"],
|
|
14840
|
+
[900, "cm"],
|
|
14841
|
+
[500, "d"],
|
|
14842
|
+
[400, "cd"],
|
|
14843
|
+
[100, "c"],
|
|
14844
|
+
[90, "xc"],
|
|
14845
|
+
[50, "l"],
|
|
14846
|
+
[40, "xl"],
|
|
14847
|
+
[10, "x"],
|
|
14848
|
+
[9, "ix"],
|
|
14849
|
+
[5, "v"],
|
|
14850
|
+
[4, "iv"],
|
|
14851
|
+
[1, "i"]
|
|
14852
|
+
];
|
|
14853
|
+
let v = n;
|
|
14854
|
+
let s = "";
|
|
14855
|
+
for (const [k, r] of map) while (v >= k) {
|
|
14856
|
+
s += r;
|
|
14857
|
+
v -= k;
|
|
14858
|
+
}
|
|
14859
|
+
return s;
|
|
14860
|
+
}
|
|
14861
|
+
/**
|
|
14862
|
+
* Marker text per block (null for plain paragraphs). Ordered numbering is
|
|
14863
|
+
* continuous across consecutive same-depth items and restarts after a
|
|
14864
|
+
* non-list block, a shallower item or a kind change; `start` seeds a run.
|
|
14865
|
+
* Markers are layout-only: they never enter `text` or offsets.
|
|
14866
|
+
*/
|
|
14867
|
+
function listMarkers(blocks) {
|
|
14868
|
+
const stack = [];
|
|
14869
|
+
return blocks.map((b) => {
|
|
14870
|
+
if (!b.kind) {
|
|
14871
|
+
stack.length = 0;
|
|
14872
|
+
return null;
|
|
14873
|
+
}
|
|
14874
|
+
const d = Math.max(0, Math.floor(b.depth ?? 0));
|
|
14875
|
+
stack.length = Math.min(stack.length, d + 1);
|
|
14876
|
+
while (stack.length < d) stack.push({
|
|
14877
|
+
kind: b.kind,
|
|
14878
|
+
n: 0
|
|
14879
|
+
});
|
|
14880
|
+
const cur = stack[d];
|
|
14881
|
+
if (!cur || cur.kind !== b.kind) stack[d] = {
|
|
14882
|
+
kind: b.kind,
|
|
14883
|
+
n: b.kind === "ol" ? Math.max(1, b.start ?? 1) : 0
|
|
14884
|
+
};
|
|
14885
|
+
else if (b.kind === "ol") cur.n += 1;
|
|
14886
|
+
if (b.kind === "ul") return BULLETS[d % 3];
|
|
14887
|
+
const n = stack[d].n;
|
|
14888
|
+
const style = d % 3;
|
|
14889
|
+
return `${style === 0 ? n : style === 1 ? alpha(n) : roman(n)}.`;
|
|
14890
|
+
});
|
|
14891
|
+
}
|
|
14892
|
+
/**
|
|
14893
|
+
* Format of the range for toolbars. A collapsed range reports the stored
|
|
14894
|
+
* marks (when given) or the marks typing would inherit.
|
|
14895
|
+
*/
|
|
14896
|
+
function getSelectionFormat(blocks, start, end, baseFontSize, stored) {
|
|
14897
|
+
const q = (key) => queryMark(blocks, start, end, key, stored);
|
|
14898
|
+
const ratios = [];
|
|
14899
|
+
if (start >= end) ratios.push(runRatio(stored ?? marksAtOffset(blocks, start)));
|
|
14900
|
+
else {
|
|
14901
|
+
const spans = blockSpans(blocks);
|
|
14902
|
+
blocks.forEach((block, i) => {
|
|
14903
|
+
const s = spans[i];
|
|
14904
|
+
if (s.end <= start || s.start >= end) return;
|
|
14905
|
+
const from = Math.max(start, s.start) - s.start;
|
|
14906
|
+
const to = Math.min(end, s.end) - s.start;
|
|
14907
|
+
for (const r of sliceRuns(block.runs, from, to)) ratios.push(runRatio(r.m));
|
|
14908
|
+
});
|
|
14909
|
+
}
|
|
14910
|
+
let size;
|
|
14911
|
+
if (ratios.length === 0 || ratios.every((r) => r === ratios[0])) {
|
|
14912
|
+
const r = ratios[0] ?? 1;
|
|
14913
|
+
size = {
|
|
14914
|
+
state: r === 1 ? "none" : "all",
|
|
14915
|
+
value: ratioToSize(r, baseFontSize)
|
|
14916
|
+
};
|
|
14917
|
+
} else size = { state: "some" };
|
|
14918
|
+
const idx = blockIndicesInRange(blocks, start, end);
|
|
14919
|
+
const kinds = new Set(idx.map((i) => blocks[i].kind ?? "none"));
|
|
14920
|
+
let list = { kind: "none" };
|
|
14921
|
+
if (kinds.size === 1) {
|
|
14922
|
+
const kind = [...kinds][0];
|
|
14923
|
+
const depths = new Set(idx.map((i) => blocks[i].depth ?? 0));
|
|
14924
|
+
list = kind === "none" ? { kind } : {
|
|
14925
|
+
kind,
|
|
14926
|
+
...depths.size === 1 ? { depth: [...depths][0] } : {}
|
|
14927
|
+
};
|
|
14928
|
+
} else if (kinds.size > 1) list = { kind: "mixed" };
|
|
14929
|
+
return {
|
|
14930
|
+
bold: q("bold"),
|
|
14931
|
+
italic: q("italic"),
|
|
14932
|
+
underline: q("underline"),
|
|
14933
|
+
strike: q("strike"),
|
|
14934
|
+
color: q("color"),
|
|
14935
|
+
family: q("family"),
|
|
14936
|
+
link: q("link"),
|
|
14937
|
+
lineHeight: q("lineHeight"),
|
|
14938
|
+
align: alignQuery(blocks, start, end),
|
|
14939
|
+
size,
|
|
14940
|
+
list
|
|
14941
|
+
};
|
|
14942
|
+
}
|
|
14943
|
+
function alignQuery(blocks, start, end) {
|
|
14944
|
+
const vals = blockIndicesInRange(blocks, start, end).map((i) => blocks[i].align);
|
|
14945
|
+
const set = vals.filter((v) => v !== void 0);
|
|
14946
|
+
if (set.length === 0) return { state: "none" };
|
|
14947
|
+
if (set.length < vals.length || set.some((v) => v !== set[0])) return { state: "some" };
|
|
14948
|
+
return {
|
|
14949
|
+
state: "all",
|
|
14950
|
+
value: set[0]
|
|
14951
|
+
};
|
|
14952
|
+
}
|
|
14567
14953
|
//#endregion
|
|
14568
14954
|
//#region src/nodes/text-v2/layout/engine.ts
|
|
14955
|
+
const DEFAULT_INDENT_STEP = 1.5;
|
|
14569
14956
|
/** Quote multi-word families like `Konva` does so the font shorthand parses. */
|
|
14570
14957
|
function normalizeFontFamily(fontFamily) {
|
|
14571
14958
|
return fontFamily.split(",").map((family) => {
|
|
@@ -14578,6 +14965,50 @@ function normalizeFontFamily(fontFamily) {
|
|
|
14578
14965
|
function fontString(s) {
|
|
14579
14966
|
return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
|
|
14580
14967
|
}
|
|
14968
|
+
/** Style of a run: node style overridden by the run marks. */
|
|
14969
|
+
function runStyle(style, m) {
|
|
14970
|
+
if (!m) return style;
|
|
14971
|
+
let fontStyle = style.fontStyle;
|
|
14972
|
+
if (m.bold || m.italic) {
|
|
14973
|
+
const tokens = new Set(fontStyle.split(/\s+/).filter(Boolean));
|
|
14974
|
+
tokens.delete("normal");
|
|
14975
|
+
if (m.italic) tokens.add("italic");
|
|
14976
|
+
if (m.bold) tokens.add("bold");
|
|
14977
|
+
fontStyle = tokens.size === 0 ? "normal" : Array.from(tokens).join(" ");
|
|
14978
|
+
}
|
|
14979
|
+
return {
|
|
14980
|
+
...style,
|
|
14981
|
+
fontStyle,
|
|
14982
|
+
fontSize: style.fontSize * runRatio(m),
|
|
14983
|
+
fontFamily: typeof m.family === "string" && m.family ? m.family : style.fontFamily
|
|
14984
|
+
};
|
|
14985
|
+
}
|
|
14986
|
+
/** Pure fallback (same constants as Konva.Text when the browser has none). */
|
|
14987
|
+
const approxMetrics = (_font, size) => ({
|
|
14988
|
+
ascent: .91 * size,
|
|
14989
|
+
descent: .21 * size
|
|
14990
|
+
});
|
|
14991
|
+
const metricsCache = /* @__PURE__ */ new Map();
|
|
14992
|
+
/** Ascent/descent used for baseline placement (parity with Konva.Text). */
|
|
14993
|
+
function fontMetrics(font, size) {
|
|
14994
|
+
if (Konva.legacyTextRendering) return {
|
|
14995
|
+
ascent: 0,
|
|
14996
|
+
descent: 0
|
|
14997
|
+
};
|
|
14998
|
+
const hit = metricsCache.get(font);
|
|
14999
|
+
if (hit) return hit;
|
|
15000
|
+
const ctx = Konva.Util.createCanvasElement().getContext("2d");
|
|
15001
|
+
ctx.font = font;
|
|
15002
|
+
const m = ctx.measureText("M");
|
|
15003
|
+
const scale = size / 100;
|
|
15004
|
+
const out = {
|
|
15005
|
+
ascent: m.fontBoundingBoxAscent ?? 91 * scale,
|
|
15006
|
+
descent: m.fontBoundingBoxDescent ?? 21 * scale
|
|
15007
|
+
};
|
|
15008
|
+
if (metricsCache.size > 2e3) metricsCache.clear();
|
|
15009
|
+
metricsCache.set(font, out);
|
|
15010
|
+
return out;
|
|
15011
|
+
}
|
|
14581
15012
|
let sharedContext = null;
|
|
14582
15013
|
/** Default measurer: works on browser, node-canvas and skia-canvas backends. */
|
|
14583
15014
|
const canvasMeasurer = (font, text) => {
|
|
@@ -14625,11 +15056,15 @@ function wordWrapIndex(graphemes, start, matchLen, matchWidth, maxWidth) {
|
|
|
14625
15056
|
const seg = graphemes.slice(start, start + matchLen);
|
|
14626
15057
|
return Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
|
|
14627
15058
|
}
|
|
14628
|
-
function applyAlign(lines,
|
|
14629
|
-
if (align === "left") return;
|
|
15059
|
+
function applyAlign(lines, width, padding) {
|
|
14630
15060
|
for (const line of lines) {
|
|
14631
|
-
const
|
|
14632
|
-
|
|
15061
|
+
const align = line.align;
|
|
15062
|
+
if (align === "left") {
|
|
15063
|
+
line.offsetX = line.indent;
|
|
15064
|
+
continue;
|
|
15065
|
+
}
|
|
15066
|
+
const free = width - line.indent - line.width - padding * 2;
|
|
15067
|
+
line.offsetX = line.indent + (align === "center" ? free / 2 : free);
|
|
14633
15068
|
}
|
|
14634
15069
|
}
|
|
14635
15070
|
var TextLayoutEngine = class {
|
|
@@ -14639,8 +15074,10 @@ var TextLayoutEngine = class {
|
|
|
14639
15074
|
/** Entries by signature, so paragraphs that shifted index are reused. */
|
|
14640
15075
|
pool = /* @__PURE__ */ new Map();
|
|
14641
15076
|
lastParagraphsRelaid = 0;
|
|
14642
|
-
|
|
15077
|
+
metrics;
|
|
15078
|
+
constructor(measurer = canvasMeasurer, metrics = approxMetrics) {
|
|
14643
15079
|
this.measure = measurer;
|
|
15080
|
+
this.metrics = metrics;
|
|
14644
15081
|
}
|
|
14645
15082
|
/** Number of paragraphs recomputed by the last `layout()` call. */
|
|
14646
15083
|
getLastRelaidCount() {
|
|
@@ -14651,10 +15088,17 @@ var TextLayoutEngine = class {
|
|
|
14651
15088
|
this.paragraphs = [];
|
|
14652
15089
|
this.pool.clear();
|
|
14653
15090
|
}
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
15091
|
+
/** Text indent of a block in px (0 for plain paragraphs). */
|
|
15092
|
+
indentFor(block, style, fixedWidth, maxWidth) {
|
|
15093
|
+
if (!block.kind) return 0;
|
|
15094
|
+
const stepPx = (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
|
|
15095
|
+
const wanted = (Math.max(0, Math.floor(block.depth ?? 0)) + 1) * stepPx;
|
|
15096
|
+
if (!fixedWidth) return wanted;
|
|
15097
|
+
const limit = Math.max(0, maxWidth - 4 * style.fontSize);
|
|
15098
|
+
return Math.min(wanted, limit);
|
|
15099
|
+
}
|
|
15100
|
+
sigFor(block, style, fixedWidth, maxWidth, indent) {
|
|
15101
|
+
return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
|
|
14658
15102
|
}
|
|
14659
15103
|
/**
|
|
14660
15104
|
* Pre-compute paragraph layouts for `blocks` within a time budget so a
|
|
@@ -14662,14 +15106,14 @@ var TextLayoutEngine = class {
|
|
|
14662
15106
|
* Lets callers time-slice very large content (e.g. bulk paste).
|
|
14663
15107
|
*/
|
|
14664
15108
|
warm(blocks, style, bounds, budgetMs) {
|
|
14665
|
-
const
|
|
14666
|
-
const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
|
|
15109
|
+
const fixedWidth = isNum(bounds.width);
|
|
14667
15110
|
const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
|
|
14668
15111
|
const deadline = now() + budgetMs;
|
|
14669
15112
|
for (const block of blocks) {
|
|
14670
|
-
const
|
|
15113
|
+
const indent = this.indentFor(block, style, fixedWidth, maxWidth);
|
|
15114
|
+
const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
|
|
14671
15115
|
if (this.pool.has(sig)) continue;
|
|
14672
|
-
this.pool.set(sig, this.layoutParagraph(block, sig,
|
|
15116
|
+
this.pool.set(sig, this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent));
|
|
14673
15117
|
if (now() >= deadline) return false;
|
|
14674
15118
|
}
|
|
14675
15119
|
return true;
|
|
@@ -14687,35 +15131,38 @@ var TextLayoutEngine = class {
|
|
|
14687
15131
|
return w + text.length * spacing;
|
|
14688
15132
|
}
|
|
14689
15133
|
layout(blocks, style, bounds) {
|
|
14690
|
-
const font = fontString(style);
|
|
14691
15134
|
const padding = bounds.padding;
|
|
14692
15135
|
const fixedWidth = isNum(bounds.width);
|
|
14693
15136
|
const fixedHeight = isNum(bounds.height);
|
|
14694
15137
|
const maxWidth = (bounds.width ?? 0) - padding * 2;
|
|
14695
15138
|
const maxHeightPx = (bounds.height ?? 0) - padding * 2;
|
|
14696
15139
|
const lineHeightPx = style.lineHeight * style.fontSize;
|
|
14697
|
-
const
|
|
15140
|
+
const markers = listMarkers(blocks);
|
|
14698
15141
|
const prev = this.paragraphs;
|
|
14699
15142
|
const next = [];
|
|
14700
15143
|
let relaid = 0;
|
|
14701
15144
|
const lines = [];
|
|
14702
15145
|
let textWidth = 0;
|
|
14703
15146
|
let offset = 0;
|
|
15147
|
+
let contentHeight = 0;
|
|
14704
15148
|
for (let p = 0; p < blocks.length; p++) {
|
|
14705
15149
|
const block = blocks[p];
|
|
14706
|
-
const
|
|
15150
|
+
const indent = this.indentFor(block, style, fixedWidth, maxWidth);
|
|
15151
|
+
const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
|
|
14707
15152
|
let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
|
|
14708
15153
|
if (!entry) {
|
|
14709
|
-
entry = this.layoutParagraph(block, sig,
|
|
15154
|
+
entry = this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent);
|
|
14710
15155
|
relaid++;
|
|
14711
15156
|
}
|
|
14712
15157
|
next.push(entry);
|
|
14713
15158
|
const emitted = this.emitLines(entry, block, p, offset, lines, {
|
|
14714
|
-
font,
|
|
14715
15159
|
style,
|
|
14716
|
-
|
|
15160
|
+
indent,
|
|
15161
|
+
marker: markers[p],
|
|
15162
|
+
y: contentHeight,
|
|
14717
15163
|
maxHeightPx: fixedHeight ? maxHeightPx : void 0
|
|
14718
15164
|
});
|
|
15165
|
+
contentHeight = emitted.y;
|
|
14719
15166
|
textWidth = Math.max(textWidth, emitted.maxWidth);
|
|
14720
15167
|
if (emitted.stopped) break;
|
|
14721
15168
|
offset += blockText(block).length + 1;
|
|
@@ -14724,25 +15171,42 @@ var TextLayoutEngine = class {
|
|
|
14724
15171
|
this.pool = new Map(next.map((e) => [e.sig, e]));
|
|
14725
15172
|
this.lastParagraphsRelaid = relaid;
|
|
14726
15173
|
const width = fixedWidth ? bounds.width : textWidth + padding * 2;
|
|
14727
|
-
const height = fixedHeight ? bounds.height :
|
|
14728
|
-
applyAlign(lines,
|
|
15174
|
+
const height = fixedHeight ? bounds.height : contentHeight + padding * 2;
|
|
15175
|
+
applyAlign(lines, width, padding);
|
|
14729
15176
|
return {
|
|
14730
15177
|
lines,
|
|
14731
15178
|
textWidth,
|
|
14732
15179
|
textHeight: style.fontSize,
|
|
14733
15180
|
lineHeightPx,
|
|
15181
|
+
contentHeight,
|
|
14734
15182
|
width,
|
|
14735
15183
|
height
|
|
14736
15184
|
};
|
|
14737
15185
|
}
|
|
14738
15186
|
/** Append a paragraph's lines to `lines`; stops once a fixed height is full. */
|
|
14739
15187
|
emitLines(entry, block, paragraph, offset, lines, opts) {
|
|
14740
|
-
const {
|
|
15188
|
+
const { style, indent, maxHeightPx } = opts;
|
|
15189
|
+
let { y } = opts;
|
|
14741
15190
|
let maxWidth = 0;
|
|
15191
|
+
let first = true;
|
|
14742
15192
|
for (const pl of entry.lines) {
|
|
14743
15193
|
const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
|
|
14744
|
-
const segments = this.buildSegments(entry, block, pl,
|
|
15194
|
+
const segments = this.buildSegments(entry, block, pl, style);
|
|
14745
15195
|
const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
|
|
15196
|
+
let marker;
|
|
15197
|
+
if (first && opts.marker) {
|
|
15198
|
+
const m0 = block.runs[0]?.m;
|
|
15199
|
+
const rs = runStyle(style, m0);
|
|
15200
|
+
const stepPx = Math.min(indent, this.stepPx(style));
|
|
15201
|
+
marker = {
|
|
15202
|
+
text: opts.marker,
|
|
15203
|
+
x: Math.max(0, indent - stepPx),
|
|
15204
|
+
font: fontString(rs),
|
|
15205
|
+
fontSize: rs.fontSize,
|
|
15206
|
+
color: typeof m0?.color === "string" ? m0.color : void 0
|
|
15207
|
+
};
|
|
15208
|
+
}
|
|
15209
|
+
first = false;
|
|
14746
15210
|
lines.push({
|
|
14747
15211
|
paragraph,
|
|
14748
15212
|
start: offset + startChar,
|
|
@@ -14750,30 +15214,41 @@ var TextLayoutEngine = class {
|
|
|
14750
15214
|
text,
|
|
14751
15215
|
width: segments.reduce((a, s) => a + s.width, 0),
|
|
14752
15216
|
measuredWidth: pl.measuredWidth,
|
|
14753
|
-
offsetX:
|
|
14754
|
-
y
|
|
15217
|
+
offsetX: indent,
|
|
15218
|
+
y,
|
|
15219
|
+
height: pl.height,
|
|
15220
|
+
baseline: pl.baseline,
|
|
15221
|
+
indent,
|
|
15222
|
+
marker,
|
|
15223
|
+
align: block.align ?? style.align,
|
|
14755
15224
|
segments,
|
|
14756
15225
|
lastInParagraph: pl.lastInParagraph
|
|
14757
15226
|
});
|
|
14758
|
-
|
|
14759
|
-
|
|
15227
|
+
y += pl.height;
|
|
15228
|
+
maxWidth = Math.max(maxWidth, pl.measuredWidth + indent);
|
|
15229
|
+
if (maxHeightPx !== void 0 && y + pl.height > maxHeightPx) return {
|
|
14760
15230
|
stopped: true,
|
|
14761
|
-
maxWidth
|
|
15231
|
+
maxWidth,
|
|
15232
|
+
y
|
|
14762
15233
|
};
|
|
14763
15234
|
}
|
|
14764
15235
|
return {
|
|
14765
15236
|
stopped: false,
|
|
14766
|
-
maxWidth
|
|
15237
|
+
maxWidth,
|
|
15238
|
+
y
|
|
14767
15239
|
};
|
|
14768
15240
|
}
|
|
14769
|
-
|
|
15241
|
+
stepPx(style) {
|
|
15242
|
+
return (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
|
|
15243
|
+
}
|
|
15244
|
+
rangeWidth(entry, from, to, spacing) {
|
|
14770
15245
|
let width = 0;
|
|
14771
15246
|
let i = from;
|
|
14772
15247
|
while (i < to) {
|
|
14773
15248
|
const run = entry.runOf[i];
|
|
14774
15249
|
let j = i;
|
|
14775
15250
|
while (j < to && entry.runOf[j] === run) j++;
|
|
14776
|
-
width += this.measureChunk(
|
|
15251
|
+
width += this.measureChunk(entry.fonts[run], entry.graphemes.slice(i, j).join(""), spacing);
|
|
14777
15252
|
i = j;
|
|
14778
15253
|
}
|
|
14779
15254
|
return width;
|
|
@@ -14809,17 +15284,58 @@ var TextLayoutEngine = class {
|
|
|
14809
15284
|
matchWidth
|
|
14810
15285
|
};
|
|
14811
15286
|
}
|
|
14812
|
-
layoutParagraph(block, sig,
|
|
15287
|
+
layoutParagraph(block, sig, style, fixedWidth, maxWidth) {
|
|
15288
|
+
const entry = this.wrapParagraph(block, sig, style, fixedWidth, maxWidth);
|
|
15289
|
+
this.measureLines(entry, style);
|
|
15290
|
+
return entry;
|
|
15291
|
+
}
|
|
15292
|
+
/** Per-line box height and baseline from the runs on each line. */
|
|
15293
|
+
measureLines(entry, style) {
|
|
15294
|
+
const baseFont = fontString(style);
|
|
15295
|
+
for (const line of entry.lines) {
|
|
15296
|
+
let size = 0;
|
|
15297
|
+
let height = 0;
|
|
15298
|
+
let asc = 0;
|
|
15299
|
+
let desc = 0;
|
|
15300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15301
|
+
for (let g = line.startG; g < line.endG; g++) {
|
|
15302
|
+
const r = entry.runOf[g];
|
|
15303
|
+
if (seen.has(r)) continue;
|
|
15304
|
+
seen.add(r);
|
|
15305
|
+
size = Math.max(size, entry.sizes[r]);
|
|
15306
|
+
height = Math.max(height, entry.lineHeights[r] * entry.sizes[r]);
|
|
15307
|
+
const m = this.metrics(entry.fonts[r], entry.sizes[r]);
|
|
15308
|
+
asc = Math.max(asc, m.ascent);
|
|
15309
|
+
desc = Math.max(desc, m.descent);
|
|
15310
|
+
}
|
|
15311
|
+
if (seen.size === 0) {
|
|
15312
|
+
size = style.fontSize;
|
|
15313
|
+
height = style.lineHeight * size;
|
|
15314
|
+
const m = this.metrics(baseFont, style.fontSize);
|
|
15315
|
+
asc = m.ascent;
|
|
15316
|
+
desc = m.descent;
|
|
15317
|
+
}
|
|
15318
|
+
line.height = height;
|
|
15319
|
+
line.baseline = (asc - desc) / 2 + line.height / 2;
|
|
15320
|
+
}
|
|
15321
|
+
}
|
|
15322
|
+
wrapParagraph(block, sig, style, fixedWidth, maxWidth) {
|
|
14813
15323
|
const { graphemes, runOf } = flattenBlock(block);
|
|
15324
|
+
const baseFont = fontString(style);
|
|
15325
|
+
const fonts = block.runs.map((r) => r.m ? fontString(runStyle(style, r.m)) : baseFont);
|
|
15326
|
+
const sizes = block.runs.map((r) => style.fontSize * runRatio(r.m));
|
|
14814
15327
|
const entry = {
|
|
14815
15328
|
sig,
|
|
14816
15329
|
graphemes,
|
|
14817
15330
|
runOf,
|
|
15331
|
+
fonts,
|
|
15332
|
+
lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
|
|
15333
|
+
sizes,
|
|
14818
15334
|
lines: []
|
|
14819
15335
|
};
|
|
14820
15336
|
const n = graphemes.length;
|
|
14821
15337
|
const spacing = style.letterSpacing;
|
|
14822
|
-
const width = (a, b) => this.rangeWidth(entry, a, b,
|
|
15338
|
+
const width = (a, b) => this.rangeWidth(entry, a, b, spacing);
|
|
14823
15339
|
const shouldWrap = style.wrap !== "none";
|
|
14824
15340
|
const wrapAtWord = style.wrap !== "char" && shouldWrap;
|
|
14825
15341
|
const whole = width(0, n);
|
|
@@ -14828,7 +15344,9 @@ var TextLayoutEngine = class {
|
|
|
14828
15344
|
startG: 0,
|
|
14829
15345
|
endG: n,
|
|
14830
15346
|
measuredWidth: whole,
|
|
14831
|
-
lastInParagraph: true
|
|
15347
|
+
lastInParagraph: true,
|
|
15348
|
+
height: 0,
|
|
15349
|
+
baseline: 0
|
|
14832
15350
|
});
|
|
14833
15351
|
return entry;
|
|
14834
15352
|
}
|
|
@@ -14840,7 +15358,9 @@ var TextLayoutEngine = class {
|
|
|
14840
15358
|
startG: start,
|
|
14841
15359
|
endG: n,
|
|
14842
15360
|
measuredWidth: matchWidth,
|
|
14843
|
-
lastInParagraph: true
|
|
15361
|
+
lastInParagraph: true,
|
|
15362
|
+
height: 0,
|
|
15363
|
+
baseline: 0
|
|
14844
15364
|
});
|
|
14845
15365
|
return entry;
|
|
14846
15366
|
}
|
|
@@ -14856,7 +15376,9 @@ var TextLayoutEngine = class {
|
|
|
14856
15376
|
startG: start,
|
|
14857
15377
|
endG: trimTrailingSpace(graphemes, start, end),
|
|
14858
15378
|
measuredWidth: matchWidth,
|
|
14859
|
-
lastInParagraph: false
|
|
15379
|
+
lastInParagraph: false,
|
|
15380
|
+
height: 0,
|
|
15381
|
+
baseline: 0
|
|
14860
15382
|
});
|
|
14861
15383
|
start = skipSpace(graphemes, end);
|
|
14862
15384
|
}
|
|
@@ -14864,7 +15386,7 @@ var TextLayoutEngine = class {
|
|
|
14864
15386
|
if (last) last.lastInParagraph = true;
|
|
14865
15387
|
return entry;
|
|
14866
15388
|
}
|
|
14867
|
-
buildSegments(entry, block, pl,
|
|
15389
|
+
buildSegments(entry, block, pl, style) {
|
|
14868
15390
|
const segments = [];
|
|
14869
15391
|
let x = 0;
|
|
14870
15392
|
let i = pl.startG;
|
|
@@ -14873,12 +15395,16 @@ var TextLayoutEngine = class {
|
|
|
14873
15395
|
let j = i;
|
|
14874
15396
|
while (j < pl.endG && entry.runOf[j] === run) j++;
|
|
14875
15397
|
const t = entry.graphemes.slice(i, j).join("");
|
|
15398
|
+
const font = entry.fonts[run];
|
|
14876
15399
|
const width = this.measureChunk(font, t, style.letterSpacing);
|
|
14877
15400
|
segments.push({
|
|
14878
15401
|
t,
|
|
14879
15402
|
m: block.runs[run].m,
|
|
15403
|
+
run,
|
|
14880
15404
|
x,
|
|
14881
|
-
width
|
|
15405
|
+
width,
|
|
15406
|
+
font,
|
|
15407
|
+
fontSize: entry.sizes[run]
|
|
14882
15408
|
});
|
|
14883
15409
|
x += width;
|
|
14884
15410
|
i = j;
|
|
@@ -14910,12 +15436,18 @@ const LAYOUT_ATTRS = [
|
|
|
14910
15436
|
* code written against `Konva.Text` keeps working.
|
|
14911
15437
|
*/
|
|
14912
15438
|
var WeaveTextV2Shape = class extends Konva.Shape {
|
|
14913
|
-
engine = new TextLayoutEngine();
|
|
14914
|
-
naturalEngine = new TextLayoutEngine();
|
|
15439
|
+
engine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
|
|
15440
|
+
naturalEngine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
|
|
14915
15441
|
layoutCache = null;
|
|
14916
15442
|
blocksKey = {};
|
|
14917
15443
|
blocksCache = [];
|
|
14918
15444
|
viewport = null;
|
|
15445
|
+
indentStepValue;
|
|
15446
|
+
linkStyle = {
|
|
15447
|
+
color: "#1155ccff",
|
|
15448
|
+
hoverColor: "#3d7be0ff"
|
|
15449
|
+
};
|
|
15450
|
+
hoveredRun = null;
|
|
14919
15451
|
constructor(config) {
|
|
14920
15452
|
super(config);
|
|
14921
15453
|
this.className = "WeaveTextV2Shape";
|
|
@@ -14932,6 +15464,55 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14932
15464
|
this._setAttr("text", value == null ? "" : String(value));
|
|
14933
15465
|
return this;
|
|
14934
15466
|
}
|
|
15467
|
+
/**
|
|
15468
|
+
* List indent per depth, as a multiple of `fontSize`. Not a node attr: it
|
|
15469
|
+
* comes from the node config and must not be serialized into the document.
|
|
15470
|
+
*/
|
|
15471
|
+
setIndentStep(step) {
|
|
15472
|
+
if (this.indentStepValue !== step) {
|
|
15473
|
+
this.indentStepValue = step;
|
|
15474
|
+
this.layoutCache = null;
|
|
15475
|
+
}
|
|
15476
|
+
return this;
|
|
15477
|
+
}
|
|
15478
|
+
/** Link run colours (from the node config; not serialized). */
|
|
15479
|
+
setLinkStyle(style) {
|
|
15480
|
+
this.linkStyle = style;
|
|
15481
|
+
return this;
|
|
15482
|
+
}
|
|
15483
|
+
/** Run under the pointer (`paragraph:run`), repainted with the hover colour. */
|
|
15484
|
+
setHoveredRun(key) {
|
|
15485
|
+
if (this.hoveredRun !== key) {
|
|
15486
|
+
this.hoveredRun = key;
|
|
15487
|
+
this.getLayer()?.batchDraw();
|
|
15488
|
+
}
|
|
15489
|
+
return this;
|
|
15490
|
+
}
|
|
15491
|
+
getHoveredRun() {
|
|
15492
|
+
return this.hoveredRun;
|
|
15493
|
+
}
|
|
15494
|
+
/**
|
|
15495
|
+
* Link run at a point in node-local coordinates (the legacy node-level
|
|
15496
|
+
* `link` wins: runs are then not links).
|
|
15497
|
+
*/
|
|
15498
|
+
linkAtPoint(point) {
|
|
15499
|
+
if (this.attrs.link) return null;
|
|
15500
|
+
const layout = this.ensureLayout();
|
|
15501
|
+
const o = this.getContentOffset();
|
|
15502
|
+
const x = point.x - o.x;
|
|
15503
|
+
const y = point.y - o.y;
|
|
15504
|
+
for (const line of layout.lines) {
|
|
15505
|
+
if (y < line.y || y >= line.y + line.height) continue;
|
|
15506
|
+
for (const seg of line.segments) {
|
|
15507
|
+
const sx = line.offsetX + seg.x;
|
|
15508
|
+
if (x >= sx && x <= sx + seg.width && isSafeLink(seg.m?.link)) return {
|
|
15509
|
+
key: `${line.paragraph}:${seg.run}`,
|
|
15510
|
+
url: seg.m.link
|
|
15511
|
+
};
|
|
15512
|
+
}
|
|
15513
|
+
}
|
|
15514
|
+
return null;
|
|
15515
|
+
}
|
|
14935
15516
|
/** Resolved blocks: stored or lazily synthesized (never written back). */
|
|
14936
15517
|
getBlocks() {
|
|
14937
15518
|
const k = this.blocksKey;
|
|
@@ -14952,6 +15533,10 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14952
15533
|
}
|
|
14953
15534
|
/** Replace content with blocks, keeping the `text` mirror in sync. */
|
|
14954
15535
|
setBlocks(blocks) {
|
|
15536
|
+
if (!Array.isArray(blocks)) {
|
|
15537
|
+
this._setAttr("blocks", void 0);
|
|
15538
|
+
return this;
|
|
15539
|
+
}
|
|
14955
15540
|
this._setAttr("blocks", blocks);
|
|
14956
15541
|
this._setAttr("text", blocksToText(blocks));
|
|
14957
15542
|
return this;
|
|
@@ -14989,6 +15574,16 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14989
15574
|
textDecoration(v) {
|
|
14990
15575
|
return this.accessor("textDecoration", "", v);
|
|
14991
15576
|
}
|
|
15577
|
+
/**
|
|
15578
|
+
* Always stroke under the node transform. Weave turns `strokeScaleEnabled`
|
|
15579
|
+
* off while a node is transformed; Konva then resets the canvas transform
|
|
15580
|
+
* before stroking, which only works for path shapes. `strokeText` is not a
|
|
15581
|
+
* path, so the outline was painted at local coordinates on the canvas
|
|
15582
|
+
* origin at stage scale 1 (visible over other nodes while resizing).
|
|
15583
|
+
*/
|
|
15584
|
+
getStrokeScaleEnabled() {
|
|
15585
|
+
return true;
|
|
15586
|
+
}
|
|
14992
15587
|
accessor(name, fallback, value) {
|
|
14993
15588
|
if (value === void 0) return this.attrs[name] ?? fallback;
|
|
14994
15589
|
this.setAttr(name, value);
|
|
@@ -15023,7 +15618,8 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15023
15618
|
lineHeight: this.lineHeight(),
|
|
15024
15619
|
letterSpacing: this.letterSpacing(),
|
|
15025
15620
|
wrap: this.wrap(),
|
|
15026
|
-
align: this.align()
|
|
15621
|
+
align: this.align(),
|
|
15622
|
+
indentStep: this.indentStepValue
|
|
15027
15623
|
};
|
|
15028
15624
|
}
|
|
15029
15625
|
/** Lazy, cached layout. Size accessors and painting force it. */
|
|
@@ -15053,14 +15649,13 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15053
15649
|
};
|
|
15054
15650
|
}
|
|
15055
15651
|
/** Rendered width of a string with this shape's font and letter spacing. */
|
|
15056
|
-
measureText(text) {
|
|
15057
|
-
return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
|
|
15652
|
+
measureText(text, font) {
|
|
15653
|
+
return canvasMeasurer(font ?? fontString(this.style()), text) + text.length * this.letterSpacing();
|
|
15058
15654
|
}
|
|
15059
15655
|
/** Offset of the first line inside the box (padding + vertical align). */
|
|
15060
15656
|
getContentOffset() {
|
|
15061
15657
|
const padding = this.padding();
|
|
15062
|
-
const
|
|
15063
|
-
const used = layout.lines.length * layout.lineHeightPx + padding * 2;
|
|
15658
|
+
const used = this.ensureLayout().contentHeight + padding * 2;
|
|
15064
15659
|
const va = this.verticalAlign();
|
|
15065
15660
|
let alignY = 0;
|
|
15066
15661
|
if (va === "middle") alignY = (this.getHeight() - used) / 2;
|
|
@@ -15083,34 +15678,40 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15083
15678
|
setViewport(range) {
|
|
15084
15679
|
this.viewport = range;
|
|
15085
15680
|
}
|
|
15086
|
-
|
|
15681
|
+
/**
|
|
15682
|
+
* Visible y-range in local coordinates, derived from the context being
|
|
15683
|
+
* painted (current transform + backing canvas size) instead of the stage.
|
|
15684
|
+
* The stage is not the paint target during export (off-screen canvases,
|
|
15685
|
+
* tiles, server-side), so using it culled visible lines. Returns null (paint
|
|
15686
|
+
* everything) when the transform cannot be read.
|
|
15687
|
+
*/
|
|
15688
|
+
visibleRange(ctx) {
|
|
15087
15689
|
if (this.viewport) return this.viewport;
|
|
15088
|
-
const
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
const
|
|
15092
|
-
|
|
15093
|
-
const
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
];
|
|
15690
|
+
const native = ctx._context;
|
|
15691
|
+
const m = native.getTransform?.() ?? native.currentTransform;
|
|
15692
|
+
if (!m) return null;
|
|
15693
|
+
const det = m.a * m.d - m.b * m.c;
|
|
15694
|
+
if (!det || !Number.isFinite(det)) return null;
|
|
15695
|
+
const canvas = ctx.getCanvas();
|
|
15696
|
+
const backing = canvas._canvas;
|
|
15697
|
+
const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
|
|
15698
|
+
const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
|
|
15699
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
15700
|
+
let y1 = Infinity;
|
|
15701
|
+
let y2 = -Infinity;
|
|
15702
|
+
for (const [px, py] of [
|
|
15703
|
+
[0, 0],
|
|
15704
|
+
[w, 0],
|
|
15705
|
+
[0, h],
|
|
15706
|
+
[w, h]
|
|
15707
|
+
]) {
|
|
15708
|
+
const y = (m.a * (py - m.f) - m.b * (px - m.e)) / det;
|
|
15709
|
+
if (y < y1) y1 = y;
|
|
15710
|
+
if (y > y2) y2 = y;
|
|
15711
|
+
}
|
|
15111
15712
|
return {
|
|
15112
|
-
y1
|
|
15113
|
-
y2
|
|
15713
|
+
y1,
|
|
15714
|
+
y2
|
|
15114
15715
|
};
|
|
15115
15716
|
}
|
|
15116
15717
|
_sceneFunc = (ctx, shape) => {
|
|
@@ -15118,30 +15719,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15118
15719
|
const layout = self.ensureLayout();
|
|
15119
15720
|
if (layout.lines.length === 0) return;
|
|
15120
15721
|
const padding = self.padding();
|
|
15121
|
-
const lineHeightPx = layout.lineHeightPx;
|
|
15122
15722
|
let baseline = "middle";
|
|
15123
|
-
|
|
15124
|
-
if (!Konva.legacyTextRendering) {
|
|
15125
|
-
const m = self.measureMetrics();
|
|
15126
|
-
baseline = "alphabetic";
|
|
15127
|
-
translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
|
|
15128
|
-
}
|
|
15723
|
+
if (!Konva.legacyTextRendering) baseline = "alphabetic";
|
|
15129
15724
|
ctx.setAttr("font", fontString(self.style()));
|
|
15130
15725
|
ctx.setAttr("textBaseline", baseline);
|
|
15131
15726
|
ctx.setAttr("textAlign", "left");
|
|
15132
15727
|
const alignY = self.verticalOffset(layout);
|
|
15728
|
+
const range = self.visibleRange(ctx);
|
|
15133
15729
|
ctx.translate(padding, alignY + padding);
|
|
15134
|
-
const
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
const lineTop = top + n * lineHeightPx;
|
|
15138
|
-
if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
|
|
15139
|
-
self.paintLine(ctx, layout.lines[n], translateY + n * lineHeightPx);
|
|
15730
|
+
for (const line of layout.lines) {
|
|
15731
|
+
if (range && (alignY + padding + line.y + line.height < range.y1 - line.height || alignY + padding + line.y > range.y2 + line.height)) continue;
|
|
15732
|
+
self.paintLine(ctx, line, line.y + line.baseline);
|
|
15140
15733
|
}
|
|
15141
15734
|
};
|
|
15142
15735
|
/** Vertical offset of the text block inside the box (verticalAlign). */
|
|
15143
15736
|
verticalOffset(layout) {
|
|
15144
|
-
const used = layout.
|
|
15737
|
+
const used = layout.contentHeight + this.padding() * 2;
|
|
15145
15738
|
const va = this.verticalAlign();
|
|
15146
15739
|
if (va === "middle") return (this.getHeight() - used) / 2;
|
|
15147
15740
|
if (va === "bottom") return this.getHeight() - used;
|
|
@@ -15152,7 +15745,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15152
15745
|
const fontSize = this.fontSize();
|
|
15153
15746
|
ctx.save();
|
|
15154
15747
|
if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
|
|
15155
|
-
|
|
15748
|
+
if (line.marker) {
|
|
15749
|
+
const mk = line.marker;
|
|
15750
|
+
ctx.setAttr("font", mk.font);
|
|
15751
|
+
this.fillText(ctx, mk.text, mk.x, y, mk.color);
|
|
15752
|
+
}
|
|
15753
|
+
for (const seg of line.segments) {
|
|
15754
|
+
ctx.setAttr("font", seg.font);
|
|
15755
|
+
const x = line.offsetX + seg.x;
|
|
15756
|
+
const isLink = !this.attrs.link && isSafeLink(seg.m?.link);
|
|
15757
|
+
let color = typeof seg.m?.color === "string" ? seg.m.color : void 0;
|
|
15758
|
+
if (isLink) color = this.hoveredRun === `${line.paragraph}:${seg.run}` ? this.linkStyle.hoverColor : this.linkStyle.color;
|
|
15759
|
+
this.paintSegment(ctx, seg, x, y, color);
|
|
15760
|
+
if (seg.m?.underline || isLink) this.strokeSegment(ctx, seg, x, y + Math.round(seg.fontSize / 4), color);
|
|
15761
|
+
if (seg.m?.strike) this.strokeSegment(ctx, seg, x, y - Math.round(seg.fontSize / 4), color);
|
|
15762
|
+
}
|
|
15156
15763
|
if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
|
|
15157
15764
|
ctx.restore();
|
|
15158
15765
|
}
|
|
@@ -15166,24 +15773,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15166
15773
|
ctx.stroke();
|
|
15167
15774
|
ctx.restore();
|
|
15168
15775
|
}
|
|
15169
|
-
|
|
15776
|
+
strokeSegment(ctx, seg, x, y, color) {
|
|
15777
|
+
ctx.save();
|
|
15778
|
+
ctx.beginPath();
|
|
15779
|
+
ctx.moveTo(x, y);
|
|
15780
|
+
ctx.lineTo(x + Math.round(seg.width), y);
|
|
15781
|
+
ctx.lineWidth = seg.fontSize / 15;
|
|
15782
|
+
ctx.strokeStyle = color ?? this.fill();
|
|
15783
|
+
ctx.stroke();
|
|
15784
|
+
ctx.restore();
|
|
15785
|
+
}
|
|
15786
|
+
paintSegment(ctx, seg, x, y, color) {
|
|
15170
15787
|
const spacing = this.letterSpacing();
|
|
15171
15788
|
if (spacing === 0) {
|
|
15172
|
-
this.fillText(ctx,
|
|
15789
|
+
this.fillText(ctx, seg.t, x, y, color);
|
|
15173
15790
|
return;
|
|
15174
15791
|
}
|
|
15175
15792
|
let cx = x;
|
|
15176
|
-
for (const ch of Array.from(
|
|
15177
|
-
this.fillText(ctx, ch, cx, y);
|
|
15178
|
-
cx +=
|
|
15793
|
+
for (const ch of Array.from(seg.t)) {
|
|
15794
|
+
this.fillText(ctx, ch, cx, y, color);
|
|
15795
|
+
cx += canvasMeasurer(seg.font, ch) + spacing;
|
|
15179
15796
|
}
|
|
15180
15797
|
}
|
|
15181
|
-
fillText(ctx, text, x, y) {
|
|
15798
|
+
fillText(ctx, text, x, y, color) {
|
|
15182
15799
|
const self = this;
|
|
15183
15800
|
self._partialText = text;
|
|
15184
15801
|
self._partialTextX = x;
|
|
15185
15802
|
self._partialTextY = y;
|
|
15186
|
-
|
|
15803
|
+
if (color === void 0) {
|
|
15804
|
+
ctx.fillStrokeShape(this);
|
|
15805
|
+
return;
|
|
15806
|
+
}
|
|
15807
|
+
const prev = this.attrs.fill;
|
|
15808
|
+
this.attrs.fill = color;
|
|
15809
|
+
try {
|
|
15810
|
+
ctx.fillStrokeShape(this);
|
|
15811
|
+
} finally {
|
|
15812
|
+
this.attrs.fill = prev;
|
|
15813
|
+
}
|
|
15187
15814
|
}
|
|
15188
15815
|
_hitFunc = (ctx, shape) => {
|
|
15189
15816
|
ctx.beginPath();
|
|
@@ -15299,9 +15926,27 @@ function lineIndexAt(layout, offset) {
|
|
|
15299
15926
|
}
|
|
15300
15927
|
return fallback;
|
|
15301
15928
|
}
|
|
15929
|
+
/** Width of the first `k` UTF-16 units of a line, run by run. */
|
|
15930
|
+
function prefixWidth(line, k, measure) {
|
|
15931
|
+
if (k <= 0) return 0;
|
|
15932
|
+
if (line.segments.length === 0) return measure(line.text.slice(0, k));
|
|
15933
|
+
let width = 0;
|
|
15934
|
+
let pos = 0;
|
|
15935
|
+
for (const seg of line.segments) {
|
|
15936
|
+
const end = pos + seg.t.length;
|
|
15937
|
+
if (k >= end) width += seg.width;
|
|
15938
|
+
else {
|
|
15939
|
+
width += measure(seg.t.slice(0, k - pos), seg.font);
|
|
15940
|
+
break;
|
|
15941
|
+
}
|
|
15942
|
+
pos = end;
|
|
15943
|
+
if (k === end) break;
|
|
15944
|
+
}
|
|
15945
|
+
return width;
|
|
15946
|
+
}
|
|
15302
15947
|
function lineX(line, offset, measure) {
|
|
15303
15948
|
const k = Math.max(0, Math.min(offset - line.start, line.text.length));
|
|
15304
|
-
return line.offsetX + (
|
|
15949
|
+
return line.offsetX + prefixWidth(line, k, measure);
|
|
15305
15950
|
}
|
|
15306
15951
|
/** Caret rectangle in text-local coordinates (before padding/valign offset). */
|
|
15307
15952
|
function caretRect(layout, offset, measure) {
|
|
@@ -15315,16 +15960,22 @@ function caretRect(layout, offset, measure) {
|
|
|
15315
15960
|
};
|
|
15316
15961
|
return {
|
|
15317
15962
|
x: lineX(line, offset, measure),
|
|
15318
|
-
y:
|
|
15319
|
-
height:
|
|
15963
|
+
y: line.y,
|
|
15964
|
+
height: line.height,
|
|
15320
15965
|
line: i
|
|
15321
15966
|
};
|
|
15322
15967
|
}
|
|
15968
|
+
/** Index of the line containing a local y (clamped). */
|
|
15969
|
+
function lineIndexAtY(layout, y) {
|
|
15970
|
+
const lines = layout.lines;
|
|
15971
|
+
for (let i = 0; i < lines.length; i++) if (y < lines[i].y + lines[i].height) return i;
|
|
15972
|
+
return Math.max(0, lines.length - 1);
|
|
15973
|
+
}
|
|
15323
15974
|
/** Offset closest to a point in text-local coordinates. */
|
|
15324
15975
|
function offsetFromPoint(layout, point, measure) {
|
|
15325
15976
|
const lines = layout.lines;
|
|
15326
15977
|
if (lines.length === 0) return 0;
|
|
15327
|
-
return offsetInLine(lines[
|
|
15978
|
+
return offsetInLine(lines[lineIndexAtY(layout, point.y)], point.x, measure);
|
|
15328
15979
|
}
|
|
15329
15980
|
function offsetInLine(line, x, measure) {
|
|
15330
15981
|
const rel = x - line.offsetX;
|
|
@@ -15332,7 +15983,7 @@ function offsetInLine(line, x, measure) {
|
|
|
15332
15983
|
const b = boundaries(line.text, graphemeSeg);
|
|
15333
15984
|
let prevW = 0;
|
|
15334
15985
|
for (let k = 1; k < b.length; k++) {
|
|
15335
|
-
const w =
|
|
15986
|
+
const w = prefixWidth(line, b[k], measure);
|
|
15336
15987
|
if (rel < (prevW + w) / 2) return line.start + b[k - 1];
|
|
15337
15988
|
prevW = w;
|
|
15338
15989
|
}
|
|
@@ -15356,7 +16007,7 @@ function selectionRects(layout, from, to, measure) {
|
|
|
15356
16007
|
if (from > to) [from, to] = [to, from];
|
|
15357
16008
|
if (from === to) return [];
|
|
15358
16009
|
const out = [];
|
|
15359
|
-
layout.lines.forEach((line
|
|
16010
|
+
layout.lines.forEach((line) => {
|
|
15360
16011
|
const hasNewline = line.lastInParagraph;
|
|
15361
16012
|
const lineFrom = Math.max(from, line.start);
|
|
15362
16013
|
const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
|
|
@@ -15367,9 +16018,9 @@ function selectionRects(layout, from, to, measure) {
|
|
|
15367
16018
|
if (x2 <= x1) return;
|
|
15368
16019
|
out.push({
|
|
15369
16020
|
x: x1,
|
|
15370
|
-
y:
|
|
16021
|
+
y: line.y,
|
|
15371
16022
|
width: x2 - x1,
|
|
15372
|
-
height:
|
|
16023
|
+
height: line.height
|
|
15373
16024
|
});
|
|
15374
16025
|
});
|
|
15375
16026
|
return out;
|
|
@@ -15388,6 +16039,11 @@ var TextEditModel = class {
|
|
|
15388
16039
|
focus = 0;
|
|
15389
16040
|
/** x kept while moving vertically; reset by any other move. */
|
|
15390
16041
|
desiredX = null;
|
|
16042
|
+
/**
|
|
16043
|
+
* Marks the next typed text takes at a collapsed caret (e.g. bold toggled
|
|
16044
|
+
* with nothing selected). Cleared when the selection moves.
|
|
16045
|
+
*/
|
|
16046
|
+
storedMarks = null;
|
|
15391
16047
|
undoStack = [];
|
|
15392
16048
|
redoStack = [];
|
|
15393
16049
|
lastKind = null;
|
|
@@ -15424,12 +16080,14 @@ var TextEditModel = class {
|
|
|
15424
16080
|
const len = this.getText().length;
|
|
15425
16081
|
this.anchor = Math.max(0, Math.min(anchor, len));
|
|
15426
16082
|
this.focus = Math.max(0, Math.min(focus, len));
|
|
16083
|
+
this.storedMarks = null;
|
|
15427
16084
|
this.breakCoalescing();
|
|
15428
16085
|
}
|
|
15429
16086
|
moveTo(offset, extend = false, keepDesiredX = false) {
|
|
15430
16087
|
const len = this.getText().length;
|
|
15431
16088
|
const o = Math.max(0, Math.min(offset, len));
|
|
15432
16089
|
this.focus = o;
|
|
16090
|
+
this.storedMarks = null;
|
|
15433
16091
|
if (!extend) this.anchor = o;
|
|
15434
16092
|
if (!keepDesiredX) this.desiredX = null;
|
|
15435
16093
|
this.breakCoalescing();
|
|
@@ -15437,6 +16095,7 @@ var TextEditModel = class {
|
|
|
15437
16095
|
selectAll() {
|
|
15438
16096
|
this.anchor = 0;
|
|
15439
16097
|
this.focus = this.getText().length;
|
|
16098
|
+
this.storedMarks = null;
|
|
15440
16099
|
this.breakCoalescing();
|
|
15441
16100
|
}
|
|
15442
16101
|
breakCoalescing() {
|
|
@@ -15472,7 +16131,14 @@ var TextEditModel = class {
|
|
|
15472
16131
|
end = Math.max(start, Math.min(end, len));
|
|
15473
16132
|
if (start === end && text === "") return false;
|
|
15474
16133
|
this.record(kind, start);
|
|
15475
|
-
this.
|
|
16134
|
+
const carried = this.storedMarks ?? marksAtOffset(this.blocks, start);
|
|
16135
|
+
this.blocks = replaceRange(this.blocks, start, end, text, this.storedMarks ?? void 0);
|
|
16136
|
+
this.storedMarks = null;
|
|
16137
|
+
if (text.endsWith("\n") && carried) {
|
|
16138
|
+
const { link: _link, ...rest } = carried;
|
|
16139
|
+
const next = cleanMarks(rest);
|
|
16140
|
+
if (next && !marksAtOffset(this.blocks, start + text.length)) this.storedMarks = next;
|
|
16141
|
+
}
|
|
15476
16142
|
this.anchor = this.focus = start + text.length;
|
|
15477
16143
|
this.lastEditEnd = this.focus;
|
|
15478
16144
|
this.desiredX = null;
|
|
@@ -15512,14 +16178,102 @@ var TextEditModel = class {
|
|
|
15512
16178
|
this.restore(next);
|
|
15513
16179
|
return true;
|
|
15514
16180
|
}
|
|
16181
|
+
/** Current marks at the caret / selection start (stored marks win). */
|
|
16182
|
+
caretMarks() {
|
|
16183
|
+
return this.storedMarks ?? marksAtOffset(this.blocks, this.getSelection().start);
|
|
16184
|
+
}
|
|
16185
|
+
/** Always one new undo entry, never coalesced. */
|
|
16186
|
+
commitFormat(next) {
|
|
16187
|
+
if (JSON.stringify(next) === JSON.stringify(this.blocks)) return false;
|
|
16188
|
+
this.lastKind = null;
|
|
16189
|
+
this.record("format", -1);
|
|
16190
|
+
this.lastKind = null;
|
|
16191
|
+
this.blocks = next;
|
|
16192
|
+
return true;
|
|
16193
|
+
}
|
|
16194
|
+
/** Set (`undefined` clears) one mark on the selection or the stored marks. */
|
|
16195
|
+
setMark(key, value) {
|
|
16196
|
+
const { start, end } = this.getSelection();
|
|
16197
|
+
if (start === end) {
|
|
16198
|
+
const base = { ...this.caretMarks() ?? {} };
|
|
16199
|
+
if (value === void 0 || value === false) delete base[key];
|
|
16200
|
+
else base[key] = value;
|
|
16201
|
+
this.storedMarks = cleanMarks(base) ?? {};
|
|
16202
|
+
return true;
|
|
16203
|
+
}
|
|
16204
|
+
return this.commitFormat(applyMark(this.blocks, start, end, key, value));
|
|
16205
|
+
}
|
|
16206
|
+
/** Toggle a boolean mark: on unless the whole selection already has it. */
|
|
16207
|
+
toggleMark(key) {
|
|
16208
|
+
const { start, end } = this.getSelection();
|
|
16209
|
+
const q = queryMark(this.blocks, start, end, key, this.storedMarks);
|
|
16210
|
+
return this.setMark(key, q.state === "all" ? void 0 : true);
|
|
16211
|
+
}
|
|
16212
|
+
toggleList(kind) {
|
|
16213
|
+
const { start, end } = this.getSelection();
|
|
16214
|
+
return this.commitFormat(toggleList(this.blocks, start, end, kind));
|
|
16215
|
+
}
|
|
16216
|
+
setAlign(align) {
|
|
16217
|
+
const { start, end } = this.getSelection();
|
|
16218
|
+
return this.commitFormat(setBlockAlign(this.blocks, start, end, align));
|
|
16219
|
+
}
|
|
16220
|
+
indent() {
|
|
16221
|
+
const { start, end } = this.getSelection();
|
|
16222
|
+
return this.commitFormat(indentBlocks(this.blocks, start, end));
|
|
16223
|
+
}
|
|
16224
|
+
outdent() {
|
|
16225
|
+
const { start, end } = this.getSelection();
|
|
16226
|
+
return this.commitFormat(outdentBlocks(this.blocks, start, end));
|
|
16227
|
+
}
|
|
16228
|
+
blockAt(offset) {
|
|
16229
|
+
let pos = 0;
|
|
16230
|
+
for (let i = 0; i < this.blocks.length; i++) {
|
|
16231
|
+
const len = blockText(this.blocks[i]).length;
|
|
16232
|
+
if (offset <= pos + len) return {
|
|
16233
|
+
index: i,
|
|
16234
|
+
start: pos,
|
|
16235
|
+
length: len
|
|
16236
|
+
};
|
|
16237
|
+
pos += len + 1;
|
|
16238
|
+
}
|
|
16239
|
+
}
|
|
16240
|
+
/**
|
|
16241
|
+
* Enter in a list: an empty item exits the list; otherwise the item
|
|
16242
|
+
* continues (the block split carries `kind` and `depth`).
|
|
16243
|
+
*/
|
|
16244
|
+
enter() {
|
|
16245
|
+
const { start, end } = this.getSelection();
|
|
16246
|
+
const b = start === end ? this.blockAt(start) : void 0;
|
|
16247
|
+
if (b && this.blocks[b.index].kind && b.length === 0) return this.commitFormat(outdentToPlain(this.blocks, b.index));
|
|
16248
|
+
return this.replaceRange(start, end, "\n", "other");
|
|
16249
|
+
}
|
|
16250
|
+
/**
|
|
16251
|
+
* Backspace at the start of a list item outdents it, or removes the
|
|
16252
|
+
* marker at depth 0. Returns false when the key is not handled here.
|
|
16253
|
+
*/
|
|
16254
|
+
backspaceInList() {
|
|
16255
|
+
const { start, end } = this.getSelection();
|
|
16256
|
+
if (start !== end) return false;
|
|
16257
|
+
const b = this.blockAt(start);
|
|
16258
|
+
if (!b || b.start !== start || !this.blocks[b.index].kind) return false;
|
|
16259
|
+
return this.commitFormat(outdentBlocks(this.blocks, start, start));
|
|
16260
|
+
}
|
|
15515
16261
|
restore(s) {
|
|
15516
16262
|
this.blocks = s.blocks;
|
|
16263
|
+
this.storedMarks = null;
|
|
15517
16264
|
this.anchor = s.anchor;
|
|
15518
16265
|
this.focus = s.focus;
|
|
15519
16266
|
this.lastKind = null;
|
|
15520
16267
|
this.desiredX = null;
|
|
15521
16268
|
}
|
|
15522
16269
|
};
|
|
16270
|
+
function outdentToPlain(blocks, index) {
|
|
16271
|
+
return blocks.map((b, i) => {
|
|
16272
|
+
if (i !== index) return b;
|
|
16273
|
+
const { kind: _k, depth: _d, start: _s, ...rest } = b;
|
|
16274
|
+
return rest;
|
|
16275
|
+
});
|
|
16276
|
+
}
|
|
15523
16277
|
const SLICE_BUDGET_MS = 8;
|
|
15524
16278
|
const BLINK_MS = 530;
|
|
15525
16279
|
var TextEditSession = class {
|
|
@@ -15577,7 +16331,7 @@ var TextEditSession = class {
|
|
|
15577
16331
|
};
|
|
15578
16332
|
if (this.host.selectAllOnStart) this.model.selectAll();
|
|
15579
16333
|
this.syncTextarea();
|
|
15580
|
-
ta.focus();
|
|
16334
|
+
ta.focus({ preventScroll: true });
|
|
15581
16335
|
this.positionTextarea();
|
|
15582
16336
|
this.startBlink();
|
|
15583
16337
|
this.repaint();
|
|
@@ -15606,7 +16360,7 @@ var TextEditSession = class {
|
|
|
15606
16360
|
if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
|
|
15607
16361
|
}
|
|
15608
16362
|
focus() {
|
|
15609
|
-
this.textarea?.focus();
|
|
16363
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15610
16364
|
}
|
|
15611
16365
|
createTextarea() {
|
|
15612
16366
|
const ta = this.host.container.ownerDocument.createElement("textarea");
|
|
@@ -15655,8 +16409,12 @@ var TextEditSession = class {
|
|
|
15655
16409
|
if (!ta) return;
|
|
15656
16410
|
const c = this.caretStagePoint();
|
|
15657
16411
|
const p = this.host.stageToContainer(c);
|
|
15658
|
-
|
|
15659
|
-
|
|
16412
|
+
this.host.revealCaret?.(p);
|
|
16413
|
+
const box = this.host.container;
|
|
16414
|
+
const maxX = Math.max(0, (box.clientWidth || Infinity) - 2);
|
|
16415
|
+
const maxY = Math.max(0, (box.clientHeight || Infinity) - 20);
|
|
16416
|
+
ta.style.left = `${Math.min(Math.max(p.x, 0), maxX)}px`;
|
|
16417
|
+
ta.style.top = `${Math.min(Math.max(p.y, 0), maxY)}px`;
|
|
15660
16418
|
}
|
|
15661
16419
|
caretStagePoint() {
|
|
15662
16420
|
const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
|
|
@@ -15688,10 +16446,41 @@ var TextEditSession = class {
|
|
|
15688
16446
|
if (e.key === "Escape") this.host.onRequestExit("escape");
|
|
15689
16447
|
else if (isUndo) this.undo();
|
|
15690
16448
|
else if (isRedo) this.redo();
|
|
16449
|
+
else if (this.handleRichKey(e, mod, key)) return true;
|
|
15691
16450
|
else if (e.key === "Tab") return true;
|
|
15692
16451
|
else return false;
|
|
15693
16452
|
return true;
|
|
15694
16453
|
}
|
|
16454
|
+
/** Format shortcuts and list key rules; only when rich text is enabled. */
|
|
16455
|
+
handleRichKey(e, mod, key) {
|
|
16456
|
+
if (!this.host.richText) return false;
|
|
16457
|
+
if (mod && !e.shiftKey && !e.altKey) {
|
|
16458
|
+
const marks = {
|
|
16459
|
+
b: "bold",
|
|
16460
|
+
i: "italic",
|
|
16461
|
+
u: "underline"
|
|
16462
|
+
};
|
|
16463
|
+
if (marks[key]) {
|
|
16464
|
+
this.toggleMark(marks[key]);
|
|
16465
|
+
return true;
|
|
16466
|
+
}
|
|
16467
|
+
}
|
|
16468
|
+
if (mod || this.composing) return false;
|
|
16469
|
+
if (e.key === "Tab") {
|
|
16470
|
+
if (e.shiftKey) this.outdent();
|
|
16471
|
+
else this.indent();
|
|
16472
|
+
return true;
|
|
16473
|
+
}
|
|
16474
|
+
if (e.key === "Enter") {
|
|
16475
|
+
if (this.model.enter()) this.afterFormat("command", true);
|
|
16476
|
+
return true;
|
|
16477
|
+
}
|
|
16478
|
+
if (e.key === "Backspace" && this.model.backspaceInList()) {
|
|
16479
|
+
this.afterFormat("command", true);
|
|
16480
|
+
return true;
|
|
16481
|
+
}
|
|
16482
|
+
return false;
|
|
16483
|
+
}
|
|
15695
16484
|
handleNavigationKey(e) {
|
|
15696
16485
|
if (e.ctrlKey || e.metaKey) return false;
|
|
15697
16486
|
if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
|
|
@@ -15745,7 +16534,15 @@ var TextEditSession = class {
|
|
|
15745
16534
|
while (p < max && oldText[p] === newText[p]) p++;
|
|
15746
16535
|
let s = 0;
|
|
15747
16536
|
while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
|
|
15748
|
-
|
|
16537
|
+
let inserted = newText.slice(p, newText.length - s);
|
|
16538
|
+
if (s + p === oldText.length && inserted !== "") {
|
|
16539
|
+
const q = ta.selectionEnd - inserted.length;
|
|
16540
|
+
if (q >= 0 && q < p && newText.slice(q, q + inserted.length) === inserted && oldText.slice(0, q) + inserted + oldText.slice(q) === newText) {
|
|
16541
|
+
s = oldText.length - q;
|
|
16542
|
+
p = q;
|
|
16543
|
+
inserted = newText.slice(p, newText.length - s);
|
|
16544
|
+
}
|
|
16545
|
+
}
|
|
15749
16546
|
const inputType = e.inputType ?? "";
|
|
15750
16547
|
const kind = this.editKindFor(inputType);
|
|
15751
16548
|
this.model.replaceRange(p, oldText.length - s, inserted, kind);
|
|
@@ -15786,6 +16583,7 @@ var TextEditSession = class {
|
|
|
15786
16583
|
if (this.model.undo()) {
|
|
15787
16584
|
this.syncTextarea();
|
|
15788
16585
|
this.afterContentChange(0, true);
|
|
16586
|
+
this.host.onFormatChange?.("undo");
|
|
15789
16587
|
}
|
|
15790
16588
|
}
|
|
15791
16589
|
redo() {
|
|
@@ -15793,8 +16591,42 @@ var TextEditSession = class {
|
|
|
15793
16591
|
if (this.model.redo()) {
|
|
15794
16592
|
this.syncTextarea();
|
|
15795
16593
|
this.afterContentChange(0, true);
|
|
16594
|
+
this.host.onFormatChange?.("redo");
|
|
15796
16595
|
}
|
|
15797
16596
|
}
|
|
16597
|
+
afterFormat(reason, content = false) {
|
|
16598
|
+
this.flushPending();
|
|
16599
|
+
this.syncTextarea();
|
|
16600
|
+
if (content) this.afterContentChange(0, true);
|
|
16601
|
+
else this.applyContent();
|
|
16602
|
+
this.afterSelectionChange();
|
|
16603
|
+
this.host.onFormatChange?.(reason);
|
|
16604
|
+
}
|
|
16605
|
+
toggleMark(key) {
|
|
16606
|
+
if (this.model.toggleMark(key)) this.afterFormat("command");
|
|
16607
|
+
}
|
|
16608
|
+
setMark(key, value) {
|
|
16609
|
+
if (this.model.setMark(key, value)) this.afterFormat("command");
|
|
16610
|
+
}
|
|
16611
|
+
toggleList(kind) {
|
|
16612
|
+
if (this.model.toggleList(kind)) this.afterFormat("command");
|
|
16613
|
+
}
|
|
16614
|
+
setAlign(align) {
|
|
16615
|
+
if (this.model.setAlign(align)) this.afterFormat("command");
|
|
16616
|
+
}
|
|
16617
|
+
indent() {
|
|
16618
|
+
if (this.model.indent()) this.afterFormat("command");
|
|
16619
|
+
}
|
|
16620
|
+
outdent() {
|
|
16621
|
+
if (this.model.outdent()) this.afterFormat("command");
|
|
16622
|
+
}
|
|
16623
|
+
getSelection() {
|
|
16624
|
+
return this.model.getSelection();
|
|
16625
|
+
}
|
|
16626
|
+
getFormat() {
|
|
16627
|
+
const { start, end } = this.model.getSelection();
|
|
16628
|
+
return getSelectionFormat(this.model.getBlocks(), start, end, this.target.getFontSize(), this.model.storedMarks);
|
|
16629
|
+
}
|
|
15798
16630
|
toLocal(e) {
|
|
15799
16631
|
const stagePt = this.host.clientToStage(e.clientX, e.clientY);
|
|
15800
16632
|
const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
|
|
@@ -15809,12 +16641,26 @@ var TextEditSession = class {
|
|
|
15809
16641
|
const { width, height } = this.target.getSize();
|
|
15810
16642
|
return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
|
|
15811
16643
|
}
|
|
16644
|
+
/**
|
|
16645
|
+
* True for pointer targets inside an element the app marked with
|
|
16646
|
+
* `data-weave-text-edit-keep` (e.g. a formatting toolbar): they neither
|
|
16647
|
+
* end the session nor, unless they are form fields, steal focus.
|
|
16648
|
+
*/
|
|
16649
|
+
isKeepTarget(e) {
|
|
16650
|
+
const t = e.target;
|
|
16651
|
+
return typeof Element !== "undefined" && t instanceof Element && !!t.closest("[data-weave-text-edit-keep]");
|
|
16652
|
+
}
|
|
15812
16653
|
onMouseDown = (e) => {
|
|
16654
|
+
if (this.isKeepTarget(e)) {
|
|
16655
|
+
if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
|
|
16656
|
+
return;
|
|
16657
|
+
}
|
|
15813
16658
|
if (this.isInside(this.toLocal(e))) e.preventDefault();
|
|
15814
16659
|
};
|
|
15815
16660
|
onPointerDown = (e) => {
|
|
15816
16661
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
15817
16662
|
if (this.textarea && e.target === this.textarea) return;
|
|
16663
|
+
if (this.isKeepTarget(e)) return;
|
|
15818
16664
|
const local = this.toLocal(e);
|
|
15819
16665
|
if (!this.isInside(local)) {
|
|
15820
16666
|
e.stopPropagation();
|
|
@@ -15825,10 +16671,11 @@ var TextEditSession = class {
|
|
|
15825
16671
|
e.preventDefault();
|
|
15826
16672
|
const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
|
|
15827
16673
|
const text = this.model.getText();
|
|
15828
|
-
|
|
16674
|
+
const clicks = this.countClicks(e);
|
|
16675
|
+
if (clicks >= 3) {
|
|
15829
16676
|
const [s, en] = paragraphRangeAt(text, offset);
|
|
15830
16677
|
this.model.setSelection(s, en);
|
|
15831
|
-
} else if (
|
|
16678
|
+
} else if (clicks === 2) {
|
|
15832
16679
|
const [s, en] = wordRangeAt(text, offset);
|
|
15833
16680
|
this.model.setSelection(s, en);
|
|
15834
16681
|
} else {
|
|
@@ -15837,8 +16684,31 @@ var TextEditSession = class {
|
|
|
15837
16684
|
}
|
|
15838
16685
|
this.syncTextarea();
|
|
15839
16686
|
this.afterSelectionChange();
|
|
15840
|
-
this.textarea?.focus();
|
|
16687
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15841
16688
|
};
|
|
16689
|
+
lastClick = {
|
|
16690
|
+
t: 0,
|
|
16691
|
+
x: 0,
|
|
16692
|
+
y: 0,
|
|
16693
|
+
n: 0
|
|
16694
|
+
};
|
|
16695
|
+
/**
|
|
16696
|
+
* Browsers report `detail` as 0 on pointerdown, so multi-click is counted
|
|
16697
|
+
* here (same spot, within 500 ms). A real `detail` still wins.
|
|
16698
|
+
*/
|
|
16699
|
+
countClicks(e) {
|
|
16700
|
+
const now = e.timeStamp || Date.now();
|
|
16701
|
+
const l = this.lastClick;
|
|
16702
|
+
const near = now - l.t < 500 && Math.hypot(e.clientX - l.x, e.clientY - l.y) < 5 && l.n > 0;
|
|
16703
|
+
const n = Math.max(e.detail, near ? l.n + 1 : 1);
|
|
16704
|
+
this.lastClick = {
|
|
16705
|
+
t: now,
|
|
16706
|
+
x: e.clientX,
|
|
16707
|
+
y: e.clientY,
|
|
16708
|
+
n: n >= 3 ? 0 : n
|
|
16709
|
+
};
|
|
16710
|
+
return n;
|
|
16711
|
+
}
|
|
15842
16712
|
onPointerMove = (e) => {
|
|
15843
16713
|
if (!this.dragging) return;
|
|
15844
16714
|
const local = this.toLocal(e);
|
|
@@ -15850,13 +16720,14 @@ var TextEditSession = class {
|
|
|
15850
16720
|
onPointerUp = () => {
|
|
15851
16721
|
if (!this.dragging) return;
|
|
15852
16722
|
this.dragging = false;
|
|
15853
|
-
this.textarea?.focus();
|
|
16723
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15854
16724
|
};
|
|
15855
16725
|
afterSelectionChange() {
|
|
15856
16726
|
this.syncTextarea();
|
|
15857
16727
|
this.positionTextarea();
|
|
15858
16728
|
this.resetBlink();
|
|
15859
16729
|
this.repaint();
|
|
16730
|
+
this.host.onSelectionChange?.();
|
|
15860
16731
|
}
|
|
15861
16732
|
afterContentChange(insertedLength, immediate = false) {
|
|
15862
16733
|
this.flushPending();
|
|
@@ -15880,6 +16751,7 @@ var TextEditSession = class {
|
|
|
15880
16751
|
applyContent() {
|
|
15881
16752
|
this.target.setBlocks(this.model.getBlocks());
|
|
15882
16753
|
this.host.onChange?.(this.model.getBlocks());
|
|
16754
|
+
this.host.onSelectionChange?.();
|
|
15883
16755
|
this.positionTextarea();
|
|
15884
16756
|
this.target.requestRedraw();
|
|
15885
16757
|
this.repaint();
|
|
@@ -15955,15 +16827,29 @@ function host_signal(signal, abort) {
|
|
|
15955
16827
|
//#region src/nodes/text-v2/constants.ts
|
|
15956
16828
|
/** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
|
|
15957
16829
|
const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
|
|
16830
|
+
/** Default list indent per depth, as a multiple of the node `fontSize`. */
|
|
16831
|
+
const WEAVE_TEXT_V2_DEFAULT_INDENT_STEP = 1.5;
|
|
15958
16832
|
const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
|
|
15959
16833
|
const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
|
|
15960
16834
|
...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
|
|
15961
|
-
edition: {
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
16835
|
+
edition: {
|
|
16836
|
+
outline: {
|
|
16837
|
+
enabled: true,
|
|
16838
|
+
color: "#0074ff",
|
|
16839
|
+
width: 1,
|
|
16840
|
+
padding: 0
|
|
16841
|
+
},
|
|
16842
|
+
followPadding: {
|
|
16843
|
+
top: 40,
|
|
16844
|
+
right: 40,
|
|
16845
|
+
bottom: 40,
|
|
16846
|
+
left: 40
|
|
16847
|
+
}
|
|
16848
|
+
},
|
|
16849
|
+
richText: {
|
|
16850
|
+
enabled: false,
|
|
16851
|
+
indentStep: WEAVE_TEXT_V2_DEFAULT_INDENT_STEP
|
|
16852
|
+
},
|
|
15967
16853
|
sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
|
|
15968
16854
|
};
|
|
15969
16855
|
//#endregion
|
|
@@ -15991,6 +16877,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
15991
16877
|
editingNodeId = null;
|
|
15992
16878
|
session = null;
|
|
15993
16879
|
sizeWarned = false;
|
|
16880
|
+
selectionTimer;
|
|
15994
16881
|
nodeRenderedAddedRegistered = false;
|
|
15995
16882
|
keyPressHandler;
|
|
15996
16883
|
constructor(params) {
|
|
@@ -16064,6 +16951,11 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16064
16951
|
linkPreviousTextDecoration
|
|
16065
16952
|
}
|
|
16066
16953
|
});
|
|
16954
|
+
text.setIndentStep(this.indentStep());
|
|
16955
|
+
text.setLinkStyle({
|
|
16956
|
+
color: this.config.link.defaultColor,
|
|
16957
|
+
hoverColor: this.config.link.hoverColor
|
|
16958
|
+
});
|
|
16067
16959
|
this.setupDefaultNodeAugmentation(text);
|
|
16068
16960
|
const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
|
|
16069
16961
|
text.getTransformerProperties = function() {
|
|
@@ -16159,7 +17051,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16159
17051
|
this.applySize(shape);
|
|
16160
17052
|
},
|
|
16161
17053
|
getLayout: () => shape.ensureLayout(),
|
|
16162
|
-
measure: (t) => shape.measureText(t),
|
|
17054
|
+
measure: (t, f) => shape.measureText(t, f),
|
|
16163
17055
|
getAbsoluteTransform: () => shape.getAbsoluteTransform(),
|
|
16164
17056
|
getContentOffset: () => shape.getContentOffset(),
|
|
16165
17057
|
getSize: () => ({
|
|
@@ -16210,7 +17102,23 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16210
17102
|
y: p.y * sy
|
|
16211
17103
|
};
|
|
16212
17104
|
},
|
|
17105
|
+
revealCaret: (p) => {
|
|
17106
|
+
const box = container;
|
|
17107
|
+
const w = box.clientWidth;
|
|
17108
|
+
const h = box.clientHeight;
|
|
17109
|
+
if (!w || !h) return;
|
|
17110
|
+
const f = this.config.edition.followPadding;
|
|
17111
|
+
const dx = p.x < f.left ? f.left - p.x : p.x > w - f.right ? w - f.right - p.x : 0;
|
|
17112
|
+
const dy = p.y < f.top ? f.top - p.y : p.y > h - f.bottom ? h - f.bottom - p.y : 0;
|
|
17113
|
+
if (!dx && !dy) return;
|
|
17114
|
+
stage.x(stage.x() + dx);
|
|
17115
|
+
stage.y(stage.y() + dy);
|
|
17116
|
+
this.instance.emitEvent("onStageMove");
|
|
17117
|
+
},
|
|
16213
17118
|
selectAllOnStart: !create,
|
|
17119
|
+
richText: this.isRich(shape),
|
|
17120
|
+
onSelectionChange: () => this.scheduleSelectionEvent(shape),
|
|
17121
|
+
onFormatChange: (reason) => this.emitFormatChange(shape, reason),
|
|
16214
17122
|
caretColor: this.config.cursor.color,
|
|
16215
17123
|
selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
|
|
16216
17124
|
...this.config.edition.outline.enabled && {
|
|
@@ -16246,6 +17154,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16246
17154
|
exitEditMode(shape) {
|
|
16247
17155
|
const session = this.session;
|
|
16248
17156
|
if (!session || !this.editing) return;
|
|
17157
|
+
clearTimeout(this.selectionTimer);
|
|
16249
17158
|
const { changed } = session.getResult();
|
|
16250
17159
|
session.destroy();
|
|
16251
17160
|
this.session = null;
|
|
@@ -16308,11 +17217,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16308
17217
|
text.getLayer()?.batchDraw();
|
|
16309
17218
|
}
|
|
16310
17219
|
const layout = text.getAttrs().layout;
|
|
16311
|
-
if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner()))
|
|
16312
|
-
text.width(text.width() * text.scaleX());
|
|
16313
|
-
resetScale(text);
|
|
16314
|
-
text.fontSize(text.fontSize() * text.scaleY());
|
|
16315
|
-
}
|
|
17220
|
+
if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
|
|
16316
17221
|
text.setAttr("shouldUpdateOnTransform", false);
|
|
16317
17222
|
text.getLayer()?.batchDraw();
|
|
16318
17223
|
});
|
|
@@ -16356,6 +17261,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16356
17261
|
};
|
|
16357
17262
|
const isLinked = Boolean(nextProps.link);
|
|
16358
17263
|
const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
|
|
17264
|
+
shape.setIndentStep(this.indentStep());
|
|
16359
17265
|
shape.setAttrs({
|
|
16360
17266
|
...nextProps,
|
|
16361
17267
|
...this.outlineProps(),
|
|
@@ -16393,7 +17299,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16393
17299
|
delete cleanedAttrs.dragBoundFunc;
|
|
16394
17300
|
if (cleanedAttrs.link) {
|
|
16395
17301
|
if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
|
|
16396
|
-
|
|
17302
|
+
cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
|
|
16397
17303
|
}
|
|
16398
17304
|
return {
|
|
16399
17305
|
key: attrs.id ?? "",
|
|
@@ -16408,18 +17314,85 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16408
17314
|
}
|
|
16409
17315
|
};
|
|
16410
17316
|
}
|
|
17317
|
+
linkCtxMenuGuardRegistered = false;
|
|
17318
|
+
linkHoverText = null;
|
|
17319
|
+
linkModifier = false;
|
|
17320
|
+
linkHoverRefresh = null;
|
|
17321
|
+
linkHitAt(text) {
|
|
17322
|
+
const link = text.getAttrs().link;
|
|
17323
|
+
if (link) {
|
|
17324
|
+
const p = this.localPointer(text);
|
|
17325
|
+
return p.x >= 0 && p.y >= 0 && p.x <= text.width() && p.y <= text.height() ? { url: link } : null;
|
|
17326
|
+
}
|
|
17327
|
+
return text.linkAtPoint(this.localPointer(text));
|
|
17328
|
+
}
|
|
17329
|
+
registerLinkContextMenuGuard() {
|
|
17330
|
+
if (this.linkCtxMenuGuardRegistered || this.instance.isServerSide()) return;
|
|
17331
|
+
this.linkCtxMenuGuardRegistered = true;
|
|
17332
|
+
const stage = this.instance.getStage();
|
|
17333
|
+
const signal = this.instance.getEventsController().signal;
|
|
17334
|
+
const syncModifier = (e) => {
|
|
17335
|
+
const next = e.ctrlKey || e.metaKey;
|
|
17336
|
+
if (next === this.linkModifier) return;
|
|
17337
|
+
this.linkModifier = next;
|
|
17338
|
+
this.linkHoverRefresh?.();
|
|
17339
|
+
};
|
|
17340
|
+
window.addEventListener("keydown", syncModifier, { signal });
|
|
17341
|
+
window.addEventListener("keyup", syncModifier, { signal });
|
|
17342
|
+
window.addEventListener("blur", () => {
|
|
17343
|
+
this.linkModifier = false;
|
|
17344
|
+
this.linkHoverRefresh?.();
|
|
17345
|
+
}, { signal });
|
|
17346
|
+
stage.container().addEventListener("contextmenu", (e) => {
|
|
17347
|
+
if (this.editing || !e.ctrlKey) return;
|
|
17348
|
+
const text = this.linkHoverText;
|
|
17349
|
+
if (!text) return;
|
|
17350
|
+
stage.setPointersPositions(e);
|
|
17351
|
+
const hit = this.linkHitAt(text);
|
|
17352
|
+
if (!hit) return;
|
|
17353
|
+
e.preventDefault();
|
|
17354
|
+
e.stopPropagation();
|
|
17355
|
+
this.openUrl(hit.url);
|
|
17356
|
+
}, {
|
|
17357
|
+
capture: true,
|
|
17358
|
+
signal: this.instance.getEventsController().signal
|
|
17359
|
+
});
|
|
17360
|
+
}
|
|
16411
17361
|
setupLinkBehavior(text) {
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
|
|
16415
|
-
|
|
17362
|
+
this.registerLinkContextMenuGuard();
|
|
17363
|
+
const container = () => this.instance.getStage().container();
|
|
17364
|
+
const idleCursor = () => this.isNodeSelected(text) ? "grab" : "pointer";
|
|
17365
|
+
const applyHover = () => {
|
|
17366
|
+
if (this.editing) return;
|
|
17367
|
+
this.linkHoverText = text;
|
|
17368
|
+
const hit = this.linkModifier ? this.linkHitAt(text) : null;
|
|
17369
|
+
if (!text.getAttrs().link) text.setHoveredRun(hit?.key ?? null);
|
|
17370
|
+
else {
|
|
17371
|
+
text.fill(hit ? this.config.link.hoverColor : this.config.link.defaultColor);
|
|
17372
|
+
text.getLayer()?.batchDraw();
|
|
17373
|
+
}
|
|
17374
|
+
container().style.cursor = hit ? "pointer" : idleCursor();
|
|
17375
|
+
};
|
|
17376
|
+
const updateHover = (e) => {
|
|
17377
|
+
if (e?.evt) this.linkModifier = e.evt.ctrlKey || e.evt.metaKey;
|
|
17378
|
+
this.linkHoverRefresh = applyHover;
|
|
17379
|
+
applyHover();
|
|
17380
|
+
};
|
|
17381
|
+
text.handleMouseover = (e) => {
|
|
17382
|
+
updateHover(e);
|
|
16416
17383
|
};
|
|
16417
17384
|
text.handleMouseout = () => {
|
|
17385
|
+
if (this.linkHoverText === text) {
|
|
17386
|
+
this.linkHoverText = null;
|
|
17387
|
+
this.linkHoverRefresh = null;
|
|
17388
|
+
}
|
|
17389
|
+
text.setHoveredRun(null);
|
|
16418
17390
|
if (!text.getAttrs().link) return;
|
|
16419
17391
|
text.fill(this.config.link.defaultColor);
|
|
16420
17392
|
text.getLayer()?.batchDraw();
|
|
16421
17393
|
};
|
|
16422
|
-
text.
|
|
17394
|
+
text.on("pointermove", updateHover);
|
|
17395
|
+
text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
|
|
16423
17396
|
let linkClickTimeout;
|
|
16424
17397
|
const clearLinkClickTimeout = () => {
|
|
16425
17398
|
if (linkClickTimeout) {
|
|
@@ -16433,27 +17406,41 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16433
17406
|
previousDblClick();
|
|
16434
17407
|
};
|
|
16435
17408
|
text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
|
|
16436
|
-
|
|
17409
|
+
const runLink = text.getAttrs().link ? null : text.linkAtPoint(this.localPointer(text));
|
|
17410
|
+
if (!text.getAttrs().link && !runLink) return;
|
|
17411
|
+
const open = () => runLink ? this.openUrl(runLink.url) : this.openTextLink(text);
|
|
16437
17412
|
if (ctrlOrMetaPressed) {
|
|
16438
17413
|
clearLinkClickTimeout();
|
|
16439
|
-
|
|
17414
|
+
open();
|
|
16440
17415
|
return;
|
|
16441
17416
|
}
|
|
16442
17417
|
if (wasSelected) {
|
|
16443
17418
|
clearLinkClickTimeout();
|
|
16444
17419
|
linkClickTimeout = setTimeout(() => {
|
|
16445
17420
|
linkClickTimeout = void 0;
|
|
16446
|
-
|
|
17421
|
+
open();
|
|
16447
17422
|
}, Konva.dblClickWindow);
|
|
16448
17423
|
}
|
|
16449
17424
|
};
|
|
16450
17425
|
}
|
|
16451
|
-
|
|
17426
|
+
localPointer(text) {
|
|
17427
|
+
return text.getRelativePointerPosition() ?? {
|
|
17428
|
+
x: -1,
|
|
17429
|
+
y: -1
|
|
17430
|
+
};
|
|
17431
|
+
}
|
|
17432
|
+
lastLinkOpen = 0;
|
|
17433
|
+
openUrl(link) {
|
|
16452
17434
|
if (this.instance.isServerSide()) return;
|
|
16453
|
-
const link = textNode.getAttrs().link;
|
|
16454
17435
|
if (typeof link !== "string" || !isValidTextLink(link)) return;
|
|
17436
|
+
const now = Date.now();
|
|
17437
|
+
if (now - this.lastLinkOpen < 500) return;
|
|
17438
|
+
this.lastLinkOpen = now;
|
|
16455
17439
|
window.open(link, "_blank", "noopener,noreferrer");
|
|
16456
17440
|
}
|
|
17441
|
+
openTextLink(textNode) {
|
|
17442
|
+
this.openUrl(textNode.getAttrs().link);
|
|
17443
|
+
}
|
|
16457
17444
|
getLink(nodeInstance) {
|
|
16458
17445
|
const link = nodeInstance.getAttrs().link;
|
|
16459
17446
|
return typeof link === "string" ? link : void 0;
|
|
@@ -16473,6 +17460,173 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16473
17460
|
delete serialized.props.linkPreviousTextDecoration;
|
|
16474
17461
|
this.instance.updateNode(serialized);
|
|
16475
17462
|
}
|
|
17463
|
+
indentStep() {
|
|
17464
|
+
const step = this.config.richText.indentStep;
|
|
17465
|
+
return Number.isFinite(step) && step > 0 ? step : 1.5;
|
|
17466
|
+
}
|
|
17467
|
+
/**
|
|
17468
|
+
* Rich text is on for a node when its `richText` attribute says so;
|
|
17469
|
+
* nodes without the attribute follow `config.richText.enabled`.
|
|
17470
|
+
*/
|
|
17471
|
+
isRich(nodeInstance) {
|
|
17472
|
+
const own = nodeInstance.getAttrs().richText;
|
|
17473
|
+
return typeof own === "boolean" ? own : this.config.richText.enabled;
|
|
17474
|
+
}
|
|
17475
|
+
isRichTextEnabled(nodeInstance) {
|
|
17476
|
+
return nodeInstance ? this.isRich(nodeInstance) : this.config.richText.enabled;
|
|
17477
|
+
}
|
|
17478
|
+
activeSession(shape) {
|
|
17479
|
+
return this.editing && this.editingNodeId === shape.id() ? this.session : null;
|
|
17480
|
+
}
|
|
17481
|
+
emitFormatChange(shape, reason) {
|
|
17482
|
+
this.instance.emitEvent("onTextFormatChange", {
|
|
17483
|
+
node: shape,
|
|
17484
|
+
format: this.getSelectionFormat(shape),
|
|
17485
|
+
reason
|
|
17486
|
+
});
|
|
17487
|
+
}
|
|
17488
|
+
/** Selection events fire often while dragging: coalesce them. */
|
|
17489
|
+
scheduleSelectionEvent(shape) {
|
|
17490
|
+
if (this.selectionTimer) return;
|
|
17491
|
+
this.selectionTimer = setTimeout(() => {
|
|
17492
|
+
this.selectionTimer = void 0;
|
|
17493
|
+
const session = this.activeSession(shape);
|
|
17494
|
+
if (!session) return;
|
|
17495
|
+
const { start, end } = session.getSelection();
|
|
17496
|
+
this.instance.emitEvent("onTextSelectionChange", {
|
|
17497
|
+
node: shape,
|
|
17498
|
+
start,
|
|
17499
|
+
end,
|
|
17500
|
+
format: session.getFormat()
|
|
17501
|
+
});
|
|
17502
|
+
}, 50);
|
|
17503
|
+
}
|
|
17504
|
+
/**
|
|
17505
|
+
* Format of the active selection while editing, otherwise of `range`
|
|
17506
|
+
* (default: the whole text). Sizes are px.
|
|
17507
|
+
*/
|
|
17508
|
+
getSelectionFormat(nodeInstance, range) {
|
|
17509
|
+
const shape = nodeInstance;
|
|
17510
|
+
const session = this.activeSession(shape);
|
|
17511
|
+
if (session) return session.getFormat();
|
|
17512
|
+
const blocks = shape.getBlocks();
|
|
17513
|
+
const r = range ?? {
|
|
17514
|
+
start: 0,
|
|
17515
|
+
end: blocksToText(blocks).length
|
|
17516
|
+
};
|
|
17517
|
+
return getSelectionFormat(blocks, r.start, r.end, shape.fontSize());
|
|
17518
|
+
}
|
|
17519
|
+
/**
|
|
17520
|
+
* Run a block transform on a non-editing node: one `updateNode`, so one
|
|
17521
|
+
* Weave undo step. Returns false when rich text is disabled.
|
|
17522
|
+
*/
|
|
17523
|
+
commandOnNode(shape, range, fn) {
|
|
17524
|
+
const blocks = shape.getBlocks();
|
|
17525
|
+
const next = fn(blocks, range?.start ?? 0, range?.end ?? blocksToText(blocks).length);
|
|
17526
|
+
if (JSON.stringify(next) === JSON.stringify(blocks)) return false;
|
|
17527
|
+
shape.setBlocks(next);
|
|
17528
|
+
this.applySize(shape);
|
|
17529
|
+
this.instance.updateNode(this.serialize(shape));
|
|
17530
|
+
this.emitFormatChange(shape, "command");
|
|
17531
|
+
return true;
|
|
17532
|
+
}
|
|
17533
|
+
/**
|
|
17534
|
+
* Set a mark (`undefined` clears it). `size` is in px. While the node is
|
|
17535
|
+
* being edited it applies to the editor selection, otherwise to `range`
|
|
17536
|
+
* (default: the whole text).
|
|
17537
|
+
*/
|
|
17538
|
+
applyMark(nodeInstance, key, value, range) {
|
|
17539
|
+
const shape = nodeInstance;
|
|
17540
|
+
if (!this.isRich(shape)) return false;
|
|
17541
|
+
let v = value;
|
|
17542
|
+
if (key === "link" && v !== void 0 && !isValidTextLink(String(v))) return false;
|
|
17543
|
+
if (key === "size" && typeof value === "number") v = sizeToRatio(value, shape.fontSize());
|
|
17544
|
+
const session = this.activeSession(shape);
|
|
17545
|
+
if (session) {
|
|
17546
|
+
session.setMark(key, v);
|
|
17547
|
+
return true;
|
|
17548
|
+
}
|
|
17549
|
+
return this.commandOnNode(shape, range, (b, s, e) => applyMark(b, s, e, key, v));
|
|
17550
|
+
}
|
|
17551
|
+
/** Link a range (or the editor selection) to an http(s) URL. */
|
|
17552
|
+
setRunLink(nodeInstance, url, range) {
|
|
17553
|
+
if (typeof url !== "string" || !isValidTextLink(url)) return false;
|
|
17554
|
+
return this.applyMark(nodeInstance, "link", url, range);
|
|
17555
|
+
}
|
|
17556
|
+
removeRunLink(nodeInstance, range) {
|
|
17557
|
+
return this.applyMark(nodeInstance, "link", void 0, range);
|
|
17558
|
+
}
|
|
17559
|
+
toggleMark(nodeInstance, key, range) {
|
|
17560
|
+
const shape = nodeInstance;
|
|
17561
|
+
if (!this.isRich(shape)) return false;
|
|
17562
|
+
const session = this.activeSession(shape);
|
|
17563
|
+
if (session) {
|
|
17564
|
+
session.toggleMark(key);
|
|
17565
|
+
return true;
|
|
17566
|
+
}
|
|
17567
|
+
return this.commandOnNode(shape, range, (b, s, e) => toggleMark(b, s, e, key));
|
|
17568
|
+
}
|
|
17569
|
+
toggleList(nodeInstance, kind, range) {
|
|
17570
|
+
const shape = nodeInstance;
|
|
17571
|
+
if (!this.isRich(shape)) return false;
|
|
17572
|
+
const session = this.activeSession(shape);
|
|
17573
|
+
if (session) {
|
|
17574
|
+
session.toggleList(kind);
|
|
17575
|
+
return true;
|
|
17576
|
+
}
|
|
17577
|
+
return this.commandOnNode(shape, range, (b, s, e) => toggleList(b, s, e, kind));
|
|
17578
|
+
}
|
|
17579
|
+
/** Align every paragraph touched by the range (default: all). */
|
|
17580
|
+
setAlign(nodeInstance, align, range) {
|
|
17581
|
+
const shape = nodeInstance;
|
|
17582
|
+
if (!this.isRich(shape)) return false;
|
|
17583
|
+
if (align !== void 0 && ![
|
|
17584
|
+
"left",
|
|
17585
|
+
"center",
|
|
17586
|
+
"right"
|
|
17587
|
+
].includes(align)) return false;
|
|
17588
|
+
const session = this.activeSession(shape);
|
|
17589
|
+
if (session) {
|
|
17590
|
+
session.setAlign(align);
|
|
17591
|
+
return true;
|
|
17592
|
+
}
|
|
17593
|
+
return this.commandOnNode(shape, range, (b, s, e) => setBlockAlign(b, s, e, align));
|
|
17594
|
+
}
|
|
17595
|
+
indent(nodeInstance, range) {
|
|
17596
|
+
const shape = nodeInstance;
|
|
17597
|
+
if (!this.isRich(shape)) return false;
|
|
17598
|
+
const session = this.activeSession(shape);
|
|
17599
|
+
if (session) {
|
|
17600
|
+
session.indent();
|
|
17601
|
+
return true;
|
|
17602
|
+
}
|
|
17603
|
+
return this.commandOnNode(shape, range, indentBlocks);
|
|
17604
|
+
}
|
|
17605
|
+
outdent(nodeInstance, range) {
|
|
17606
|
+
const shape = nodeInstance;
|
|
17607
|
+
if (!this.isRich(shape)) return false;
|
|
17608
|
+
const session = this.activeSession(shape);
|
|
17609
|
+
if (session) {
|
|
17610
|
+
session.outdent();
|
|
17611
|
+
return true;
|
|
17612
|
+
}
|
|
17613
|
+
return this.commandOnNode(shape, range, outdentBlocks);
|
|
17614
|
+
}
|
|
17615
|
+
/**
|
|
17616
|
+
* Scale the whole text: sets the node `fontSize` (px); size marks are
|
|
17617
|
+
* ratios of it, so every run scales with it.
|
|
17618
|
+
*/
|
|
17619
|
+
setBaseFontSize(nodeInstance, px) {
|
|
17620
|
+
const shape = nodeInstance;
|
|
17621
|
+
if (!this.isRich(shape)) return false;
|
|
17622
|
+
if (!Number.isFinite(px) || px <= 0) return false;
|
|
17623
|
+
if (shape.fontSize() === px) return false;
|
|
17624
|
+
shape.fontSize(px);
|
|
17625
|
+
this.applySize(shape);
|
|
17626
|
+
if (!this.activeSession(shape)) this.instance.updateNode(this.serialize(shape));
|
|
17627
|
+
this.emitFormatChange(shape, "base-size");
|
|
17628
|
+
return true;
|
|
17629
|
+
}
|
|
16476
17630
|
resetSmartLayout(textNode) {
|
|
16477
17631
|
textNode.setAttr("smartFixedWidth", void 0);
|
|
16478
17632
|
textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
|
|
@@ -16491,7 +17645,36 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16491
17645
|
return next;
|
|
16492
17646
|
}
|
|
16493
17647
|
static getSchema() {
|
|
16494
|
-
|
|
17648
|
+
const base = WeaveTextNode.getSchema();
|
|
17649
|
+
const marks = z.object({
|
|
17650
|
+
bold: z.boolean().optional(),
|
|
17651
|
+
italic: z.boolean().optional(),
|
|
17652
|
+
underline: z.boolean().optional(),
|
|
17653
|
+
strike: z.boolean().optional(),
|
|
17654
|
+
color: z.string().optional(),
|
|
17655
|
+
size: z.number().positive().optional().describe("Ratio of the node fontSize (1 is omitted)."),
|
|
17656
|
+
family: z.string().optional(),
|
|
17657
|
+
lineHeight: z.number().positive().optional(),
|
|
17658
|
+
link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional()
|
|
17659
|
+
}).passthrough();
|
|
17660
|
+
const block = z.object({
|
|
17661
|
+
runs: z.array(z.object({
|
|
17662
|
+
t: z.string(),
|
|
17663
|
+
m: marks.optional()
|
|
17664
|
+
})),
|
|
17665
|
+
kind: z.enum(["ul", "ol"]).optional(),
|
|
17666
|
+
depth: z.number().int().min(0).optional(),
|
|
17667
|
+
start: z.number().int().min(1).optional(),
|
|
17668
|
+
align: z.enum([
|
|
17669
|
+
"left",
|
|
17670
|
+
"center",
|
|
17671
|
+
"right"
|
|
17672
|
+
]).optional()
|
|
17673
|
+
});
|
|
17674
|
+
return base.extend({ props: base.shape.props.extend({
|
|
17675
|
+
richText: z.boolean().optional().describe("Enables rich text commands for this node."),
|
|
17676
|
+
blocks: z.array(block).optional().describe("Rich content: paragraphs of runs with marks and optional list fields. `text` is the derived plain mirror.")
|
|
17677
|
+
}) });
|
|
16495
17678
|
}
|
|
16496
17679
|
};
|
|
16497
17680
|
//#endregion
|
|
@@ -27035,8 +28218,16 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
27035
28218
|
cancelAction;
|
|
27036
28219
|
onPropsChange = void 0;
|
|
27037
28220
|
onInit = void 0;
|
|
27038
|
-
|
|
28221
|
+
toolName;
|
|
28222
|
+
initialProps;
|
|
28223
|
+
/**
|
|
28224
|
+
* `name` and `props` let an app register several text tools, e.g. a
|
|
28225
|
+
* plain one and one creating rich text nodes (`props: { richText: true }`).
|
|
28226
|
+
*/
|
|
28227
|
+
constructor(params) {
|
|
27039
28228
|
super();
|
|
28229
|
+
this.toolName = params?.name ?? "textTool";
|
|
28230
|
+
this.initialProps = params?.props ?? {};
|
|
27040
28231
|
this.initialize();
|
|
27041
28232
|
}
|
|
27042
28233
|
initialize() {
|
|
@@ -27048,7 +28239,7 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
27048
28239
|
this.props = this.initProps();
|
|
27049
28240
|
}
|
|
27050
28241
|
getName() {
|
|
27051
|
-
return
|
|
28242
|
+
return this.toolName;
|
|
27052
28243
|
}
|
|
27053
28244
|
initProps() {
|
|
27054
28245
|
return {
|
|
@@ -27060,7 +28251,8 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
27060
28251
|
align: "left",
|
|
27061
28252
|
lineHeight: 1,
|
|
27062
28253
|
verticalAlign: "top",
|
|
27063
|
-
strokeEnabled: false
|
|
28254
|
+
strokeEnabled: false,
|
|
28255
|
+
...this.initialProps
|
|
27064
28256
|
};
|
|
27065
28257
|
}
|
|
27066
28258
|
setupEvents() {
|