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