@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/sdk.node.js
CHANGED
|
@@ -4774,12 +4774,39 @@ function registerKeyboardHandlers(ctx) {
|
|
|
4774
4774
|
}
|
|
4775
4775
|
}
|
|
4776
4776
|
if (e.code === "Backspace" || e.code === "Delete") {
|
|
4777
|
-
|
|
4777
|
+
queueMicrotask(() => {
|
|
4778
4778
|
ctx.removeSelectedNodes();
|
|
4779
4779
|
});
|
|
4780
4780
|
return;
|
|
4781
4781
|
}
|
|
4782
4782
|
}, { signal });
|
|
4783
|
+
const isModifier = (e) => e.key === "Control" || e.key === "Meta";
|
|
4784
|
+
const hideHover = () => ctx.getTransformerController().getHoverTransformer().nodes([]);
|
|
4785
|
+
const restoreHover = () => {
|
|
4786
|
+
const pos = stage.getPointerPosition();
|
|
4787
|
+
const shape = pos ? stage.getIntersection(pos) : null;
|
|
4788
|
+
if (!shape) return;
|
|
4789
|
+
shape.fire("pointerover", {
|
|
4790
|
+
evt: {
|
|
4791
|
+
ctrlKey: false,
|
|
4792
|
+
metaKey: false
|
|
4793
|
+
},
|
|
4794
|
+
target: shape
|
|
4795
|
+
}, true);
|
|
4796
|
+
};
|
|
4797
|
+
window.addEventListener("keydown", (e) => {
|
|
4798
|
+
if (isModifier(e)) hideHover();
|
|
4799
|
+
}, {
|
|
4800
|
+
signal,
|
|
4801
|
+
capture: true
|
|
4802
|
+
});
|
|
4803
|
+
window.addEventListener("keyup", (e) => {
|
|
4804
|
+
if (isModifier(e)) restoreHover();
|
|
4805
|
+
}, {
|
|
4806
|
+
signal,
|
|
4807
|
+
capture: true
|
|
4808
|
+
});
|
|
4809
|
+
window.addEventListener("blur", restoreHover, { signal });
|
|
4783
4810
|
stage.container().addEventListener("keyup", (e) => {
|
|
4784
4811
|
if (e.code === "Space") ctx.setSpaceKeyPressed(false);
|
|
4785
4812
|
}, { signal });
|
|
@@ -6535,7 +6562,10 @@ var WeaveNode = class {
|
|
|
6535
6562
|
}
|
|
6536
6563
|
handleMouseOver(e, node) {
|
|
6537
6564
|
const stage = this.instance.getStage();
|
|
6538
|
-
if (e.evt.ctrlKey || e.evt.metaKey)
|
|
6565
|
+
if (e.evt.ctrlKey || e.evt.metaKey) {
|
|
6566
|
+
this.hideHoverState();
|
|
6567
|
+
return false;
|
|
6568
|
+
}
|
|
6539
6569
|
const user = this.instance.getStore().getUser();
|
|
6540
6570
|
const activeAction = this.instance.getActiveAction();
|
|
6541
6571
|
const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled();
|
|
@@ -6582,7 +6612,10 @@ var WeaveNode = class {
|
|
|
6582
6612
|
return cancelBubble;
|
|
6583
6613
|
}
|
|
6584
6614
|
handleMouseout(e, node) {
|
|
6585
|
-
if (e.evt?.ctrlKey || e.evt?.metaKey)
|
|
6615
|
+
if (e.evt?.ctrlKey || e.evt?.metaKey) {
|
|
6616
|
+
this.hideHoverState();
|
|
6617
|
+
return false;
|
|
6618
|
+
}
|
|
6586
6619
|
if (this.instance.getInstanceRecursive(node)) this.hideHoverState();
|
|
6587
6620
|
}
|
|
6588
6621
|
create(key, props) {
|
|
@@ -9505,7 +9538,7 @@ var WeaveRegisterManager = class {
|
|
|
9505
9538
|
};
|
|
9506
9539
|
//#endregion
|
|
9507
9540
|
//#region package.json
|
|
9508
|
-
var version = "5.7.0-SNAPSHOT.
|
|
9541
|
+
var version = "5.7.0-SNAPSHOT.599.1";
|
|
9509
9542
|
//#endregion
|
|
9510
9543
|
//#region src/managers/setup.ts
|
|
9511
9544
|
var WeaveSetupManager = class {
|
|
@@ -13764,7 +13797,7 @@ var WeaveTextNode = class extends WeaveNode {
|
|
|
13764
13797
|
delete cleanedAttrs.dragBoundFunc;
|
|
13765
13798
|
if (cleanedAttrs.link) {
|
|
13766
13799
|
if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
|
|
13767
|
-
|
|
13800
|
+
cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
|
|
13768
13801
|
}
|
|
13769
13802
|
return {
|
|
13770
13803
|
key: attrs.id ?? "",
|
|
@@ -14400,6 +14433,31 @@ var WeaveTextNode = class extends WeaveNode {
|
|
|
14400
14433
|
};
|
|
14401
14434
|
//#endregion
|
|
14402
14435
|
//#region src/nodes/text-v2/model.ts
|
|
14436
|
+
const MIN_LINE_HEIGHT = .5;
|
|
14437
|
+
/** Valid `lineHeight` mark of a run, or undefined (use the node value). */
|
|
14438
|
+
function runLineHeight(m) {
|
|
14439
|
+
const v = m?.lineHeight;
|
|
14440
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(4, Math.max(MIN_LINE_HEIGHT, v)) : void 0;
|
|
14441
|
+
}
|
|
14442
|
+
/** Only absolute http(s) URLs are links (same rule as the legacy node). */
|
|
14443
|
+
function isSafeLink(value) {
|
|
14444
|
+
if (typeof value !== "string") return false;
|
|
14445
|
+
try {
|
|
14446
|
+
return ["http:", "https:"].includes(new URL(value).protocol);
|
|
14447
|
+
} catch {
|
|
14448
|
+
return false;
|
|
14449
|
+
}
|
|
14450
|
+
}
|
|
14451
|
+
/** Block fields that describe structure (not content). */
|
|
14452
|
+
function blockFields(block, withStart) {
|
|
14453
|
+
const out = {};
|
|
14454
|
+
if (!block) return out;
|
|
14455
|
+
if (block.kind) out.kind = block.kind;
|
|
14456
|
+
if (block.depth) out.depth = block.depth;
|
|
14457
|
+
if (block.align) out.align = block.align;
|
|
14458
|
+
if (withStart && block.start !== void 0) out.start = block.start;
|
|
14459
|
+
return out;
|
|
14460
|
+
}
|
|
14403
14461
|
function isEmptyMarks(m) {
|
|
14404
14462
|
return !m || Object.keys(m).length === 0;
|
|
14405
14463
|
}
|
|
@@ -14480,7 +14538,7 @@ function marksAt(runs, offset) {
|
|
|
14480
14538
|
* blocks) with plain `insert`. Marks on untouched text are preserved; the
|
|
14481
14539
|
* inserted text inherits marks of the character before `start`.
|
|
14482
14540
|
*/
|
|
14483
|
-
function replaceRange(blocks, start, end, insert) {
|
|
14541
|
+
function replaceRange(blocks, start, end, insert, insertMarks) {
|
|
14484
14542
|
const total = blocksToText(blocks).length;
|
|
14485
14543
|
start = Math.max(0, Math.min(start, total));
|
|
14486
14544
|
end = Math.max(start, Math.min(end, total));
|
|
@@ -14489,6 +14547,7 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14489
14547
|
const prefixBlocks = [];
|
|
14490
14548
|
const suffixBlocks = [];
|
|
14491
14549
|
let inheritedMarks;
|
|
14550
|
+
let startBlock;
|
|
14492
14551
|
let pos = 0;
|
|
14493
14552
|
blocks.forEach((block) => {
|
|
14494
14553
|
const len = blockText(block).length;
|
|
@@ -14501,11 +14560,13 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14501
14560
|
if (start >= bStart && start <= bEnd) {
|
|
14502
14561
|
const local = start - bStart;
|
|
14503
14562
|
before = sliceRuns(block.runs, 0, local);
|
|
14563
|
+
startBlock = block;
|
|
14504
14564
|
inheritedMarks = marksAt(block.runs, local);
|
|
14505
14565
|
}
|
|
14506
14566
|
if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
|
|
14507
14567
|
}
|
|
14508
14568
|
});
|
|
14569
|
+
if (insertMarks !== void 0) inheritedMarks = insertMarks ?? void 0;
|
|
14509
14570
|
const lines = insert.split("\n");
|
|
14510
14571
|
const mk = (t) => t === "" ? [] : [{
|
|
14511
14572
|
t,
|
|
@@ -14516,7 +14577,10 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14516
14577
|
let runs = mk(line);
|
|
14517
14578
|
if (idx === 0) runs = [...before, ...runs];
|
|
14518
14579
|
if (idx === lines.length - 1) runs = [...runs, ...after];
|
|
14519
|
-
middle.push({
|
|
14580
|
+
middle.push({
|
|
14581
|
+
...blockFields(startBlock, idx === 0),
|
|
14582
|
+
runs: normalizeRuns(runs)
|
|
14583
|
+
});
|
|
14520
14584
|
});
|
|
14521
14585
|
return [
|
|
14522
14586
|
...prefixBlocks,
|
|
@@ -14524,8 +14588,331 @@ function replaceRange(blocks, start, end, insert) {
|
|
|
14524
14588
|
...suffixBlocks
|
|
14525
14589
|
];
|
|
14526
14590
|
}
|
|
14591
|
+
function safeBase(base) {
|
|
14592
|
+
return Number.isFinite(base) && base > 0 ? base : 1;
|
|
14593
|
+
}
|
|
14594
|
+
/** px -> stored ratio (4 decimals). */
|
|
14595
|
+
function sizeToRatio(px, base) {
|
|
14596
|
+
return Math.round(px / safeBase(base) * 1e4) / 1e4;
|
|
14597
|
+
}
|
|
14598
|
+
/** stored ratio -> px (rounded to 0.1). Invalid ratios resolve to the base. */
|
|
14599
|
+
function ratioToSize(ratio, base) {
|
|
14600
|
+
const b = safeBase(base);
|
|
14601
|
+
return Math.round((typeof ratio === "number" && ratio > 0 ? ratio : 1) * b * 10) / 10;
|
|
14602
|
+
}
|
|
14603
|
+
/** Effective ratio of a run (invalid or missing -> 1). */
|
|
14604
|
+
function runRatio(m) {
|
|
14605
|
+
const r = m?.size;
|
|
14606
|
+
return typeof r === "number" && Number.isFinite(r) && r > 0 ? r : 1;
|
|
14607
|
+
}
|
|
14608
|
+
function normalizeMarks(m) {
|
|
14609
|
+
const out = {};
|
|
14610
|
+
for (const [k, v] of Object.entries(m)) {
|
|
14611
|
+
if (v === void 0 || v === false || v === null || v === "") continue;
|
|
14612
|
+
if (k === "size" && v === 1) continue;
|
|
14613
|
+
out[k] = v;
|
|
14614
|
+
}
|
|
14615
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
14616
|
+
}
|
|
14617
|
+
/** Normalized marks of a run, or undefined when empty. */
|
|
14618
|
+
function cleanMarks(m) {
|
|
14619
|
+
return m ? normalizeMarks(m) : void 0;
|
|
14620
|
+
}
|
|
14621
|
+
function blockSpans(blocks) {
|
|
14622
|
+
const out = [];
|
|
14623
|
+
let pos = 0;
|
|
14624
|
+
blocks.forEach((b, index) => {
|
|
14625
|
+
const len = blockText(b).length;
|
|
14626
|
+
out.push({
|
|
14627
|
+
index,
|
|
14628
|
+
start: pos,
|
|
14629
|
+
end: pos + len
|
|
14630
|
+
});
|
|
14631
|
+
pos += len + 1;
|
|
14632
|
+
});
|
|
14633
|
+
return out;
|
|
14634
|
+
}
|
|
14635
|
+
/** Indices of blocks touched by [start, end] (collapsed range -> its block). */
|
|
14636
|
+
function blockIndicesInRange(blocks, start, end) {
|
|
14637
|
+
const out = [];
|
|
14638
|
+
for (const s of blockSpans(blocks)) if (s.end >= start && s.start <= end) {
|
|
14639
|
+
if (end > start && s.start === end && s.start !== start) continue;
|
|
14640
|
+
out.push(s.index);
|
|
14641
|
+
}
|
|
14642
|
+
return out;
|
|
14643
|
+
}
|
|
14644
|
+
/** Marks that typing at a collapsed offset would inherit. */
|
|
14645
|
+
function marksAtOffset(blocks, offset) {
|
|
14646
|
+
for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) return cleanMarks(marksAt(blocks[s.index].runs, offset - s.start));
|
|
14647
|
+
}
|
|
14648
|
+
function mapRunsInRange(block, from, to, fn) {
|
|
14649
|
+
const runs = [];
|
|
14650
|
+
let pos = 0;
|
|
14651
|
+
for (const run of block.runs) {
|
|
14652
|
+
const rs = pos;
|
|
14653
|
+
const re = pos + run.t.length;
|
|
14654
|
+
pos = re;
|
|
14655
|
+
const s = Math.max(from, rs);
|
|
14656
|
+
const e = Math.min(to, re);
|
|
14657
|
+
if (s >= e) {
|
|
14658
|
+
runs.push(run);
|
|
14659
|
+
continue;
|
|
14660
|
+
}
|
|
14661
|
+
if (s > rs) runs.push({
|
|
14662
|
+
t: run.t.slice(0, s - rs),
|
|
14663
|
+
m: run.m
|
|
14664
|
+
});
|
|
14665
|
+
runs.push({
|
|
14666
|
+
t: run.t.slice(s - rs, e - rs),
|
|
14667
|
+
m: fn(run.m)
|
|
14668
|
+
});
|
|
14669
|
+
if (e < re) runs.push({
|
|
14670
|
+
t: run.t.slice(e - rs),
|
|
14671
|
+
m: run.m
|
|
14672
|
+
});
|
|
14673
|
+
}
|
|
14674
|
+
return {
|
|
14675
|
+
...block,
|
|
14676
|
+
runs: normalizeRuns(runs)
|
|
14677
|
+
};
|
|
14678
|
+
}
|
|
14679
|
+
/** Set (or remove with `undefined`) one mark over the global range. */
|
|
14680
|
+
function applyMark(blocks, start, end, key, value) {
|
|
14681
|
+
if (start >= end) return blocks;
|
|
14682
|
+
if (key === "lineHeight" && value !== void 0) {
|
|
14683
|
+
const lh = runLineHeight({ lineHeight: value });
|
|
14684
|
+
if (lh === void 0) return blocks;
|
|
14685
|
+
value = lh;
|
|
14686
|
+
}
|
|
14687
|
+
const spans = blockSpans(blocks);
|
|
14688
|
+
return blocks.map((block, i) => {
|
|
14689
|
+
const s = spans[i];
|
|
14690
|
+
if (s.end <= start || s.start >= end) return block;
|
|
14691
|
+
return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
|
|
14692
|
+
...m,
|
|
14693
|
+
[key]: value
|
|
14694
|
+
}));
|
|
14695
|
+
});
|
|
14696
|
+
}
|
|
14697
|
+
/** Query one mark over a range; collapsed ranges report the caret marks. */
|
|
14698
|
+
function queryMark(blocks, start, end, key, stored) {
|
|
14699
|
+
const values = [];
|
|
14700
|
+
if (start >= end) {
|
|
14701
|
+
const v = (stored ?? marksAtOffset(blocks, start))?.[key];
|
|
14702
|
+
return v === void 0 || v === false ? { state: "none" } : {
|
|
14703
|
+
state: "all",
|
|
14704
|
+
value: v
|
|
14705
|
+
};
|
|
14706
|
+
}
|
|
14707
|
+
const spans = blockSpans(blocks);
|
|
14708
|
+
blocks.forEach((block, i) => {
|
|
14709
|
+
const s = spans[i];
|
|
14710
|
+
if (s.end <= start || s.start >= end) return;
|
|
14711
|
+
const from = Math.max(start, s.start) - s.start;
|
|
14712
|
+
const to = Math.min(end, s.end) - s.start;
|
|
14713
|
+
for (const r of sliceRuns(block.runs, from, to)) {
|
|
14714
|
+
const v = r.m?.[key];
|
|
14715
|
+
values.push(v === false ? void 0 : v);
|
|
14716
|
+
}
|
|
14717
|
+
});
|
|
14718
|
+
if (values.length === 0) return { state: "none" };
|
|
14719
|
+
const set = values.filter((v) => v !== void 0);
|
|
14720
|
+
if (set.length === 0) return { state: "none" };
|
|
14721
|
+
if (set.length < values.length) return { state: "some" };
|
|
14722
|
+
return values.every((v) => v === values[0]) ? {
|
|
14723
|
+
state: "all",
|
|
14724
|
+
value: values[0]
|
|
14725
|
+
} : { state: "some" };
|
|
14726
|
+
}
|
|
14727
|
+
/** Toggle a boolean mark: removes when fully set, sets otherwise. */
|
|
14728
|
+
function toggleMark(blocks, start, end, key) {
|
|
14729
|
+
return applyMark(blocks, start, end, key, queryMark(blocks, start, end, key).state === "all" ? void 0 : true);
|
|
14730
|
+
}
|
|
14731
|
+
function mapBlocks(blocks, indices, fn) {
|
|
14732
|
+
const set = new Set(indices);
|
|
14733
|
+
return blocks.map((b, i) => set.has(i) ? fn(b) : b);
|
|
14734
|
+
}
|
|
14735
|
+
function stripList(b) {
|
|
14736
|
+
const { kind: _k, depth: _d, start: _s, ...rest } = b;
|
|
14737
|
+
return rest;
|
|
14738
|
+
}
|
|
14739
|
+
/** Toggle list `kind` on the blocks of a range (all already that kind -> off). */
|
|
14740
|
+
function toggleList(blocks, start, end, kind) {
|
|
14741
|
+
const idx = blockIndicesInRange(blocks, start, end);
|
|
14742
|
+
const allSame = idx.every((i) => blocks[i].kind === kind);
|
|
14743
|
+
return mapBlocks(blocks, idx, (b) => allSame ? stripList(b) : {
|
|
14744
|
+
...b,
|
|
14745
|
+
kind
|
|
14746
|
+
});
|
|
14747
|
+
}
|
|
14748
|
+
/** Set (or clear with `undefined`) the alignment of the blocks in a range. */
|
|
14749
|
+
function setBlockAlign(blocks, start, end, align) {
|
|
14750
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
|
|
14751
|
+
if (align === void 0) {
|
|
14752
|
+
const { align: _a, ...rest } = b;
|
|
14753
|
+
return rest;
|
|
14754
|
+
}
|
|
14755
|
+
return {
|
|
14756
|
+
...b,
|
|
14757
|
+
align
|
|
14758
|
+
};
|
|
14759
|
+
});
|
|
14760
|
+
}
|
|
14761
|
+
/** Increase depth of list blocks in the range (non-list blocks untouched). */
|
|
14762
|
+
function indentBlocks(blocks, start, end) {
|
|
14763
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => b.kind ? {
|
|
14764
|
+
...b,
|
|
14765
|
+
depth: (b.depth ?? 0) + 1
|
|
14766
|
+
} : b);
|
|
14767
|
+
}
|
|
14768
|
+
/** Decrease depth; at depth 0 the list marker is removed. */
|
|
14769
|
+
function outdentBlocks(blocks, start, end) {
|
|
14770
|
+
return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
|
|
14771
|
+
if (!b.kind) return b;
|
|
14772
|
+
const d = b.depth ?? 0;
|
|
14773
|
+
if (d <= 0) return stripList(b);
|
|
14774
|
+
const next = {
|
|
14775
|
+
...b,
|
|
14776
|
+
depth: d - 1
|
|
14777
|
+
};
|
|
14778
|
+
if (d - 1 === 0) delete next.depth;
|
|
14779
|
+
return next;
|
|
14780
|
+
});
|
|
14781
|
+
}
|
|
14782
|
+
const BULLETS = [
|
|
14783
|
+
"•",
|
|
14784
|
+
"◦",
|
|
14785
|
+
"▪"
|
|
14786
|
+
];
|
|
14787
|
+
function alpha(n) {
|
|
14788
|
+
let s = "";
|
|
14789
|
+
let v = n;
|
|
14790
|
+
while (v > 0) {
|
|
14791
|
+
v--;
|
|
14792
|
+
s = String.fromCharCode(97 + v % 26) + s;
|
|
14793
|
+
v = Math.floor(v / 26);
|
|
14794
|
+
}
|
|
14795
|
+
return s;
|
|
14796
|
+
}
|
|
14797
|
+
function roman(n) {
|
|
14798
|
+
const map = [
|
|
14799
|
+
[1e3, "m"],
|
|
14800
|
+
[900, "cm"],
|
|
14801
|
+
[500, "d"],
|
|
14802
|
+
[400, "cd"],
|
|
14803
|
+
[100, "c"],
|
|
14804
|
+
[90, "xc"],
|
|
14805
|
+
[50, "l"],
|
|
14806
|
+
[40, "xl"],
|
|
14807
|
+
[10, "x"],
|
|
14808
|
+
[9, "ix"],
|
|
14809
|
+
[5, "v"],
|
|
14810
|
+
[4, "iv"],
|
|
14811
|
+
[1, "i"]
|
|
14812
|
+
];
|
|
14813
|
+
let v = n;
|
|
14814
|
+
let s = "";
|
|
14815
|
+
for (const [k, r] of map) while (v >= k) {
|
|
14816
|
+
s += r;
|
|
14817
|
+
v -= k;
|
|
14818
|
+
}
|
|
14819
|
+
return s;
|
|
14820
|
+
}
|
|
14821
|
+
/**
|
|
14822
|
+
* Marker text per block (null for plain paragraphs). Ordered numbering is
|
|
14823
|
+
* continuous across consecutive same-depth items and restarts after a
|
|
14824
|
+
* non-list block, a shallower item or a kind change; `start` seeds a run.
|
|
14825
|
+
* Markers are layout-only: they never enter `text` or offsets.
|
|
14826
|
+
*/
|
|
14827
|
+
function listMarkers(blocks) {
|
|
14828
|
+
const stack = [];
|
|
14829
|
+
return blocks.map((b) => {
|
|
14830
|
+
if (!b.kind) {
|
|
14831
|
+
stack.length = 0;
|
|
14832
|
+
return null;
|
|
14833
|
+
}
|
|
14834
|
+
const d = Math.max(0, Math.floor(b.depth ?? 0));
|
|
14835
|
+
stack.length = Math.min(stack.length, d + 1);
|
|
14836
|
+
while (stack.length < d) stack.push({
|
|
14837
|
+
kind: b.kind,
|
|
14838
|
+
n: 0
|
|
14839
|
+
});
|
|
14840
|
+
const cur = stack[d];
|
|
14841
|
+
if (!cur || cur.kind !== b.kind) stack[d] = {
|
|
14842
|
+
kind: b.kind,
|
|
14843
|
+
n: b.kind === "ol" ? Math.max(1, b.start ?? 1) : 0
|
|
14844
|
+
};
|
|
14845
|
+
else if (b.kind === "ol") cur.n += 1;
|
|
14846
|
+
if (b.kind === "ul") return BULLETS[d % 3];
|
|
14847
|
+
const n = stack[d].n;
|
|
14848
|
+
const style = d % 3;
|
|
14849
|
+
return `${style === 0 ? n : style === 1 ? alpha(n) : roman(n)}.`;
|
|
14850
|
+
});
|
|
14851
|
+
}
|
|
14852
|
+
/**
|
|
14853
|
+
* Format of the range for toolbars. A collapsed range reports the stored
|
|
14854
|
+
* marks (when given) or the marks typing would inherit.
|
|
14855
|
+
*/
|
|
14856
|
+
function getSelectionFormat(blocks, start, end, baseFontSize, stored) {
|
|
14857
|
+
const q = (key) => queryMark(blocks, start, end, key, stored);
|
|
14858
|
+
const ratios = [];
|
|
14859
|
+
if (start >= end) ratios.push(runRatio(stored ?? marksAtOffset(blocks, start)));
|
|
14860
|
+
else {
|
|
14861
|
+
const spans = blockSpans(blocks);
|
|
14862
|
+
blocks.forEach((block, i) => {
|
|
14863
|
+
const s = spans[i];
|
|
14864
|
+
if (s.end <= start || s.start >= end) return;
|
|
14865
|
+
const from = Math.max(start, s.start) - s.start;
|
|
14866
|
+
const to = Math.min(end, s.end) - s.start;
|
|
14867
|
+
for (const r of sliceRuns(block.runs, from, to)) ratios.push(runRatio(r.m));
|
|
14868
|
+
});
|
|
14869
|
+
}
|
|
14870
|
+
let size;
|
|
14871
|
+
if (ratios.length === 0 || ratios.every((r) => r === ratios[0])) {
|
|
14872
|
+
const r = ratios[0] ?? 1;
|
|
14873
|
+
size = {
|
|
14874
|
+
state: r === 1 ? "none" : "all",
|
|
14875
|
+
value: ratioToSize(r, baseFontSize)
|
|
14876
|
+
};
|
|
14877
|
+
} else size = { state: "some" };
|
|
14878
|
+
const idx = blockIndicesInRange(blocks, start, end);
|
|
14879
|
+
const kinds = new Set(idx.map((i) => blocks[i].kind ?? "none"));
|
|
14880
|
+
let list = { kind: "none" };
|
|
14881
|
+
if (kinds.size === 1) {
|
|
14882
|
+
const kind = [...kinds][0];
|
|
14883
|
+
const depths = new Set(idx.map((i) => blocks[i].depth ?? 0));
|
|
14884
|
+
list = kind === "none" ? { kind } : {
|
|
14885
|
+
kind,
|
|
14886
|
+
...depths.size === 1 ? { depth: [...depths][0] } : {}
|
|
14887
|
+
};
|
|
14888
|
+
} else if (kinds.size > 1) list = { kind: "mixed" };
|
|
14889
|
+
return {
|
|
14890
|
+
bold: q("bold"),
|
|
14891
|
+
italic: q("italic"),
|
|
14892
|
+
underline: q("underline"),
|
|
14893
|
+
strike: q("strike"),
|
|
14894
|
+
color: q("color"),
|
|
14895
|
+
family: q("family"),
|
|
14896
|
+
link: q("link"),
|
|
14897
|
+
lineHeight: q("lineHeight"),
|
|
14898
|
+
align: alignQuery(blocks, start, end),
|
|
14899
|
+
size,
|
|
14900
|
+
list
|
|
14901
|
+
};
|
|
14902
|
+
}
|
|
14903
|
+
function alignQuery(blocks, start, end) {
|
|
14904
|
+
const vals = blockIndicesInRange(blocks, start, end).map((i) => blocks[i].align);
|
|
14905
|
+
const set = vals.filter((v) => v !== void 0);
|
|
14906
|
+
if (set.length === 0) return { state: "none" };
|
|
14907
|
+
if (set.length < vals.length || set.some((v) => v !== set[0])) return { state: "some" };
|
|
14908
|
+
return {
|
|
14909
|
+
state: "all",
|
|
14910
|
+
value: set[0]
|
|
14911
|
+
};
|
|
14912
|
+
}
|
|
14527
14913
|
//#endregion
|
|
14528
14914
|
//#region src/nodes/text-v2/layout/engine.ts
|
|
14915
|
+
const DEFAULT_INDENT_STEP = 1.5;
|
|
14529
14916
|
/** Quote multi-word families like `Konva` does so the font shorthand parses. */
|
|
14530
14917
|
function normalizeFontFamily(fontFamily) {
|
|
14531
14918
|
return fontFamily.split(",").map((family) => {
|
|
@@ -14538,6 +14925,50 @@ function normalizeFontFamily(fontFamily) {
|
|
|
14538
14925
|
function fontString(s) {
|
|
14539
14926
|
return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
|
|
14540
14927
|
}
|
|
14928
|
+
/** Style of a run: node style overridden by the run marks. */
|
|
14929
|
+
function runStyle(style, m) {
|
|
14930
|
+
if (!m) return style;
|
|
14931
|
+
let fontStyle = style.fontStyle;
|
|
14932
|
+
if (m.bold || m.italic) {
|
|
14933
|
+
const tokens = new Set(fontStyle.split(/\s+/).filter(Boolean));
|
|
14934
|
+
tokens.delete("normal");
|
|
14935
|
+
if (m.italic) tokens.add("italic");
|
|
14936
|
+
if (m.bold) tokens.add("bold");
|
|
14937
|
+
fontStyle = tokens.size === 0 ? "normal" : Array.from(tokens).join(" ");
|
|
14938
|
+
}
|
|
14939
|
+
return {
|
|
14940
|
+
...style,
|
|
14941
|
+
fontStyle,
|
|
14942
|
+
fontSize: style.fontSize * runRatio(m),
|
|
14943
|
+
fontFamily: typeof m.family === "string" && m.family ? m.family : style.fontFamily
|
|
14944
|
+
};
|
|
14945
|
+
}
|
|
14946
|
+
/** Pure fallback (same constants as Konva.Text when the browser has none). */
|
|
14947
|
+
const approxMetrics = (_font, size) => ({
|
|
14948
|
+
ascent: .91 * size,
|
|
14949
|
+
descent: .21 * size
|
|
14950
|
+
});
|
|
14951
|
+
const metricsCache = /* @__PURE__ */ new Map();
|
|
14952
|
+
/** Ascent/descent used for baseline placement (parity with Konva.Text). */
|
|
14953
|
+
function fontMetrics(font, size) {
|
|
14954
|
+
if (Konva.legacyTextRendering) return {
|
|
14955
|
+
ascent: 0,
|
|
14956
|
+
descent: 0
|
|
14957
|
+
};
|
|
14958
|
+
const hit = metricsCache.get(font);
|
|
14959
|
+
if (hit) return hit;
|
|
14960
|
+
const ctx = Konva.Util.createCanvasElement().getContext("2d");
|
|
14961
|
+
ctx.font = font;
|
|
14962
|
+
const m = ctx.measureText("M");
|
|
14963
|
+
const scale = size / 100;
|
|
14964
|
+
const out = {
|
|
14965
|
+
ascent: m.fontBoundingBoxAscent ?? 91 * scale,
|
|
14966
|
+
descent: m.fontBoundingBoxDescent ?? 21 * scale
|
|
14967
|
+
};
|
|
14968
|
+
if (metricsCache.size > 2e3) metricsCache.clear();
|
|
14969
|
+
metricsCache.set(font, out);
|
|
14970
|
+
return out;
|
|
14971
|
+
}
|
|
14541
14972
|
let sharedContext = null;
|
|
14542
14973
|
/** Default measurer: works on browser, node-canvas and skia-canvas backends. */
|
|
14543
14974
|
const canvasMeasurer = (font, text) => {
|
|
@@ -14585,11 +15016,15 @@ function wordWrapIndex(graphemes, start, matchLen, matchWidth, maxWidth) {
|
|
|
14585
15016
|
const seg = graphemes.slice(start, start + matchLen);
|
|
14586
15017
|
return Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
|
|
14587
15018
|
}
|
|
14588
|
-
function applyAlign(lines,
|
|
14589
|
-
if (align === "left") return;
|
|
15019
|
+
function applyAlign(lines, width, padding) {
|
|
14590
15020
|
for (const line of lines) {
|
|
14591
|
-
const
|
|
14592
|
-
|
|
15021
|
+
const align = line.align;
|
|
15022
|
+
if (align === "left") {
|
|
15023
|
+
line.offsetX = line.indent;
|
|
15024
|
+
continue;
|
|
15025
|
+
}
|
|
15026
|
+
const free = width - line.indent - line.width - padding * 2;
|
|
15027
|
+
line.offsetX = line.indent + (align === "center" ? free / 2 : free);
|
|
14593
15028
|
}
|
|
14594
15029
|
}
|
|
14595
15030
|
var TextLayoutEngine = class {
|
|
@@ -14599,8 +15034,10 @@ var TextLayoutEngine = class {
|
|
|
14599
15034
|
/** Entries by signature, so paragraphs that shifted index are reused. */
|
|
14600
15035
|
pool = /* @__PURE__ */ new Map();
|
|
14601
15036
|
lastParagraphsRelaid = 0;
|
|
14602
|
-
|
|
15037
|
+
metrics;
|
|
15038
|
+
constructor(measurer = canvasMeasurer, metrics = approxMetrics) {
|
|
14603
15039
|
this.measure = measurer;
|
|
15040
|
+
this.metrics = metrics;
|
|
14604
15041
|
}
|
|
14605
15042
|
/** Number of paragraphs recomputed by the last `layout()` call. */
|
|
14606
15043
|
getLastRelaidCount() {
|
|
@@ -14611,10 +15048,17 @@ var TextLayoutEngine = class {
|
|
|
14611
15048
|
this.paragraphs = [];
|
|
14612
15049
|
this.pool.clear();
|
|
14613
15050
|
}
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
15051
|
+
/** Text indent of a block in px (0 for plain paragraphs). */
|
|
15052
|
+
indentFor(block, style, fixedWidth, maxWidth) {
|
|
15053
|
+
if (!block.kind) return 0;
|
|
15054
|
+
const stepPx = (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
|
|
15055
|
+
const wanted = (Math.max(0, Math.floor(block.depth ?? 0)) + 1) * stepPx;
|
|
15056
|
+
if (!fixedWidth) return wanted;
|
|
15057
|
+
const limit = Math.max(0, maxWidth - 4 * style.fontSize);
|
|
15058
|
+
return Math.min(wanted, limit);
|
|
15059
|
+
}
|
|
15060
|
+
sigFor(block, style, fixedWidth, maxWidth, indent) {
|
|
15061
|
+
return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
|
|
14618
15062
|
}
|
|
14619
15063
|
/**
|
|
14620
15064
|
* Pre-compute paragraph layouts for `blocks` within a time budget so a
|
|
@@ -14622,14 +15066,14 @@ var TextLayoutEngine = class {
|
|
|
14622
15066
|
* Lets callers time-slice very large content (e.g. bulk paste).
|
|
14623
15067
|
*/
|
|
14624
15068
|
warm(blocks, style, bounds, budgetMs) {
|
|
14625
|
-
const
|
|
14626
|
-
const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
|
|
15069
|
+
const fixedWidth = isNum(bounds.width);
|
|
14627
15070
|
const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
|
|
14628
15071
|
const deadline = now() + budgetMs;
|
|
14629
15072
|
for (const block of blocks) {
|
|
14630
|
-
const
|
|
15073
|
+
const indent = this.indentFor(block, style, fixedWidth, maxWidth);
|
|
15074
|
+
const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
|
|
14631
15075
|
if (this.pool.has(sig)) continue;
|
|
14632
|
-
this.pool.set(sig, this.layoutParagraph(block, sig,
|
|
15076
|
+
this.pool.set(sig, this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent));
|
|
14633
15077
|
if (now() >= deadline) return false;
|
|
14634
15078
|
}
|
|
14635
15079
|
return true;
|
|
@@ -14647,35 +15091,38 @@ var TextLayoutEngine = class {
|
|
|
14647
15091
|
return w + text.length * spacing;
|
|
14648
15092
|
}
|
|
14649
15093
|
layout(blocks, style, bounds) {
|
|
14650
|
-
const font = fontString(style);
|
|
14651
15094
|
const padding = bounds.padding;
|
|
14652
15095
|
const fixedWidth = isNum(bounds.width);
|
|
14653
15096
|
const fixedHeight = isNum(bounds.height);
|
|
14654
15097
|
const maxWidth = (bounds.width ?? 0) - padding * 2;
|
|
14655
15098
|
const maxHeightPx = (bounds.height ?? 0) - padding * 2;
|
|
14656
15099
|
const lineHeightPx = style.lineHeight * style.fontSize;
|
|
14657
|
-
const
|
|
15100
|
+
const markers = listMarkers(blocks);
|
|
14658
15101
|
const prev = this.paragraphs;
|
|
14659
15102
|
const next = [];
|
|
14660
15103
|
let relaid = 0;
|
|
14661
15104
|
const lines = [];
|
|
14662
15105
|
let textWidth = 0;
|
|
14663
15106
|
let offset = 0;
|
|
15107
|
+
let contentHeight = 0;
|
|
14664
15108
|
for (let p = 0; p < blocks.length; p++) {
|
|
14665
15109
|
const block = blocks[p];
|
|
14666
|
-
const
|
|
15110
|
+
const indent = this.indentFor(block, style, fixedWidth, maxWidth);
|
|
15111
|
+
const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
|
|
14667
15112
|
let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
|
|
14668
15113
|
if (!entry) {
|
|
14669
|
-
entry = this.layoutParagraph(block, sig,
|
|
15114
|
+
entry = this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent);
|
|
14670
15115
|
relaid++;
|
|
14671
15116
|
}
|
|
14672
15117
|
next.push(entry);
|
|
14673
15118
|
const emitted = this.emitLines(entry, block, p, offset, lines, {
|
|
14674
|
-
font,
|
|
14675
15119
|
style,
|
|
14676
|
-
|
|
15120
|
+
indent,
|
|
15121
|
+
marker: markers[p],
|
|
15122
|
+
y: contentHeight,
|
|
14677
15123
|
maxHeightPx: fixedHeight ? maxHeightPx : void 0
|
|
14678
15124
|
});
|
|
15125
|
+
contentHeight = emitted.y;
|
|
14679
15126
|
textWidth = Math.max(textWidth, emitted.maxWidth);
|
|
14680
15127
|
if (emitted.stopped) break;
|
|
14681
15128
|
offset += blockText(block).length + 1;
|
|
@@ -14684,25 +15131,42 @@ var TextLayoutEngine = class {
|
|
|
14684
15131
|
this.pool = new Map(next.map((e) => [e.sig, e]));
|
|
14685
15132
|
this.lastParagraphsRelaid = relaid;
|
|
14686
15133
|
const width = fixedWidth ? bounds.width : textWidth + padding * 2;
|
|
14687
|
-
const height = fixedHeight ? bounds.height :
|
|
14688
|
-
applyAlign(lines,
|
|
15134
|
+
const height = fixedHeight ? bounds.height : contentHeight + padding * 2;
|
|
15135
|
+
applyAlign(lines, width, padding);
|
|
14689
15136
|
return {
|
|
14690
15137
|
lines,
|
|
14691
15138
|
textWidth,
|
|
14692
15139
|
textHeight: style.fontSize,
|
|
14693
15140
|
lineHeightPx,
|
|
15141
|
+
contentHeight,
|
|
14694
15142
|
width,
|
|
14695
15143
|
height
|
|
14696
15144
|
};
|
|
14697
15145
|
}
|
|
14698
15146
|
/** Append a paragraph's lines to `lines`; stops once a fixed height is full. */
|
|
14699
15147
|
emitLines(entry, block, paragraph, offset, lines, opts) {
|
|
14700
|
-
const {
|
|
15148
|
+
const { style, indent, maxHeightPx } = opts;
|
|
15149
|
+
let { y } = opts;
|
|
14701
15150
|
let maxWidth = 0;
|
|
15151
|
+
let first = true;
|
|
14702
15152
|
for (const pl of entry.lines) {
|
|
14703
15153
|
const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
|
|
14704
|
-
const segments = this.buildSegments(entry, block, pl,
|
|
15154
|
+
const segments = this.buildSegments(entry, block, pl, style);
|
|
14705
15155
|
const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
|
|
15156
|
+
let marker;
|
|
15157
|
+
if (first && opts.marker) {
|
|
15158
|
+
const m0 = block.runs[0]?.m;
|
|
15159
|
+
const rs = runStyle(style, m0);
|
|
15160
|
+
const stepPx = Math.min(indent, this.stepPx(style));
|
|
15161
|
+
marker = {
|
|
15162
|
+
text: opts.marker,
|
|
15163
|
+
x: Math.max(0, indent - stepPx),
|
|
15164
|
+
font: fontString(rs),
|
|
15165
|
+
fontSize: rs.fontSize,
|
|
15166
|
+
color: typeof m0?.color === "string" ? m0.color : void 0
|
|
15167
|
+
};
|
|
15168
|
+
}
|
|
15169
|
+
first = false;
|
|
14706
15170
|
lines.push({
|
|
14707
15171
|
paragraph,
|
|
14708
15172
|
start: offset + startChar,
|
|
@@ -14710,30 +15174,41 @@ var TextLayoutEngine = class {
|
|
|
14710
15174
|
text,
|
|
14711
15175
|
width: segments.reduce((a, s) => a + s.width, 0),
|
|
14712
15176
|
measuredWidth: pl.measuredWidth,
|
|
14713
|
-
offsetX:
|
|
14714
|
-
y
|
|
15177
|
+
offsetX: indent,
|
|
15178
|
+
y,
|
|
15179
|
+
height: pl.height,
|
|
15180
|
+
baseline: pl.baseline,
|
|
15181
|
+
indent,
|
|
15182
|
+
marker,
|
|
15183
|
+
align: block.align ?? style.align,
|
|
14715
15184
|
segments,
|
|
14716
15185
|
lastInParagraph: pl.lastInParagraph
|
|
14717
15186
|
});
|
|
14718
|
-
|
|
14719
|
-
|
|
15187
|
+
y += pl.height;
|
|
15188
|
+
maxWidth = Math.max(maxWidth, pl.measuredWidth + indent);
|
|
15189
|
+
if (maxHeightPx !== void 0 && y + pl.height > maxHeightPx) return {
|
|
14720
15190
|
stopped: true,
|
|
14721
|
-
maxWidth
|
|
15191
|
+
maxWidth,
|
|
15192
|
+
y
|
|
14722
15193
|
};
|
|
14723
15194
|
}
|
|
14724
15195
|
return {
|
|
14725
15196
|
stopped: false,
|
|
14726
|
-
maxWidth
|
|
15197
|
+
maxWidth,
|
|
15198
|
+
y
|
|
14727
15199
|
};
|
|
14728
15200
|
}
|
|
14729
|
-
|
|
15201
|
+
stepPx(style) {
|
|
15202
|
+
return (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
|
|
15203
|
+
}
|
|
15204
|
+
rangeWidth(entry, from, to, spacing) {
|
|
14730
15205
|
let width = 0;
|
|
14731
15206
|
let i = from;
|
|
14732
15207
|
while (i < to) {
|
|
14733
15208
|
const run = entry.runOf[i];
|
|
14734
15209
|
let j = i;
|
|
14735
15210
|
while (j < to && entry.runOf[j] === run) j++;
|
|
14736
|
-
width += this.measureChunk(
|
|
15211
|
+
width += this.measureChunk(entry.fonts[run], entry.graphemes.slice(i, j).join(""), spacing);
|
|
14737
15212
|
i = j;
|
|
14738
15213
|
}
|
|
14739
15214
|
return width;
|
|
@@ -14769,17 +15244,58 @@ var TextLayoutEngine = class {
|
|
|
14769
15244
|
matchWidth
|
|
14770
15245
|
};
|
|
14771
15246
|
}
|
|
14772
|
-
layoutParagraph(block, sig,
|
|
15247
|
+
layoutParagraph(block, sig, style, fixedWidth, maxWidth) {
|
|
15248
|
+
const entry = this.wrapParagraph(block, sig, style, fixedWidth, maxWidth);
|
|
15249
|
+
this.measureLines(entry, style);
|
|
15250
|
+
return entry;
|
|
15251
|
+
}
|
|
15252
|
+
/** Per-line box height and baseline from the runs on each line. */
|
|
15253
|
+
measureLines(entry, style) {
|
|
15254
|
+
const baseFont = fontString(style);
|
|
15255
|
+
for (const line of entry.lines) {
|
|
15256
|
+
let size = 0;
|
|
15257
|
+
let height = 0;
|
|
15258
|
+
let asc = 0;
|
|
15259
|
+
let desc = 0;
|
|
15260
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15261
|
+
for (let g = line.startG; g < line.endG; g++) {
|
|
15262
|
+
const r = entry.runOf[g];
|
|
15263
|
+
if (seen.has(r)) continue;
|
|
15264
|
+
seen.add(r);
|
|
15265
|
+
size = Math.max(size, entry.sizes[r]);
|
|
15266
|
+
height = Math.max(height, entry.lineHeights[r] * entry.sizes[r]);
|
|
15267
|
+
const m = this.metrics(entry.fonts[r], entry.sizes[r]);
|
|
15268
|
+
asc = Math.max(asc, m.ascent);
|
|
15269
|
+
desc = Math.max(desc, m.descent);
|
|
15270
|
+
}
|
|
15271
|
+
if (seen.size === 0) {
|
|
15272
|
+
size = style.fontSize;
|
|
15273
|
+
height = style.lineHeight * size;
|
|
15274
|
+
const m = this.metrics(baseFont, style.fontSize);
|
|
15275
|
+
asc = m.ascent;
|
|
15276
|
+
desc = m.descent;
|
|
15277
|
+
}
|
|
15278
|
+
line.height = height;
|
|
15279
|
+
line.baseline = (asc - desc) / 2 + line.height / 2;
|
|
15280
|
+
}
|
|
15281
|
+
}
|
|
15282
|
+
wrapParagraph(block, sig, style, fixedWidth, maxWidth) {
|
|
14773
15283
|
const { graphemes, runOf } = flattenBlock(block);
|
|
15284
|
+
const baseFont = fontString(style);
|
|
15285
|
+
const fonts = block.runs.map((r) => r.m ? fontString(runStyle(style, r.m)) : baseFont);
|
|
15286
|
+
const sizes = block.runs.map((r) => style.fontSize * runRatio(r.m));
|
|
14774
15287
|
const entry = {
|
|
14775
15288
|
sig,
|
|
14776
15289
|
graphemes,
|
|
14777
15290
|
runOf,
|
|
15291
|
+
fonts,
|
|
15292
|
+
lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
|
|
15293
|
+
sizes,
|
|
14778
15294
|
lines: []
|
|
14779
15295
|
};
|
|
14780
15296
|
const n = graphemes.length;
|
|
14781
15297
|
const spacing = style.letterSpacing;
|
|
14782
|
-
const width = (a, b) => this.rangeWidth(entry, a, b,
|
|
15298
|
+
const width = (a, b) => this.rangeWidth(entry, a, b, spacing);
|
|
14783
15299
|
const shouldWrap = style.wrap !== "none";
|
|
14784
15300
|
const wrapAtWord = style.wrap !== "char" && shouldWrap;
|
|
14785
15301
|
const whole = width(0, n);
|
|
@@ -14788,7 +15304,9 @@ var TextLayoutEngine = class {
|
|
|
14788
15304
|
startG: 0,
|
|
14789
15305
|
endG: n,
|
|
14790
15306
|
measuredWidth: whole,
|
|
14791
|
-
lastInParagraph: true
|
|
15307
|
+
lastInParagraph: true,
|
|
15308
|
+
height: 0,
|
|
15309
|
+
baseline: 0
|
|
14792
15310
|
});
|
|
14793
15311
|
return entry;
|
|
14794
15312
|
}
|
|
@@ -14800,7 +15318,9 @@ var TextLayoutEngine = class {
|
|
|
14800
15318
|
startG: start,
|
|
14801
15319
|
endG: n,
|
|
14802
15320
|
measuredWidth: matchWidth,
|
|
14803
|
-
lastInParagraph: true
|
|
15321
|
+
lastInParagraph: true,
|
|
15322
|
+
height: 0,
|
|
15323
|
+
baseline: 0
|
|
14804
15324
|
});
|
|
14805
15325
|
return entry;
|
|
14806
15326
|
}
|
|
@@ -14816,7 +15336,9 @@ var TextLayoutEngine = class {
|
|
|
14816
15336
|
startG: start,
|
|
14817
15337
|
endG: trimTrailingSpace(graphemes, start, end),
|
|
14818
15338
|
measuredWidth: matchWidth,
|
|
14819
|
-
lastInParagraph: false
|
|
15339
|
+
lastInParagraph: false,
|
|
15340
|
+
height: 0,
|
|
15341
|
+
baseline: 0
|
|
14820
15342
|
});
|
|
14821
15343
|
start = skipSpace(graphemes, end);
|
|
14822
15344
|
}
|
|
@@ -14824,7 +15346,7 @@ var TextLayoutEngine = class {
|
|
|
14824
15346
|
if (last) last.lastInParagraph = true;
|
|
14825
15347
|
return entry;
|
|
14826
15348
|
}
|
|
14827
|
-
buildSegments(entry, block, pl,
|
|
15349
|
+
buildSegments(entry, block, pl, style) {
|
|
14828
15350
|
const segments = [];
|
|
14829
15351
|
let x = 0;
|
|
14830
15352
|
let i = pl.startG;
|
|
@@ -14833,12 +15355,16 @@ var TextLayoutEngine = class {
|
|
|
14833
15355
|
let j = i;
|
|
14834
15356
|
while (j < pl.endG && entry.runOf[j] === run) j++;
|
|
14835
15357
|
const t = entry.graphemes.slice(i, j).join("");
|
|
15358
|
+
const font = entry.fonts[run];
|
|
14836
15359
|
const width = this.measureChunk(font, t, style.letterSpacing);
|
|
14837
15360
|
segments.push({
|
|
14838
15361
|
t,
|
|
14839
15362
|
m: block.runs[run].m,
|
|
15363
|
+
run,
|
|
14840
15364
|
x,
|
|
14841
|
-
width
|
|
15365
|
+
width,
|
|
15366
|
+
font,
|
|
15367
|
+
fontSize: entry.sizes[run]
|
|
14842
15368
|
});
|
|
14843
15369
|
x += width;
|
|
14844
15370
|
i = j;
|
|
@@ -14870,12 +15396,18 @@ const LAYOUT_ATTRS = [
|
|
|
14870
15396
|
* code written against `Konva.Text` keeps working.
|
|
14871
15397
|
*/
|
|
14872
15398
|
var WeaveTextV2Shape = class extends Konva.Shape {
|
|
14873
|
-
engine = new TextLayoutEngine();
|
|
14874
|
-
naturalEngine = new TextLayoutEngine();
|
|
15399
|
+
engine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
|
|
15400
|
+
naturalEngine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
|
|
14875
15401
|
layoutCache = null;
|
|
14876
15402
|
blocksKey = {};
|
|
14877
15403
|
blocksCache = [];
|
|
14878
15404
|
viewport = null;
|
|
15405
|
+
indentStepValue;
|
|
15406
|
+
linkStyle = {
|
|
15407
|
+
color: "#1155ccff",
|
|
15408
|
+
hoverColor: "#3d7be0ff"
|
|
15409
|
+
};
|
|
15410
|
+
hoveredRun = null;
|
|
14879
15411
|
constructor(config) {
|
|
14880
15412
|
super(config);
|
|
14881
15413
|
this.className = "WeaveTextV2Shape";
|
|
@@ -14892,6 +15424,55 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14892
15424
|
this._setAttr("text", value == null ? "" : String(value));
|
|
14893
15425
|
return this;
|
|
14894
15426
|
}
|
|
15427
|
+
/**
|
|
15428
|
+
* List indent per depth, as a multiple of `fontSize`. Not a node attr: it
|
|
15429
|
+
* comes from the node config and must not be serialized into the document.
|
|
15430
|
+
*/
|
|
15431
|
+
setIndentStep(step) {
|
|
15432
|
+
if (this.indentStepValue !== step) {
|
|
15433
|
+
this.indentStepValue = step;
|
|
15434
|
+
this.layoutCache = null;
|
|
15435
|
+
}
|
|
15436
|
+
return this;
|
|
15437
|
+
}
|
|
15438
|
+
/** Link run colours (from the node config; not serialized). */
|
|
15439
|
+
setLinkStyle(style) {
|
|
15440
|
+
this.linkStyle = style;
|
|
15441
|
+
return this;
|
|
15442
|
+
}
|
|
15443
|
+
/** Run under the pointer (`paragraph:run`), repainted with the hover colour. */
|
|
15444
|
+
setHoveredRun(key) {
|
|
15445
|
+
if (this.hoveredRun !== key) {
|
|
15446
|
+
this.hoveredRun = key;
|
|
15447
|
+
this.getLayer()?.batchDraw();
|
|
15448
|
+
}
|
|
15449
|
+
return this;
|
|
15450
|
+
}
|
|
15451
|
+
getHoveredRun() {
|
|
15452
|
+
return this.hoveredRun;
|
|
15453
|
+
}
|
|
15454
|
+
/**
|
|
15455
|
+
* Link run at a point in node-local coordinates (the legacy node-level
|
|
15456
|
+
* `link` wins: runs are then not links).
|
|
15457
|
+
*/
|
|
15458
|
+
linkAtPoint(point) {
|
|
15459
|
+
if (this.attrs.link) return null;
|
|
15460
|
+
const layout = this.ensureLayout();
|
|
15461
|
+
const o = this.getContentOffset();
|
|
15462
|
+
const x = point.x - o.x;
|
|
15463
|
+
const y = point.y - o.y;
|
|
15464
|
+
for (const line of layout.lines) {
|
|
15465
|
+
if (y < line.y || y >= line.y + line.height) continue;
|
|
15466
|
+
for (const seg of line.segments) {
|
|
15467
|
+
const sx = line.offsetX + seg.x;
|
|
15468
|
+
if (x >= sx && x <= sx + seg.width && isSafeLink(seg.m?.link)) return {
|
|
15469
|
+
key: `${line.paragraph}:${seg.run}`,
|
|
15470
|
+
url: seg.m.link
|
|
15471
|
+
};
|
|
15472
|
+
}
|
|
15473
|
+
}
|
|
15474
|
+
return null;
|
|
15475
|
+
}
|
|
14895
15476
|
/** Resolved blocks: stored or lazily synthesized (never written back). */
|
|
14896
15477
|
getBlocks() {
|
|
14897
15478
|
const k = this.blocksKey;
|
|
@@ -14912,6 +15493,10 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14912
15493
|
}
|
|
14913
15494
|
/** Replace content with blocks, keeping the `text` mirror in sync. */
|
|
14914
15495
|
setBlocks(blocks) {
|
|
15496
|
+
if (!Array.isArray(blocks)) {
|
|
15497
|
+
this._setAttr("blocks", void 0);
|
|
15498
|
+
return this;
|
|
15499
|
+
}
|
|
14915
15500
|
this._setAttr("blocks", blocks);
|
|
14916
15501
|
this._setAttr("text", blocksToText(blocks));
|
|
14917
15502
|
return this;
|
|
@@ -14949,6 +15534,16 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14949
15534
|
textDecoration(v) {
|
|
14950
15535
|
return this.accessor("textDecoration", "", v);
|
|
14951
15536
|
}
|
|
15537
|
+
/**
|
|
15538
|
+
* Always stroke under the node transform. Weave turns `strokeScaleEnabled`
|
|
15539
|
+
* off while a node is transformed; Konva then resets the canvas transform
|
|
15540
|
+
* before stroking, which only works for path shapes. `strokeText` is not a
|
|
15541
|
+
* path, so the outline was painted at local coordinates on the canvas
|
|
15542
|
+
* origin at stage scale 1 (visible over other nodes while resizing).
|
|
15543
|
+
*/
|
|
15544
|
+
getStrokeScaleEnabled() {
|
|
15545
|
+
return true;
|
|
15546
|
+
}
|
|
14952
15547
|
accessor(name, fallback, value) {
|
|
14953
15548
|
if (value === void 0) return this.attrs[name] ?? fallback;
|
|
14954
15549
|
this.setAttr(name, value);
|
|
@@ -14983,7 +15578,8 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
14983
15578
|
lineHeight: this.lineHeight(),
|
|
14984
15579
|
letterSpacing: this.letterSpacing(),
|
|
14985
15580
|
wrap: this.wrap(),
|
|
14986
|
-
align: this.align()
|
|
15581
|
+
align: this.align(),
|
|
15582
|
+
indentStep: this.indentStepValue
|
|
14987
15583
|
};
|
|
14988
15584
|
}
|
|
14989
15585
|
/** Lazy, cached layout. Size accessors and painting force it. */
|
|
@@ -15013,14 +15609,13 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15013
15609
|
};
|
|
15014
15610
|
}
|
|
15015
15611
|
/** Rendered width of a string with this shape's font and letter spacing. */
|
|
15016
|
-
measureText(text) {
|
|
15017
|
-
return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
|
|
15612
|
+
measureText(text, font) {
|
|
15613
|
+
return canvasMeasurer(font ?? fontString(this.style()), text) + text.length * this.letterSpacing();
|
|
15018
15614
|
}
|
|
15019
15615
|
/** Offset of the first line inside the box (padding + vertical align). */
|
|
15020
15616
|
getContentOffset() {
|
|
15021
15617
|
const padding = this.padding();
|
|
15022
|
-
const
|
|
15023
|
-
const used = layout.lines.length * layout.lineHeightPx + padding * 2;
|
|
15618
|
+
const used = this.ensureLayout().contentHeight + padding * 2;
|
|
15024
15619
|
const va = this.verticalAlign();
|
|
15025
15620
|
let alignY = 0;
|
|
15026
15621
|
if (va === "middle") alignY = (this.getHeight() - used) / 2;
|
|
@@ -15043,34 +15638,40 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15043
15638
|
setViewport(range) {
|
|
15044
15639
|
this.viewport = range;
|
|
15045
15640
|
}
|
|
15046
|
-
|
|
15641
|
+
/**
|
|
15642
|
+
* Visible y-range in local coordinates, derived from the context being
|
|
15643
|
+
* painted (current transform + backing canvas size) instead of the stage.
|
|
15644
|
+
* The stage is not the paint target during export (off-screen canvases,
|
|
15645
|
+
* tiles, server-side), so using it culled visible lines. Returns null (paint
|
|
15646
|
+
* everything) when the transform cannot be read.
|
|
15647
|
+
*/
|
|
15648
|
+
visibleRange(ctx) {
|
|
15047
15649
|
if (this.viewport) return this.viewport;
|
|
15048
|
-
const
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
const
|
|
15052
|
-
|
|
15053
|
-
const
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
];
|
|
15650
|
+
const native = ctx._context;
|
|
15651
|
+
const m = native.getTransform?.() ?? native.currentTransform;
|
|
15652
|
+
if (!m) return null;
|
|
15653
|
+
const det = m.a * m.d - m.b * m.c;
|
|
15654
|
+
if (!det || !Number.isFinite(det)) return null;
|
|
15655
|
+
const canvas = ctx.getCanvas();
|
|
15656
|
+
const backing = canvas._canvas;
|
|
15657
|
+
const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
|
|
15658
|
+
const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
|
|
15659
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
15660
|
+
let y1 = Infinity;
|
|
15661
|
+
let y2 = -Infinity;
|
|
15662
|
+
for (const [px, py] of [
|
|
15663
|
+
[0, 0],
|
|
15664
|
+
[w, 0],
|
|
15665
|
+
[0, h],
|
|
15666
|
+
[w, h]
|
|
15667
|
+
]) {
|
|
15668
|
+
const y = (m.a * (py - m.f) - m.b * (px - m.e)) / det;
|
|
15669
|
+
if (y < y1) y1 = y;
|
|
15670
|
+
if (y > y2) y2 = y;
|
|
15671
|
+
}
|
|
15071
15672
|
return {
|
|
15072
|
-
y1
|
|
15073
|
-
y2
|
|
15673
|
+
y1,
|
|
15674
|
+
y2
|
|
15074
15675
|
};
|
|
15075
15676
|
}
|
|
15076
15677
|
_sceneFunc = (ctx, shape) => {
|
|
@@ -15078,30 +15679,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15078
15679
|
const layout = self.ensureLayout();
|
|
15079
15680
|
if (layout.lines.length === 0) return;
|
|
15080
15681
|
const padding = self.padding();
|
|
15081
|
-
const lineHeightPx = layout.lineHeightPx;
|
|
15082
15682
|
let baseline = "middle";
|
|
15083
|
-
|
|
15084
|
-
if (!Konva.legacyTextRendering) {
|
|
15085
|
-
const m = self.measureMetrics();
|
|
15086
|
-
baseline = "alphabetic";
|
|
15087
|
-
translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
|
|
15088
|
-
}
|
|
15683
|
+
if (!Konva.legacyTextRendering) baseline = "alphabetic";
|
|
15089
15684
|
ctx.setAttr("font", fontString(self.style()));
|
|
15090
15685
|
ctx.setAttr("textBaseline", baseline);
|
|
15091
15686
|
ctx.setAttr("textAlign", "left");
|
|
15092
15687
|
const alignY = self.verticalOffset(layout);
|
|
15688
|
+
const range = self.visibleRange(ctx);
|
|
15093
15689
|
ctx.translate(padding, alignY + padding);
|
|
15094
|
-
const
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
const lineTop = top + n * lineHeightPx;
|
|
15098
|
-
if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
|
|
15099
|
-
self.paintLine(ctx, layout.lines[n], translateY + n * lineHeightPx);
|
|
15690
|
+
for (const line of layout.lines) {
|
|
15691
|
+
if (range && (alignY + padding + line.y + line.height < range.y1 - line.height || alignY + padding + line.y > range.y2 + line.height)) continue;
|
|
15692
|
+
self.paintLine(ctx, line, line.y + line.baseline);
|
|
15100
15693
|
}
|
|
15101
15694
|
};
|
|
15102
15695
|
/** Vertical offset of the text block inside the box (verticalAlign). */
|
|
15103
15696
|
verticalOffset(layout) {
|
|
15104
|
-
const used = layout.
|
|
15697
|
+
const used = layout.contentHeight + this.padding() * 2;
|
|
15105
15698
|
const va = this.verticalAlign();
|
|
15106
15699
|
if (va === "middle") return (this.getHeight() - used) / 2;
|
|
15107
15700
|
if (va === "bottom") return this.getHeight() - used;
|
|
@@ -15112,7 +15705,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15112
15705
|
const fontSize = this.fontSize();
|
|
15113
15706
|
ctx.save();
|
|
15114
15707
|
if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
|
|
15115
|
-
|
|
15708
|
+
if (line.marker) {
|
|
15709
|
+
const mk = line.marker;
|
|
15710
|
+
ctx.setAttr("font", mk.font);
|
|
15711
|
+
this.fillText(ctx, mk.text, mk.x, y, mk.color);
|
|
15712
|
+
}
|
|
15713
|
+
for (const seg of line.segments) {
|
|
15714
|
+
ctx.setAttr("font", seg.font);
|
|
15715
|
+
const x = line.offsetX + seg.x;
|
|
15716
|
+
const isLink = !this.attrs.link && isSafeLink(seg.m?.link);
|
|
15717
|
+
let color = typeof seg.m?.color === "string" ? seg.m.color : void 0;
|
|
15718
|
+
if (isLink) color = this.hoveredRun === `${line.paragraph}:${seg.run}` ? this.linkStyle.hoverColor : this.linkStyle.color;
|
|
15719
|
+
this.paintSegment(ctx, seg, x, y, color);
|
|
15720
|
+
if (seg.m?.underline || isLink) this.strokeSegment(ctx, seg, x, y + Math.round(seg.fontSize / 4), color);
|
|
15721
|
+
if (seg.m?.strike) this.strokeSegment(ctx, seg, x, y - Math.round(seg.fontSize / 4), color);
|
|
15722
|
+
}
|
|
15116
15723
|
if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
|
|
15117
15724
|
ctx.restore();
|
|
15118
15725
|
}
|
|
@@ -15126,24 +15733,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
|
|
|
15126
15733
|
ctx.stroke();
|
|
15127
15734
|
ctx.restore();
|
|
15128
15735
|
}
|
|
15129
|
-
|
|
15736
|
+
strokeSegment(ctx, seg, x, y, color) {
|
|
15737
|
+
ctx.save();
|
|
15738
|
+
ctx.beginPath();
|
|
15739
|
+
ctx.moveTo(x, y);
|
|
15740
|
+
ctx.lineTo(x + Math.round(seg.width), y);
|
|
15741
|
+
ctx.lineWidth = seg.fontSize / 15;
|
|
15742
|
+
ctx.strokeStyle = color ?? this.fill();
|
|
15743
|
+
ctx.stroke();
|
|
15744
|
+
ctx.restore();
|
|
15745
|
+
}
|
|
15746
|
+
paintSegment(ctx, seg, x, y, color) {
|
|
15130
15747
|
const spacing = this.letterSpacing();
|
|
15131
15748
|
if (spacing === 0) {
|
|
15132
|
-
this.fillText(ctx,
|
|
15749
|
+
this.fillText(ctx, seg.t, x, y, color);
|
|
15133
15750
|
return;
|
|
15134
15751
|
}
|
|
15135
15752
|
let cx = x;
|
|
15136
|
-
for (const ch of Array.from(
|
|
15137
|
-
this.fillText(ctx, ch, cx, y);
|
|
15138
|
-
cx +=
|
|
15753
|
+
for (const ch of Array.from(seg.t)) {
|
|
15754
|
+
this.fillText(ctx, ch, cx, y, color);
|
|
15755
|
+
cx += canvasMeasurer(seg.font, ch) + spacing;
|
|
15139
15756
|
}
|
|
15140
15757
|
}
|
|
15141
|
-
fillText(ctx, text, x, y) {
|
|
15758
|
+
fillText(ctx, text, x, y, color) {
|
|
15142
15759
|
const self = this;
|
|
15143
15760
|
self._partialText = text;
|
|
15144
15761
|
self._partialTextX = x;
|
|
15145
15762
|
self._partialTextY = y;
|
|
15146
|
-
|
|
15763
|
+
if (color === void 0) {
|
|
15764
|
+
ctx.fillStrokeShape(this);
|
|
15765
|
+
return;
|
|
15766
|
+
}
|
|
15767
|
+
const prev = this.attrs.fill;
|
|
15768
|
+
this.attrs.fill = color;
|
|
15769
|
+
try {
|
|
15770
|
+
ctx.fillStrokeShape(this);
|
|
15771
|
+
} finally {
|
|
15772
|
+
this.attrs.fill = prev;
|
|
15773
|
+
}
|
|
15147
15774
|
}
|
|
15148
15775
|
_hitFunc = (ctx, shape) => {
|
|
15149
15776
|
ctx.beginPath();
|
|
@@ -15259,9 +15886,27 @@ function lineIndexAt(layout, offset) {
|
|
|
15259
15886
|
}
|
|
15260
15887
|
return fallback;
|
|
15261
15888
|
}
|
|
15889
|
+
/** Width of the first `k` UTF-16 units of a line, run by run. */
|
|
15890
|
+
function prefixWidth(line, k, measure) {
|
|
15891
|
+
if (k <= 0) return 0;
|
|
15892
|
+
if (line.segments.length === 0) return measure(line.text.slice(0, k));
|
|
15893
|
+
let width = 0;
|
|
15894
|
+
let pos = 0;
|
|
15895
|
+
for (const seg of line.segments) {
|
|
15896
|
+
const end = pos + seg.t.length;
|
|
15897
|
+
if (k >= end) width += seg.width;
|
|
15898
|
+
else {
|
|
15899
|
+
width += measure(seg.t.slice(0, k - pos), seg.font);
|
|
15900
|
+
break;
|
|
15901
|
+
}
|
|
15902
|
+
pos = end;
|
|
15903
|
+
if (k === end) break;
|
|
15904
|
+
}
|
|
15905
|
+
return width;
|
|
15906
|
+
}
|
|
15262
15907
|
function lineX(line, offset, measure) {
|
|
15263
15908
|
const k = Math.max(0, Math.min(offset - line.start, line.text.length));
|
|
15264
|
-
return line.offsetX + (
|
|
15909
|
+
return line.offsetX + prefixWidth(line, k, measure);
|
|
15265
15910
|
}
|
|
15266
15911
|
/** Caret rectangle in text-local coordinates (before padding/valign offset). */
|
|
15267
15912
|
function caretRect(layout, offset, measure) {
|
|
@@ -15275,16 +15920,22 @@ function caretRect(layout, offset, measure) {
|
|
|
15275
15920
|
};
|
|
15276
15921
|
return {
|
|
15277
15922
|
x: lineX(line, offset, measure),
|
|
15278
|
-
y:
|
|
15279
|
-
height:
|
|
15923
|
+
y: line.y,
|
|
15924
|
+
height: line.height,
|
|
15280
15925
|
line: i
|
|
15281
15926
|
};
|
|
15282
15927
|
}
|
|
15928
|
+
/** Index of the line containing a local y (clamped). */
|
|
15929
|
+
function lineIndexAtY(layout, y) {
|
|
15930
|
+
const lines = layout.lines;
|
|
15931
|
+
for (let i = 0; i < lines.length; i++) if (y < lines[i].y + lines[i].height) return i;
|
|
15932
|
+
return Math.max(0, lines.length - 1);
|
|
15933
|
+
}
|
|
15283
15934
|
/** Offset closest to a point in text-local coordinates. */
|
|
15284
15935
|
function offsetFromPoint(layout, point, measure) {
|
|
15285
15936
|
const lines = layout.lines;
|
|
15286
15937
|
if (lines.length === 0) return 0;
|
|
15287
|
-
return offsetInLine(lines[
|
|
15938
|
+
return offsetInLine(lines[lineIndexAtY(layout, point.y)], point.x, measure);
|
|
15288
15939
|
}
|
|
15289
15940
|
function offsetInLine(line, x, measure) {
|
|
15290
15941
|
const rel = x - line.offsetX;
|
|
@@ -15292,7 +15943,7 @@ function offsetInLine(line, x, measure) {
|
|
|
15292
15943
|
const b = boundaries(line.text, graphemeSeg);
|
|
15293
15944
|
let prevW = 0;
|
|
15294
15945
|
for (let k = 1; k < b.length; k++) {
|
|
15295
|
-
const w =
|
|
15946
|
+
const w = prefixWidth(line, b[k], measure);
|
|
15296
15947
|
if (rel < (prevW + w) / 2) return line.start + b[k - 1];
|
|
15297
15948
|
prevW = w;
|
|
15298
15949
|
}
|
|
@@ -15316,7 +15967,7 @@ function selectionRects(layout, from, to, measure) {
|
|
|
15316
15967
|
if (from > to) [from, to] = [to, from];
|
|
15317
15968
|
if (from === to) return [];
|
|
15318
15969
|
const out = [];
|
|
15319
|
-
layout.lines.forEach((line
|
|
15970
|
+
layout.lines.forEach((line) => {
|
|
15320
15971
|
const hasNewline = line.lastInParagraph;
|
|
15321
15972
|
const lineFrom = Math.max(from, line.start);
|
|
15322
15973
|
const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
|
|
@@ -15327,9 +15978,9 @@ function selectionRects(layout, from, to, measure) {
|
|
|
15327
15978
|
if (x2 <= x1) return;
|
|
15328
15979
|
out.push({
|
|
15329
15980
|
x: x1,
|
|
15330
|
-
y:
|
|
15981
|
+
y: line.y,
|
|
15331
15982
|
width: x2 - x1,
|
|
15332
|
-
height:
|
|
15983
|
+
height: line.height
|
|
15333
15984
|
});
|
|
15334
15985
|
});
|
|
15335
15986
|
return out;
|
|
@@ -15348,6 +15999,11 @@ var TextEditModel = class {
|
|
|
15348
15999
|
focus = 0;
|
|
15349
16000
|
/** x kept while moving vertically; reset by any other move. */
|
|
15350
16001
|
desiredX = null;
|
|
16002
|
+
/**
|
|
16003
|
+
* Marks the next typed text takes at a collapsed caret (e.g. bold toggled
|
|
16004
|
+
* with nothing selected). Cleared when the selection moves.
|
|
16005
|
+
*/
|
|
16006
|
+
storedMarks = null;
|
|
15351
16007
|
undoStack = [];
|
|
15352
16008
|
redoStack = [];
|
|
15353
16009
|
lastKind = null;
|
|
@@ -15384,12 +16040,14 @@ var TextEditModel = class {
|
|
|
15384
16040
|
const len = this.getText().length;
|
|
15385
16041
|
this.anchor = Math.max(0, Math.min(anchor, len));
|
|
15386
16042
|
this.focus = Math.max(0, Math.min(focus, len));
|
|
16043
|
+
this.storedMarks = null;
|
|
15387
16044
|
this.breakCoalescing();
|
|
15388
16045
|
}
|
|
15389
16046
|
moveTo(offset, extend = false, keepDesiredX = false) {
|
|
15390
16047
|
const len = this.getText().length;
|
|
15391
16048
|
const o = Math.max(0, Math.min(offset, len));
|
|
15392
16049
|
this.focus = o;
|
|
16050
|
+
this.storedMarks = null;
|
|
15393
16051
|
if (!extend) this.anchor = o;
|
|
15394
16052
|
if (!keepDesiredX) this.desiredX = null;
|
|
15395
16053
|
this.breakCoalescing();
|
|
@@ -15397,6 +16055,7 @@ var TextEditModel = class {
|
|
|
15397
16055
|
selectAll() {
|
|
15398
16056
|
this.anchor = 0;
|
|
15399
16057
|
this.focus = this.getText().length;
|
|
16058
|
+
this.storedMarks = null;
|
|
15400
16059
|
this.breakCoalescing();
|
|
15401
16060
|
}
|
|
15402
16061
|
breakCoalescing() {
|
|
@@ -15432,7 +16091,14 @@ var TextEditModel = class {
|
|
|
15432
16091
|
end = Math.max(start, Math.min(end, len));
|
|
15433
16092
|
if (start === end && text === "") return false;
|
|
15434
16093
|
this.record(kind, start);
|
|
15435
|
-
this.
|
|
16094
|
+
const carried = this.storedMarks ?? marksAtOffset(this.blocks, start);
|
|
16095
|
+
this.blocks = replaceRange(this.blocks, start, end, text, this.storedMarks ?? void 0);
|
|
16096
|
+
this.storedMarks = null;
|
|
16097
|
+
if (text.endsWith("\n") && carried) {
|
|
16098
|
+
const { link: _link, ...rest } = carried;
|
|
16099
|
+
const next = cleanMarks(rest);
|
|
16100
|
+
if (next && !marksAtOffset(this.blocks, start + text.length)) this.storedMarks = next;
|
|
16101
|
+
}
|
|
15436
16102
|
this.anchor = this.focus = start + text.length;
|
|
15437
16103
|
this.lastEditEnd = this.focus;
|
|
15438
16104
|
this.desiredX = null;
|
|
@@ -15472,14 +16138,102 @@ var TextEditModel = class {
|
|
|
15472
16138
|
this.restore(next);
|
|
15473
16139
|
return true;
|
|
15474
16140
|
}
|
|
16141
|
+
/** Current marks at the caret / selection start (stored marks win). */
|
|
16142
|
+
caretMarks() {
|
|
16143
|
+
return this.storedMarks ?? marksAtOffset(this.blocks, this.getSelection().start);
|
|
16144
|
+
}
|
|
16145
|
+
/** Always one new undo entry, never coalesced. */
|
|
16146
|
+
commitFormat(next) {
|
|
16147
|
+
if (JSON.stringify(next) === JSON.stringify(this.blocks)) return false;
|
|
16148
|
+
this.lastKind = null;
|
|
16149
|
+
this.record("format", -1);
|
|
16150
|
+
this.lastKind = null;
|
|
16151
|
+
this.blocks = next;
|
|
16152
|
+
return true;
|
|
16153
|
+
}
|
|
16154
|
+
/** Set (`undefined` clears) one mark on the selection or the stored marks. */
|
|
16155
|
+
setMark(key, value) {
|
|
16156
|
+
const { start, end } = this.getSelection();
|
|
16157
|
+
if (start === end) {
|
|
16158
|
+
const base = { ...this.caretMarks() ?? {} };
|
|
16159
|
+
if (value === void 0 || value === false) delete base[key];
|
|
16160
|
+
else base[key] = value;
|
|
16161
|
+
this.storedMarks = cleanMarks(base) ?? {};
|
|
16162
|
+
return true;
|
|
16163
|
+
}
|
|
16164
|
+
return this.commitFormat(applyMark(this.blocks, start, end, key, value));
|
|
16165
|
+
}
|
|
16166
|
+
/** Toggle a boolean mark: on unless the whole selection already has it. */
|
|
16167
|
+
toggleMark(key) {
|
|
16168
|
+
const { start, end } = this.getSelection();
|
|
16169
|
+
const q = queryMark(this.blocks, start, end, key, this.storedMarks);
|
|
16170
|
+
return this.setMark(key, q.state === "all" ? void 0 : true);
|
|
16171
|
+
}
|
|
16172
|
+
toggleList(kind) {
|
|
16173
|
+
const { start, end } = this.getSelection();
|
|
16174
|
+
return this.commitFormat(toggleList(this.blocks, start, end, kind));
|
|
16175
|
+
}
|
|
16176
|
+
setAlign(align) {
|
|
16177
|
+
const { start, end } = this.getSelection();
|
|
16178
|
+
return this.commitFormat(setBlockAlign(this.blocks, start, end, align));
|
|
16179
|
+
}
|
|
16180
|
+
indent() {
|
|
16181
|
+
const { start, end } = this.getSelection();
|
|
16182
|
+
return this.commitFormat(indentBlocks(this.blocks, start, end));
|
|
16183
|
+
}
|
|
16184
|
+
outdent() {
|
|
16185
|
+
const { start, end } = this.getSelection();
|
|
16186
|
+
return this.commitFormat(outdentBlocks(this.blocks, start, end));
|
|
16187
|
+
}
|
|
16188
|
+
blockAt(offset) {
|
|
16189
|
+
let pos = 0;
|
|
16190
|
+
for (let i = 0; i < this.blocks.length; i++) {
|
|
16191
|
+
const len = blockText(this.blocks[i]).length;
|
|
16192
|
+
if (offset <= pos + len) return {
|
|
16193
|
+
index: i,
|
|
16194
|
+
start: pos,
|
|
16195
|
+
length: len
|
|
16196
|
+
};
|
|
16197
|
+
pos += len + 1;
|
|
16198
|
+
}
|
|
16199
|
+
}
|
|
16200
|
+
/**
|
|
16201
|
+
* Enter in a list: an empty item exits the list; otherwise the item
|
|
16202
|
+
* continues (the block split carries `kind` and `depth`).
|
|
16203
|
+
*/
|
|
16204
|
+
enter() {
|
|
16205
|
+
const { start, end } = this.getSelection();
|
|
16206
|
+
const b = start === end ? this.blockAt(start) : void 0;
|
|
16207
|
+
if (b && this.blocks[b.index].kind && b.length === 0) return this.commitFormat(outdentToPlain(this.blocks, b.index));
|
|
16208
|
+
return this.replaceRange(start, end, "\n", "other");
|
|
16209
|
+
}
|
|
16210
|
+
/**
|
|
16211
|
+
* Backspace at the start of a list item outdents it, or removes the
|
|
16212
|
+
* marker at depth 0. Returns false when the key is not handled here.
|
|
16213
|
+
*/
|
|
16214
|
+
backspaceInList() {
|
|
16215
|
+
const { start, end } = this.getSelection();
|
|
16216
|
+
if (start !== end) return false;
|
|
16217
|
+
const b = this.blockAt(start);
|
|
16218
|
+
if (!b || b.start !== start || !this.blocks[b.index].kind) return false;
|
|
16219
|
+
return this.commitFormat(outdentBlocks(this.blocks, start, start));
|
|
16220
|
+
}
|
|
15475
16221
|
restore(s) {
|
|
15476
16222
|
this.blocks = s.blocks;
|
|
16223
|
+
this.storedMarks = null;
|
|
15477
16224
|
this.anchor = s.anchor;
|
|
15478
16225
|
this.focus = s.focus;
|
|
15479
16226
|
this.lastKind = null;
|
|
15480
16227
|
this.desiredX = null;
|
|
15481
16228
|
}
|
|
15482
16229
|
};
|
|
16230
|
+
function outdentToPlain(blocks, index) {
|
|
16231
|
+
return blocks.map((b, i) => {
|
|
16232
|
+
if (i !== index) return b;
|
|
16233
|
+
const { kind: _k, depth: _d, start: _s, ...rest } = b;
|
|
16234
|
+
return rest;
|
|
16235
|
+
});
|
|
16236
|
+
}
|
|
15483
16237
|
const SLICE_BUDGET_MS = 8;
|
|
15484
16238
|
const BLINK_MS = 530;
|
|
15485
16239
|
var TextEditSession = class {
|
|
@@ -15537,7 +16291,7 @@ var TextEditSession = class {
|
|
|
15537
16291
|
};
|
|
15538
16292
|
if (this.host.selectAllOnStart) this.model.selectAll();
|
|
15539
16293
|
this.syncTextarea();
|
|
15540
|
-
ta.focus();
|
|
16294
|
+
ta.focus({ preventScroll: true });
|
|
15541
16295
|
this.positionTextarea();
|
|
15542
16296
|
this.startBlink();
|
|
15543
16297
|
this.repaint();
|
|
@@ -15566,7 +16320,7 @@ var TextEditSession = class {
|
|
|
15566
16320
|
if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
|
|
15567
16321
|
}
|
|
15568
16322
|
focus() {
|
|
15569
|
-
this.textarea?.focus();
|
|
16323
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15570
16324
|
}
|
|
15571
16325
|
createTextarea() {
|
|
15572
16326
|
const ta = this.host.container.ownerDocument.createElement("textarea");
|
|
@@ -15615,8 +16369,12 @@ var TextEditSession = class {
|
|
|
15615
16369
|
if (!ta) return;
|
|
15616
16370
|
const c = this.caretStagePoint();
|
|
15617
16371
|
const p = this.host.stageToContainer(c);
|
|
15618
|
-
|
|
15619
|
-
|
|
16372
|
+
this.host.revealCaret?.(p);
|
|
16373
|
+
const box = this.host.container;
|
|
16374
|
+
const maxX = Math.max(0, (box.clientWidth || Infinity) - 2);
|
|
16375
|
+
const maxY = Math.max(0, (box.clientHeight || Infinity) - 20);
|
|
16376
|
+
ta.style.left = `${Math.min(Math.max(p.x, 0), maxX)}px`;
|
|
16377
|
+
ta.style.top = `${Math.min(Math.max(p.y, 0), maxY)}px`;
|
|
15620
16378
|
}
|
|
15621
16379
|
caretStagePoint() {
|
|
15622
16380
|
const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
|
|
@@ -15648,10 +16406,41 @@ var TextEditSession = class {
|
|
|
15648
16406
|
if (e.key === "Escape") this.host.onRequestExit("escape");
|
|
15649
16407
|
else if (isUndo) this.undo();
|
|
15650
16408
|
else if (isRedo) this.redo();
|
|
16409
|
+
else if (this.handleRichKey(e, mod, key)) return true;
|
|
15651
16410
|
else if (e.key === "Tab") return true;
|
|
15652
16411
|
else return false;
|
|
15653
16412
|
return true;
|
|
15654
16413
|
}
|
|
16414
|
+
/** Format shortcuts and list key rules; only when rich text is enabled. */
|
|
16415
|
+
handleRichKey(e, mod, key) {
|
|
16416
|
+
if (!this.host.richText) return false;
|
|
16417
|
+
if (mod && !e.shiftKey && !e.altKey) {
|
|
16418
|
+
const marks = {
|
|
16419
|
+
b: "bold",
|
|
16420
|
+
i: "italic",
|
|
16421
|
+
u: "underline"
|
|
16422
|
+
};
|
|
16423
|
+
if (marks[key]) {
|
|
16424
|
+
this.toggleMark(marks[key]);
|
|
16425
|
+
return true;
|
|
16426
|
+
}
|
|
16427
|
+
}
|
|
16428
|
+
if (mod || this.composing) return false;
|
|
16429
|
+
if (e.key === "Tab") {
|
|
16430
|
+
if (e.shiftKey) this.outdent();
|
|
16431
|
+
else this.indent();
|
|
16432
|
+
return true;
|
|
16433
|
+
}
|
|
16434
|
+
if (e.key === "Enter") {
|
|
16435
|
+
if (this.model.enter()) this.afterFormat("command", true);
|
|
16436
|
+
return true;
|
|
16437
|
+
}
|
|
16438
|
+
if (e.key === "Backspace" && this.model.backspaceInList()) {
|
|
16439
|
+
this.afterFormat("command", true);
|
|
16440
|
+
return true;
|
|
16441
|
+
}
|
|
16442
|
+
return false;
|
|
16443
|
+
}
|
|
15655
16444
|
handleNavigationKey(e) {
|
|
15656
16445
|
if (e.ctrlKey || e.metaKey) return false;
|
|
15657
16446
|
if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
|
|
@@ -15705,7 +16494,15 @@ var TextEditSession = class {
|
|
|
15705
16494
|
while (p < max && oldText[p] === newText[p]) p++;
|
|
15706
16495
|
let s = 0;
|
|
15707
16496
|
while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
|
|
15708
|
-
|
|
16497
|
+
let inserted = newText.slice(p, newText.length - s);
|
|
16498
|
+
if (s + p === oldText.length && inserted !== "") {
|
|
16499
|
+
const q = ta.selectionEnd - inserted.length;
|
|
16500
|
+
if (q >= 0 && q < p && newText.slice(q, q + inserted.length) === inserted && oldText.slice(0, q) + inserted + oldText.slice(q) === newText) {
|
|
16501
|
+
s = oldText.length - q;
|
|
16502
|
+
p = q;
|
|
16503
|
+
inserted = newText.slice(p, newText.length - s);
|
|
16504
|
+
}
|
|
16505
|
+
}
|
|
15709
16506
|
const inputType = e.inputType ?? "";
|
|
15710
16507
|
const kind = this.editKindFor(inputType);
|
|
15711
16508
|
this.model.replaceRange(p, oldText.length - s, inserted, kind);
|
|
@@ -15746,6 +16543,7 @@ var TextEditSession = class {
|
|
|
15746
16543
|
if (this.model.undo()) {
|
|
15747
16544
|
this.syncTextarea();
|
|
15748
16545
|
this.afterContentChange(0, true);
|
|
16546
|
+
this.host.onFormatChange?.("undo");
|
|
15749
16547
|
}
|
|
15750
16548
|
}
|
|
15751
16549
|
redo() {
|
|
@@ -15753,8 +16551,42 @@ var TextEditSession = class {
|
|
|
15753
16551
|
if (this.model.redo()) {
|
|
15754
16552
|
this.syncTextarea();
|
|
15755
16553
|
this.afterContentChange(0, true);
|
|
16554
|
+
this.host.onFormatChange?.("redo");
|
|
15756
16555
|
}
|
|
15757
16556
|
}
|
|
16557
|
+
afterFormat(reason, content = false) {
|
|
16558
|
+
this.flushPending();
|
|
16559
|
+
this.syncTextarea();
|
|
16560
|
+
if (content) this.afterContentChange(0, true);
|
|
16561
|
+
else this.applyContent();
|
|
16562
|
+
this.afterSelectionChange();
|
|
16563
|
+
this.host.onFormatChange?.(reason);
|
|
16564
|
+
}
|
|
16565
|
+
toggleMark(key) {
|
|
16566
|
+
if (this.model.toggleMark(key)) this.afterFormat("command");
|
|
16567
|
+
}
|
|
16568
|
+
setMark(key, value) {
|
|
16569
|
+
if (this.model.setMark(key, value)) this.afterFormat("command");
|
|
16570
|
+
}
|
|
16571
|
+
toggleList(kind) {
|
|
16572
|
+
if (this.model.toggleList(kind)) this.afterFormat("command");
|
|
16573
|
+
}
|
|
16574
|
+
setAlign(align) {
|
|
16575
|
+
if (this.model.setAlign(align)) this.afterFormat("command");
|
|
16576
|
+
}
|
|
16577
|
+
indent() {
|
|
16578
|
+
if (this.model.indent()) this.afterFormat("command");
|
|
16579
|
+
}
|
|
16580
|
+
outdent() {
|
|
16581
|
+
if (this.model.outdent()) this.afterFormat("command");
|
|
16582
|
+
}
|
|
16583
|
+
getSelection() {
|
|
16584
|
+
return this.model.getSelection();
|
|
16585
|
+
}
|
|
16586
|
+
getFormat() {
|
|
16587
|
+
const { start, end } = this.model.getSelection();
|
|
16588
|
+
return getSelectionFormat(this.model.getBlocks(), start, end, this.target.getFontSize(), this.model.storedMarks);
|
|
16589
|
+
}
|
|
15758
16590
|
toLocal(e) {
|
|
15759
16591
|
const stagePt = this.host.clientToStage(e.clientX, e.clientY);
|
|
15760
16592
|
const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
|
|
@@ -15769,12 +16601,26 @@ var TextEditSession = class {
|
|
|
15769
16601
|
const { width, height } = this.target.getSize();
|
|
15770
16602
|
return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
|
|
15771
16603
|
}
|
|
16604
|
+
/**
|
|
16605
|
+
* True for pointer targets inside an element the app marked with
|
|
16606
|
+
* `data-weave-text-edit-keep` (e.g. a formatting toolbar): they neither
|
|
16607
|
+
* end the session nor, unless they are form fields, steal focus.
|
|
16608
|
+
*/
|
|
16609
|
+
isKeepTarget(e) {
|
|
16610
|
+
const t = e.target;
|
|
16611
|
+
return typeof Element !== "undefined" && t instanceof Element && !!t.closest("[data-weave-text-edit-keep]");
|
|
16612
|
+
}
|
|
15772
16613
|
onMouseDown = (e) => {
|
|
16614
|
+
if (this.isKeepTarget(e)) {
|
|
16615
|
+
if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
|
|
16616
|
+
return;
|
|
16617
|
+
}
|
|
15773
16618
|
if (this.isInside(this.toLocal(e))) e.preventDefault();
|
|
15774
16619
|
};
|
|
15775
16620
|
onPointerDown = (e) => {
|
|
15776
16621
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
15777
16622
|
if (this.textarea && e.target === this.textarea) return;
|
|
16623
|
+
if (this.isKeepTarget(e)) return;
|
|
15778
16624
|
const local = this.toLocal(e);
|
|
15779
16625
|
if (!this.isInside(local)) {
|
|
15780
16626
|
e.stopPropagation();
|
|
@@ -15785,10 +16631,11 @@ var TextEditSession = class {
|
|
|
15785
16631
|
e.preventDefault();
|
|
15786
16632
|
const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
|
|
15787
16633
|
const text = this.model.getText();
|
|
15788
|
-
|
|
16634
|
+
const clicks = this.countClicks(e);
|
|
16635
|
+
if (clicks >= 3) {
|
|
15789
16636
|
const [s, en] = paragraphRangeAt(text, offset);
|
|
15790
16637
|
this.model.setSelection(s, en);
|
|
15791
|
-
} else if (
|
|
16638
|
+
} else if (clicks === 2) {
|
|
15792
16639
|
const [s, en] = wordRangeAt(text, offset);
|
|
15793
16640
|
this.model.setSelection(s, en);
|
|
15794
16641
|
} else {
|
|
@@ -15797,8 +16644,31 @@ var TextEditSession = class {
|
|
|
15797
16644
|
}
|
|
15798
16645
|
this.syncTextarea();
|
|
15799
16646
|
this.afterSelectionChange();
|
|
15800
|
-
this.textarea?.focus();
|
|
16647
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15801
16648
|
};
|
|
16649
|
+
lastClick = {
|
|
16650
|
+
t: 0,
|
|
16651
|
+
x: 0,
|
|
16652
|
+
y: 0,
|
|
16653
|
+
n: 0
|
|
16654
|
+
};
|
|
16655
|
+
/**
|
|
16656
|
+
* Browsers report `detail` as 0 on pointerdown, so multi-click is counted
|
|
16657
|
+
* here (same spot, within 500 ms). A real `detail` still wins.
|
|
16658
|
+
*/
|
|
16659
|
+
countClicks(e) {
|
|
16660
|
+
const now = e.timeStamp || Date.now();
|
|
16661
|
+
const l = this.lastClick;
|
|
16662
|
+
const near = now - l.t < 500 && Math.hypot(e.clientX - l.x, e.clientY - l.y) < 5 && l.n > 0;
|
|
16663
|
+
const n = Math.max(e.detail, near ? l.n + 1 : 1);
|
|
16664
|
+
this.lastClick = {
|
|
16665
|
+
t: now,
|
|
16666
|
+
x: e.clientX,
|
|
16667
|
+
y: e.clientY,
|
|
16668
|
+
n: n >= 3 ? 0 : n
|
|
16669
|
+
};
|
|
16670
|
+
return n;
|
|
16671
|
+
}
|
|
15802
16672
|
onPointerMove = (e) => {
|
|
15803
16673
|
if (!this.dragging) return;
|
|
15804
16674
|
const local = this.toLocal(e);
|
|
@@ -15810,13 +16680,14 @@ var TextEditSession = class {
|
|
|
15810
16680
|
onPointerUp = () => {
|
|
15811
16681
|
if (!this.dragging) return;
|
|
15812
16682
|
this.dragging = false;
|
|
15813
|
-
this.textarea?.focus();
|
|
16683
|
+
this.textarea?.focus({ preventScroll: true });
|
|
15814
16684
|
};
|
|
15815
16685
|
afterSelectionChange() {
|
|
15816
16686
|
this.syncTextarea();
|
|
15817
16687
|
this.positionTextarea();
|
|
15818
16688
|
this.resetBlink();
|
|
15819
16689
|
this.repaint();
|
|
16690
|
+
this.host.onSelectionChange?.();
|
|
15820
16691
|
}
|
|
15821
16692
|
afterContentChange(insertedLength, immediate = false) {
|
|
15822
16693
|
this.flushPending();
|
|
@@ -15840,6 +16711,7 @@ var TextEditSession = class {
|
|
|
15840
16711
|
applyContent() {
|
|
15841
16712
|
this.target.setBlocks(this.model.getBlocks());
|
|
15842
16713
|
this.host.onChange?.(this.model.getBlocks());
|
|
16714
|
+
this.host.onSelectionChange?.();
|
|
15843
16715
|
this.positionTextarea();
|
|
15844
16716
|
this.target.requestRedraw();
|
|
15845
16717
|
this.repaint();
|
|
@@ -15915,15 +16787,29 @@ function host_signal(signal, abort) {
|
|
|
15915
16787
|
//#region src/nodes/text-v2/constants.ts
|
|
15916
16788
|
/** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
|
|
15917
16789
|
const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
|
|
16790
|
+
/** Default list indent per depth, as a multiple of the node `fontSize`. */
|
|
16791
|
+
const WEAVE_TEXT_V2_DEFAULT_INDENT_STEP = 1.5;
|
|
15918
16792
|
const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
|
|
15919
16793
|
const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
|
|
15920
16794
|
...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
|
|
15921
|
-
edition: {
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
16795
|
+
edition: {
|
|
16796
|
+
outline: {
|
|
16797
|
+
enabled: true,
|
|
16798
|
+
color: "#0074ff",
|
|
16799
|
+
width: 1,
|
|
16800
|
+
padding: 0
|
|
16801
|
+
},
|
|
16802
|
+
followPadding: {
|
|
16803
|
+
top: 40,
|
|
16804
|
+
right: 40,
|
|
16805
|
+
bottom: 40,
|
|
16806
|
+
left: 40
|
|
16807
|
+
}
|
|
16808
|
+
},
|
|
16809
|
+
richText: {
|
|
16810
|
+
enabled: false,
|
|
16811
|
+
indentStep: WEAVE_TEXT_V2_DEFAULT_INDENT_STEP
|
|
16812
|
+
},
|
|
15927
16813
|
sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
|
|
15928
16814
|
};
|
|
15929
16815
|
//#endregion
|
|
@@ -15951,6 +16837,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
15951
16837
|
editingNodeId = null;
|
|
15952
16838
|
session = null;
|
|
15953
16839
|
sizeWarned = false;
|
|
16840
|
+
selectionTimer;
|
|
15954
16841
|
nodeRenderedAddedRegistered = false;
|
|
15955
16842
|
keyPressHandler;
|
|
15956
16843
|
constructor(params) {
|
|
@@ -16024,6 +16911,11 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16024
16911
|
linkPreviousTextDecoration
|
|
16025
16912
|
}
|
|
16026
16913
|
});
|
|
16914
|
+
text.setIndentStep(this.indentStep());
|
|
16915
|
+
text.setLinkStyle({
|
|
16916
|
+
color: this.config.link.defaultColor,
|
|
16917
|
+
hoverColor: this.config.link.hoverColor
|
|
16918
|
+
});
|
|
16027
16919
|
this.setupDefaultNodeAugmentation(text);
|
|
16028
16920
|
const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
|
|
16029
16921
|
text.getTransformerProperties = function() {
|
|
@@ -16119,7 +17011,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16119
17011
|
this.applySize(shape);
|
|
16120
17012
|
},
|
|
16121
17013
|
getLayout: () => shape.ensureLayout(),
|
|
16122
|
-
measure: (t) => shape.measureText(t),
|
|
17014
|
+
measure: (t, f) => shape.measureText(t, f),
|
|
16123
17015
|
getAbsoluteTransform: () => shape.getAbsoluteTransform(),
|
|
16124
17016
|
getContentOffset: () => shape.getContentOffset(),
|
|
16125
17017
|
getSize: () => ({
|
|
@@ -16170,7 +17062,23 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16170
17062
|
y: p.y * sy
|
|
16171
17063
|
};
|
|
16172
17064
|
},
|
|
17065
|
+
revealCaret: (p) => {
|
|
17066
|
+
const box = container;
|
|
17067
|
+
const w = box.clientWidth;
|
|
17068
|
+
const h = box.clientHeight;
|
|
17069
|
+
if (!w || !h) return;
|
|
17070
|
+
const f = this.config.edition.followPadding;
|
|
17071
|
+
const dx = p.x < f.left ? f.left - p.x : p.x > w - f.right ? w - f.right - p.x : 0;
|
|
17072
|
+
const dy = p.y < f.top ? f.top - p.y : p.y > h - f.bottom ? h - f.bottom - p.y : 0;
|
|
17073
|
+
if (!dx && !dy) return;
|
|
17074
|
+
stage.x(stage.x() + dx);
|
|
17075
|
+
stage.y(stage.y() + dy);
|
|
17076
|
+
this.instance.emitEvent("onStageMove");
|
|
17077
|
+
},
|
|
16173
17078
|
selectAllOnStart: !create,
|
|
17079
|
+
richText: this.isRich(shape),
|
|
17080
|
+
onSelectionChange: () => this.scheduleSelectionEvent(shape),
|
|
17081
|
+
onFormatChange: (reason) => this.emitFormatChange(shape, reason),
|
|
16174
17082
|
caretColor: this.config.cursor.color,
|
|
16175
17083
|
selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
|
|
16176
17084
|
...this.config.edition.outline.enabled && {
|
|
@@ -16206,6 +17114,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16206
17114
|
exitEditMode(shape) {
|
|
16207
17115
|
const session = this.session;
|
|
16208
17116
|
if (!session || !this.editing) return;
|
|
17117
|
+
clearTimeout(this.selectionTimer);
|
|
16209
17118
|
const { changed } = session.getResult();
|
|
16210
17119
|
session.destroy();
|
|
16211
17120
|
this.session = null;
|
|
@@ -16268,11 +17177,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16268
17177
|
text.getLayer()?.batchDraw();
|
|
16269
17178
|
}
|
|
16270
17179
|
const layout = text.getAttrs().layout;
|
|
16271
|
-
if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner()))
|
|
16272
|
-
text.width(text.width() * text.scaleX());
|
|
16273
|
-
resetScale(text);
|
|
16274
|
-
text.fontSize(text.fontSize() * text.scaleY());
|
|
16275
|
-
}
|
|
17180
|
+
if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
|
|
16276
17181
|
text.setAttr("shouldUpdateOnTransform", false);
|
|
16277
17182
|
text.getLayer()?.batchDraw();
|
|
16278
17183
|
});
|
|
@@ -16316,6 +17221,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16316
17221
|
};
|
|
16317
17222
|
const isLinked = Boolean(nextProps.link);
|
|
16318
17223
|
const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
|
|
17224
|
+
shape.setIndentStep(this.indentStep());
|
|
16319
17225
|
shape.setAttrs({
|
|
16320
17226
|
...nextProps,
|
|
16321
17227
|
...this.outlineProps(),
|
|
@@ -16353,7 +17259,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16353
17259
|
delete cleanedAttrs.dragBoundFunc;
|
|
16354
17260
|
if (cleanedAttrs.link) {
|
|
16355
17261
|
if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
|
|
16356
|
-
|
|
17262
|
+
cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
|
|
16357
17263
|
}
|
|
16358
17264
|
return {
|
|
16359
17265
|
key: attrs.id ?? "",
|
|
@@ -16368,18 +17274,85 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16368
17274
|
}
|
|
16369
17275
|
};
|
|
16370
17276
|
}
|
|
17277
|
+
linkCtxMenuGuardRegistered = false;
|
|
17278
|
+
linkHoverText = null;
|
|
17279
|
+
linkModifier = false;
|
|
17280
|
+
linkHoverRefresh = null;
|
|
17281
|
+
linkHitAt(text) {
|
|
17282
|
+
const link = text.getAttrs().link;
|
|
17283
|
+
if (link) {
|
|
17284
|
+
const p = this.localPointer(text);
|
|
17285
|
+
return p.x >= 0 && p.y >= 0 && p.x <= text.width() && p.y <= text.height() ? { url: link } : null;
|
|
17286
|
+
}
|
|
17287
|
+
return text.linkAtPoint(this.localPointer(text));
|
|
17288
|
+
}
|
|
17289
|
+
registerLinkContextMenuGuard() {
|
|
17290
|
+
if (this.linkCtxMenuGuardRegistered || this.instance.isServerSide()) return;
|
|
17291
|
+
this.linkCtxMenuGuardRegistered = true;
|
|
17292
|
+
const stage = this.instance.getStage();
|
|
17293
|
+
const signal = this.instance.getEventsController().signal;
|
|
17294
|
+
const syncModifier = (e) => {
|
|
17295
|
+
const next = e.ctrlKey || e.metaKey;
|
|
17296
|
+
if (next === this.linkModifier) return;
|
|
17297
|
+
this.linkModifier = next;
|
|
17298
|
+
this.linkHoverRefresh?.();
|
|
17299
|
+
};
|
|
17300
|
+
window.addEventListener("keydown", syncModifier, { signal });
|
|
17301
|
+
window.addEventListener("keyup", syncModifier, { signal });
|
|
17302
|
+
window.addEventListener("blur", () => {
|
|
17303
|
+
this.linkModifier = false;
|
|
17304
|
+
this.linkHoverRefresh?.();
|
|
17305
|
+
}, { signal });
|
|
17306
|
+
stage.container().addEventListener("contextmenu", (e) => {
|
|
17307
|
+
if (this.editing || !e.ctrlKey) return;
|
|
17308
|
+
const text = this.linkHoverText;
|
|
17309
|
+
if (!text) return;
|
|
17310
|
+
stage.setPointersPositions(e);
|
|
17311
|
+
const hit = this.linkHitAt(text);
|
|
17312
|
+
if (!hit) return;
|
|
17313
|
+
e.preventDefault();
|
|
17314
|
+
e.stopPropagation();
|
|
17315
|
+
this.openUrl(hit.url);
|
|
17316
|
+
}, {
|
|
17317
|
+
capture: true,
|
|
17318
|
+
signal: this.instance.getEventsController().signal
|
|
17319
|
+
});
|
|
17320
|
+
}
|
|
16371
17321
|
setupLinkBehavior(text) {
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
17322
|
+
this.registerLinkContextMenuGuard();
|
|
17323
|
+
const container = () => this.instance.getStage().container();
|
|
17324
|
+
const idleCursor = () => this.isNodeSelected(text) ? "grab" : "pointer";
|
|
17325
|
+
const applyHover = () => {
|
|
17326
|
+
if (this.editing) return;
|
|
17327
|
+
this.linkHoverText = text;
|
|
17328
|
+
const hit = this.linkModifier ? this.linkHitAt(text) : null;
|
|
17329
|
+
if (!text.getAttrs().link) text.setHoveredRun(hit?.key ?? null);
|
|
17330
|
+
else {
|
|
17331
|
+
text.fill(hit ? this.config.link.hoverColor : this.config.link.defaultColor);
|
|
17332
|
+
text.getLayer()?.batchDraw();
|
|
17333
|
+
}
|
|
17334
|
+
container().style.cursor = hit ? "pointer" : idleCursor();
|
|
17335
|
+
};
|
|
17336
|
+
const updateHover = (e) => {
|
|
17337
|
+
if (e?.evt) this.linkModifier = e.evt.ctrlKey || e.evt.metaKey;
|
|
17338
|
+
this.linkHoverRefresh = applyHover;
|
|
17339
|
+
applyHover();
|
|
17340
|
+
};
|
|
17341
|
+
text.handleMouseover = (e) => {
|
|
17342
|
+
updateHover(e);
|
|
16376
17343
|
};
|
|
16377
17344
|
text.handleMouseout = () => {
|
|
17345
|
+
if (this.linkHoverText === text) {
|
|
17346
|
+
this.linkHoverText = null;
|
|
17347
|
+
this.linkHoverRefresh = null;
|
|
17348
|
+
}
|
|
17349
|
+
text.setHoveredRun(null);
|
|
16378
17350
|
if (!text.getAttrs().link) return;
|
|
16379
17351
|
text.fill(this.config.link.defaultColor);
|
|
16380
17352
|
text.getLayer()?.batchDraw();
|
|
16381
17353
|
};
|
|
16382
|
-
text.
|
|
17354
|
+
text.on("pointermove", updateHover);
|
|
17355
|
+
text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
|
|
16383
17356
|
let linkClickTimeout;
|
|
16384
17357
|
const clearLinkClickTimeout = () => {
|
|
16385
17358
|
if (linkClickTimeout) {
|
|
@@ -16393,27 +17366,41 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16393
17366
|
previousDblClick();
|
|
16394
17367
|
};
|
|
16395
17368
|
text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
|
|
16396
|
-
|
|
17369
|
+
const runLink = text.getAttrs().link ? null : text.linkAtPoint(this.localPointer(text));
|
|
17370
|
+
if (!text.getAttrs().link && !runLink) return;
|
|
17371
|
+
const open = () => runLink ? this.openUrl(runLink.url) : this.openTextLink(text);
|
|
16397
17372
|
if (ctrlOrMetaPressed) {
|
|
16398
17373
|
clearLinkClickTimeout();
|
|
16399
|
-
|
|
17374
|
+
open();
|
|
16400
17375
|
return;
|
|
16401
17376
|
}
|
|
16402
17377
|
if (wasSelected) {
|
|
16403
17378
|
clearLinkClickTimeout();
|
|
16404
17379
|
linkClickTimeout = setTimeout(() => {
|
|
16405
17380
|
linkClickTimeout = void 0;
|
|
16406
|
-
|
|
17381
|
+
open();
|
|
16407
17382
|
}, Konva.dblClickWindow);
|
|
16408
17383
|
}
|
|
16409
17384
|
};
|
|
16410
17385
|
}
|
|
16411
|
-
|
|
17386
|
+
localPointer(text) {
|
|
17387
|
+
return text.getRelativePointerPosition() ?? {
|
|
17388
|
+
x: -1,
|
|
17389
|
+
y: -1
|
|
17390
|
+
};
|
|
17391
|
+
}
|
|
17392
|
+
lastLinkOpen = 0;
|
|
17393
|
+
openUrl(link) {
|
|
16412
17394
|
if (this.instance.isServerSide()) return;
|
|
16413
|
-
const link = textNode.getAttrs().link;
|
|
16414
17395
|
if (typeof link !== "string" || !isValidTextLink(link)) return;
|
|
17396
|
+
const now = Date.now();
|
|
17397
|
+
if (now - this.lastLinkOpen < 500) return;
|
|
17398
|
+
this.lastLinkOpen = now;
|
|
16415
17399
|
window.open(link, "_blank", "noopener,noreferrer");
|
|
16416
17400
|
}
|
|
17401
|
+
openTextLink(textNode) {
|
|
17402
|
+
this.openUrl(textNode.getAttrs().link);
|
|
17403
|
+
}
|
|
16417
17404
|
getLink(nodeInstance) {
|
|
16418
17405
|
const link = nodeInstance.getAttrs().link;
|
|
16419
17406
|
return typeof link === "string" ? link : void 0;
|
|
@@ -16433,6 +17420,173 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16433
17420
|
delete serialized.props.linkPreviousTextDecoration;
|
|
16434
17421
|
this.instance.updateNode(serialized);
|
|
16435
17422
|
}
|
|
17423
|
+
indentStep() {
|
|
17424
|
+
const step = this.config.richText.indentStep;
|
|
17425
|
+
return Number.isFinite(step) && step > 0 ? step : 1.5;
|
|
17426
|
+
}
|
|
17427
|
+
/**
|
|
17428
|
+
* Rich text is on for a node when its `richText` attribute says so;
|
|
17429
|
+
* nodes without the attribute follow `config.richText.enabled`.
|
|
17430
|
+
*/
|
|
17431
|
+
isRich(nodeInstance) {
|
|
17432
|
+
const own = nodeInstance.getAttrs().richText;
|
|
17433
|
+
return typeof own === "boolean" ? own : this.config.richText.enabled;
|
|
17434
|
+
}
|
|
17435
|
+
isRichTextEnabled(nodeInstance) {
|
|
17436
|
+
return nodeInstance ? this.isRich(nodeInstance) : this.config.richText.enabled;
|
|
17437
|
+
}
|
|
17438
|
+
activeSession(shape) {
|
|
17439
|
+
return this.editing && this.editingNodeId === shape.id() ? this.session : null;
|
|
17440
|
+
}
|
|
17441
|
+
emitFormatChange(shape, reason) {
|
|
17442
|
+
this.instance.emitEvent("onTextFormatChange", {
|
|
17443
|
+
node: shape,
|
|
17444
|
+
format: this.getSelectionFormat(shape),
|
|
17445
|
+
reason
|
|
17446
|
+
});
|
|
17447
|
+
}
|
|
17448
|
+
/** Selection events fire often while dragging: coalesce them. */
|
|
17449
|
+
scheduleSelectionEvent(shape) {
|
|
17450
|
+
if (this.selectionTimer) return;
|
|
17451
|
+
this.selectionTimer = setTimeout(() => {
|
|
17452
|
+
this.selectionTimer = void 0;
|
|
17453
|
+
const session = this.activeSession(shape);
|
|
17454
|
+
if (!session) return;
|
|
17455
|
+
const { start, end } = session.getSelection();
|
|
17456
|
+
this.instance.emitEvent("onTextSelectionChange", {
|
|
17457
|
+
node: shape,
|
|
17458
|
+
start,
|
|
17459
|
+
end,
|
|
17460
|
+
format: session.getFormat()
|
|
17461
|
+
});
|
|
17462
|
+
}, 50);
|
|
17463
|
+
}
|
|
17464
|
+
/**
|
|
17465
|
+
* Format of the active selection while editing, otherwise of `range`
|
|
17466
|
+
* (default: the whole text). Sizes are px.
|
|
17467
|
+
*/
|
|
17468
|
+
getSelectionFormat(nodeInstance, range) {
|
|
17469
|
+
const shape = nodeInstance;
|
|
17470
|
+
const session = this.activeSession(shape);
|
|
17471
|
+
if (session) return session.getFormat();
|
|
17472
|
+
const blocks = shape.getBlocks();
|
|
17473
|
+
const r = range ?? {
|
|
17474
|
+
start: 0,
|
|
17475
|
+
end: blocksToText(blocks).length
|
|
17476
|
+
};
|
|
17477
|
+
return getSelectionFormat(blocks, r.start, r.end, shape.fontSize());
|
|
17478
|
+
}
|
|
17479
|
+
/**
|
|
17480
|
+
* Run a block transform on a non-editing node: one `updateNode`, so one
|
|
17481
|
+
* Weave undo step. Returns false when rich text is disabled.
|
|
17482
|
+
*/
|
|
17483
|
+
commandOnNode(shape, range, fn) {
|
|
17484
|
+
const blocks = shape.getBlocks();
|
|
17485
|
+
const next = fn(blocks, range?.start ?? 0, range?.end ?? blocksToText(blocks).length);
|
|
17486
|
+
if (JSON.stringify(next) === JSON.stringify(blocks)) return false;
|
|
17487
|
+
shape.setBlocks(next);
|
|
17488
|
+
this.applySize(shape);
|
|
17489
|
+
this.instance.updateNode(this.serialize(shape));
|
|
17490
|
+
this.emitFormatChange(shape, "command");
|
|
17491
|
+
return true;
|
|
17492
|
+
}
|
|
17493
|
+
/**
|
|
17494
|
+
* Set a mark (`undefined` clears it). `size` is in px. While the node is
|
|
17495
|
+
* being edited it applies to the editor selection, otherwise to `range`
|
|
17496
|
+
* (default: the whole text).
|
|
17497
|
+
*/
|
|
17498
|
+
applyMark(nodeInstance, key, value, range) {
|
|
17499
|
+
const shape = nodeInstance;
|
|
17500
|
+
if (!this.isRich(shape)) return false;
|
|
17501
|
+
let v = value;
|
|
17502
|
+
if (key === "link" && v !== void 0 && !isValidTextLink(String(v))) return false;
|
|
17503
|
+
if (key === "size" && typeof value === "number") v = sizeToRatio(value, shape.fontSize());
|
|
17504
|
+
const session = this.activeSession(shape);
|
|
17505
|
+
if (session) {
|
|
17506
|
+
session.setMark(key, v);
|
|
17507
|
+
return true;
|
|
17508
|
+
}
|
|
17509
|
+
return this.commandOnNode(shape, range, (b, s, e) => applyMark(b, s, e, key, v));
|
|
17510
|
+
}
|
|
17511
|
+
/** Link a range (or the editor selection) to an http(s) URL. */
|
|
17512
|
+
setRunLink(nodeInstance, url, range) {
|
|
17513
|
+
if (typeof url !== "string" || !isValidTextLink(url)) return false;
|
|
17514
|
+
return this.applyMark(nodeInstance, "link", url, range);
|
|
17515
|
+
}
|
|
17516
|
+
removeRunLink(nodeInstance, range) {
|
|
17517
|
+
return this.applyMark(nodeInstance, "link", void 0, range);
|
|
17518
|
+
}
|
|
17519
|
+
toggleMark(nodeInstance, key, range) {
|
|
17520
|
+
const shape = nodeInstance;
|
|
17521
|
+
if (!this.isRich(shape)) return false;
|
|
17522
|
+
const session = this.activeSession(shape);
|
|
17523
|
+
if (session) {
|
|
17524
|
+
session.toggleMark(key);
|
|
17525
|
+
return true;
|
|
17526
|
+
}
|
|
17527
|
+
return this.commandOnNode(shape, range, (b, s, e) => toggleMark(b, s, e, key));
|
|
17528
|
+
}
|
|
17529
|
+
toggleList(nodeInstance, kind, range) {
|
|
17530
|
+
const shape = nodeInstance;
|
|
17531
|
+
if (!this.isRich(shape)) return false;
|
|
17532
|
+
const session = this.activeSession(shape);
|
|
17533
|
+
if (session) {
|
|
17534
|
+
session.toggleList(kind);
|
|
17535
|
+
return true;
|
|
17536
|
+
}
|
|
17537
|
+
return this.commandOnNode(shape, range, (b, s, e) => toggleList(b, s, e, kind));
|
|
17538
|
+
}
|
|
17539
|
+
/** Align every paragraph touched by the range (default: all). */
|
|
17540
|
+
setAlign(nodeInstance, align, range) {
|
|
17541
|
+
const shape = nodeInstance;
|
|
17542
|
+
if (!this.isRich(shape)) return false;
|
|
17543
|
+
if (align !== void 0 && ![
|
|
17544
|
+
"left",
|
|
17545
|
+
"center",
|
|
17546
|
+
"right"
|
|
17547
|
+
].includes(align)) return false;
|
|
17548
|
+
const session = this.activeSession(shape);
|
|
17549
|
+
if (session) {
|
|
17550
|
+
session.setAlign(align);
|
|
17551
|
+
return true;
|
|
17552
|
+
}
|
|
17553
|
+
return this.commandOnNode(shape, range, (b, s, e) => setBlockAlign(b, s, e, align));
|
|
17554
|
+
}
|
|
17555
|
+
indent(nodeInstance, range) {
|
|
17556
|
+
const shape = nodeInstance;
|
|
17557
|
+
if (!this.isRich(shape)) return false;
|
|
17558
|
+
const session = this.activeSession(shape);
|
|
17559
|
+
if (session) {
|
|
17560
|
+
session.indent();
|
|
17561
|
+
return true;
|
|
17562
|
+
}
|
|
17563
|
+
return this.commandOnNode(shape, range, indentBlocks);
|
|
17564
|
+
}
|
|
17565
|
+
outdent(nodeInstance, range) {
|
|
17566
|
+
const shape = nodeInstance;
|
|
17567
|
+
if (!this.isRich(shape)) return false;
|
|
17568
|
+
const session = this.activeSession(shape);
|
|
17569
|
+
if (session) {
|
|
17570
|
+
session.outdent();
|
|
17571
|
+
return true;
|
|
17572
|
+
}
|
|
17573
|
+
return this.commandOnNode(shape, range, outdentBlocks);
|
|
17574
|
+
}
|
|
17575
|
+
/**
|
|
17576
|
+
* Scale the whole text: sets the node `fontSize` (px); size marks are
|
|
17577
|
+
* ratios of it, so every run scales with it.
|
|
17578
|
+
*/
|
|
17579
|
+
setBaseFontSize(nodeInstance, px) {
|
|
17580
|
+
const shape = nodeInstance;
|
|
17581
|
+
if (!this.isRich(shape)) return false;
|
|
17582
|
+
if (!Number.isFinite(px) || px <= 0) return false;
|
|
17583
|
+
if (shape.fontSize() === px) return false;
|
|
17584
|
+
shape.fontSize(px);
|
|
17585
|
+
this.applySize(shape);
|
|
17586
|
+
if (!this.activeSession(shape)) this.instance.updateNode(this.serialize(shape));
|
|
17587
|
+
this.emitFormatChange(shape, "base-size");
|
|
17588
|
+
return true;
|
|
17589
|
+
}
|
|
16436
17590
|
resetSmartLayout(textNode) {
|
|
16437
17591
|
textNode.setAttr("smartFixedWidth", void 0);
|
|
16438
17592
|
textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
|
|
@@ -16451,7 +17605,36 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
16451
17605
|
return next;
|
|
16452
17606
|
}
|
|
16453
17607
|
static getSchema() {
|
|
16454
|
-
|
|
17608
|
+
const base = WeaveTextNode.getSchema();
|
|
17609
|
+
const marks = z.object({
|
|
17610
|
+
bold: z.boolean().optional(),
|
|
17611
|
+
italic: z.boolean().optional(),
|
|
17612
|
+
underline: z.boolean().optional(),
|
|
17613
|
+
strike: z.boolean().optional(),
|
|
17614
|
+
color: z.string().optional(),
|
|
17615
|
+
size: z.number().positive().optional().describe("Ratio of the node fontSize (1 is omitted)."),
|
|
17616
|
+
family: z.string().optional(),
|
|
17617
|
+
lineHeight: z.number().positive().optional(),
|
|
17618
|
+
link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional()
|
|
17619
|
+
}).passthrough();
|
|
17620
|
+
const block = z.object({
|
|
17621
|
+
runs: z.array(z.object({
|
|
17622
|
+
t: z.string(),
|
|
17623
|
+
m: marks.optional()
|
|
17624
|
+
})),
|
|
17625
|
+
kind: z.enum(["ul", "ol"]).optional(),
|
|
17626
|
+
depth: z.number().int().min(0).optional(),
|
|
17627
|
+
start: z.number().int().min(1).optional(),
|
|
17628
|
+
align: z.enum([
|
|
17629
|
+
"left",
|
|
17630
|
+
"center",
|
|
17631
|
+
"right"
|
|
17632
|
+
]).optional()
|
|
17633
|
+
});
|
|
17634
|
+
return base.extend({ props: base.shape.props.extend({
|
|
17635
|
+
richText: z.boolean().optional().describe("Enables rich text commands for this node."),
|
|
17636
|
+
blocks: z.array(block).optional().describe("Rich content: paragraphs of runs with marks and optional list fields. `text` is the derived plain mirror.")
|
|
17637
|
+
}) });
|
|
16455
17638
|
}
|
|
16456
17639
|
};
|
|
16457
17640
|
//#endregion
|
|
@@ -26973,8 +28156,16 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
26973
28156
|
cancelAction;
|
|
26974
28157
|
onPropsChange = void 0;
|
|
26975
28158
|
onInit = void 0;
|
|
26976
|
-
|
|
28159
|
+
toolName;
|
|
28160
|
+
initialProps;
|
|
28161
|
+
/**
|
|
28162
|
+
* `name` and `props` let an app register several text tools, e.g. a
|
|
28163
|
+
* plain one and one creating rich text nodes (`props: { richText: true }`).
|
|
28164
|
+
*/
|
|
28165
|
+
constructor(params) {
|
|
26977
28166
|
super();
|
|
28167
|
+
this.toolName = params?.name ?? "textTool";
|
|
28168
|
+
this.initialProps = params?.props ?? {};
|
|
26978
28169
|
this.initialize();
|
|
26979
28170
|
}
|
|
26980
28171
|
initialize() {
|
|
@@ -26986,7 +28177,7 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
26986
28177
|
this.props = this.initProps();
|
|
26987
28178
|
}
|
|
26988
28179
|
getName() {
|
|
26989
|
-
return
|
|
28180
|
+
return this.toolName;
|
|
26990
28181
|
}
|
|
26991
28182
|
initProps() {
|
|
26992
28183
|
return {
|
|
@@ -26998,7 +28189,8 @@ var WeaveTextToolAction = class extends WeaveAction {
|
|
|
26998
28189
|
align: "left",
|
|
26999
28190
|
lineHeight: 1,
|
|
27000
28191
|
verticalAlign: "top",
|
|
27001
|
-
strokeEnabled: false
|
|
28192
|
+
strokeEnabled: false,
|
|
28193
|
+
...this.initialProps
|
|
27002
28194
|
};
|
|
27003
28195
|
}
|
|
27004
28196
|
setupEvents() {
|