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