@markdstage/markdstage 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/shared/README.md +35 -15
- package/shared/architecture-editor/editor.css +116 -20
- package/shared/architecture-editor/editor.js +577 -42
- package/shared/architecture-editor/index.html +53 -24
- package/shared/renderer/architecture.mjs +96 -15
- package/shared/renderer/index.html +11 -0
- package/shared/renderer/renderer.js +227 -13
- package/shared/renderer/slides.css +5 -2
- package/shared/runtime/pptx-package.mjs +9 -1
- package/shared/schema/architecture-v1.schema.json +11 -1
|
@@ -7,40 +7,66 @@
|
|
|
7
7
|
<link rel="stylesheet" href="./renderer/slides.css">
|
|
8
8
|
<link rel="stylesheet" href="./editor/editor.css">
|
|
9
9
|
</head>
|
|
10
|
-
<body>
|
|
10
|
+
<body data-elements-open="true" data-inspector-open="false">
|
|
11
11
|
<header class="editor-header">
|
|
12
12
|
<div class="editor-title">
|
|
13
13
|
<strong>Architecture Editor</strong>
|
|
14
14
|
<span id="sourceLabel"></span>
|
|
15
15
|
<span id="dirtyBadge" class="dirty-badge" hidden>Unsaved</span>
|
|
16
16
|
</div>
|
|
17
|
+
<div class="editor-panel-actions" role="group" aria-label="Designer panels">
|
|
18
|
+
<button id="elementsPanelButton" class="editor-panel-toggle" type="button" aria-controls="elementPanel" aria-expanded="false">Elements</button>
|
|
19
|
+
<button id="inspectorPanelButton" class="editor-panel-toggle" type="button" aria-controls="inspectorPanel" aria-expanded="false">Properties</button>
|
|
20
|
+
</div>
|
|
17
21
|
<div class="editor-actions" role="toolbar" aria-label="Architecture editing controls">
|
|
18
|
-
<button type="button" data-action="undo" title="Undo (Ctrl+Z)">↶</button>
|
|
19
|
-
<button type="button" data-action="redo" title="Redo (Ctrl+Y)">↷</button>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<button type="button" data-action="add-connector">Connector</button>
|
|
25
|
-
<button type="button" data-action="duplicate" title="Duplicate (Ctrl+D)">Duplicate</button>
|
|
26
|
-
<button type="button" data-action="delete" title="Delete (Delete)">Delete</button>
|
|
27
|
-
<button type="button" data-action="release-layout">Release layout</button>
|
|
28
|
-
<span class="toolbar-separator" aria-hidden="true"></span>
|
|
29
|
-
<button type="button" data-action="order-back" title="Move earlier in declaration order">Send backward</button>
|
|
30
|
-
<button type="button" data-action="order-front" title="Move later in declaration order">Bring forward</button>
|
|
31
|
-
<span class="toolbar-separator" aria-hidden="true"></span>
|
|
32
|
-
<button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
|
|
33
|
-
<button type="button" data-action="zoom-fit">Fit</button>
|
|
34
|
-
<button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
|
|
35
|
-
<label class="snap-toggle"><input id="snapToggle" type="checkbox" checked> Grid</label>
|
|
22
|
+
<button type="button" data-action="undo" title="Undo (Ctrl+Z)" aria-label="Undo">↶</button>
|
|
23
|
+
<button type="button" data-action="redo" title="Redo (Ctrl+Y)" aria-label="Redo">↷</button>
|
|
24
|
+
<div class="shape-palette-host">
|
|
25
|
+
<button id="shapePaletteButton" type="button" data-action="toggle-shape-palette" aria-haspopup="menu" aria-controls="shapePalette" aria-expanded="false">Shape</button>
|
|
26
|
+
<div id="shapePalette" class="shape-palette" role="menu" aria-label="Add a shape" hidden></div>
|
|
27
|
+
</div>
|
|
36
28
|
<button type="button" class="save-button" data-action="save" title="Save to Markdown (Ctrl+S)">Save</button>
|
|
37
|
-
<
|
|
29
|
+
<div class="toolbar-more-host">
|
|
30
|
+
<button id="toolbarMoreButton" type="button" aria-haspopup="menu" aria-controls="toolbarMoreMenu" aria-expanded="false">More</button>
|
|
31
|
+
<div id="toolbarMoreMenu" class="toolbar-more-menu" role="menu" aria-label="More editing controls" hidden>
|
|
32
|
+
<div class="toolbar-more-group" role="presentation">
|
|
33
|
+
<span class="toolbar-more-label">Add</span>
|
|
34
|
+
<button type="button" role="menuitem" data-action="add-group">Group</button>
|
|
35
|
+
<button type="button" role="menuitem" data-action="add-image">Image</button>
|
|
36
|
+
<button type="button" role="menuitem" data-action="add-connector">Connector</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="toolbar-more-group" role="presentation">
|
|
39
|
+
<span class="toolbar-more-label">Edit</span>
|
|
40
|
+
<button type="button" role="menuitem" data-action="duplicate" title="Duplicate (Ctrl+D)">Duplicate</button>
|
|
41
|
+
<button type="button" role="menuitem" data-action="delete" title="Delete (Delete)">Delete</button>
|
|
42
|
+
<button type="button" role="menuitem" data-action="release-layout">Release layout</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="toolbar-more-group" role="presentation">
|
|
45
|
+
<span class="toolbar-more-label">Arrange</span>
|
|
46
|
+
<button type="button" role="menuitem" data-action="order-back" title="Move earlier in declaration order">Send backward</button>
|
|
47
|
+
<button type="button" role="menuitem" data-action="order-front" title="Move later in declaration order">Bring forward</button>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="toolbar-more-group" role="presentation">
|
|
50
|
+
<span class="toolbar-more-label">View</span>
|
|
51
|
+
<div class="toolbar-more-row">
|
|
52
|
+
<button type="button" role="menuitem" data-action="zoom-out" aria-label="Zoom out">−</button>
|
|
53
|
+
<button type="button" role="menuitem" data-action="zoom-fit">Fit</button>
|
|
54
|
+
<button type="button" role="menuitem" data-action="zoom-in" aria-label="Zoom in">+</button>
|
|
55
|
+
</div>
|
|
56
|
+
<label class="snap-toggle"><input id="snapToggle" type="checkbox" checked> Snap to grid</label>
|
|
57
|
+
</div>
|
|
58
|
+
<button type="button" role="menuitem" data-action="reload" title="Reload from Markdown">Reload from Markdown</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
38
61
|
</div>
|
|
39
62
|
</header>
|
|
40
63
|
|
|
41
64
|
<main class="editor-layout">
|
|
42
|
-
<aside class="editor-sidebar element-panel" aria-label="Elements">
|
|
43
|
-
<
|
|
65
|
+
<aside id="elementPanel" class="editor-sidebar element-panel" aria-label="Elements">
|
|
66
|
+
<div class="editor-sidebar-head">
|
|
67
|
+
<h2>Elements</h2>
|
|
68
|
+
<button class="editor-panel-close" type="button" data-panel-close="elements" aria-label="Close Elements">×</button>
|
|
69
|
+
</div>
|
|
44
70
|
<div id="elementTree" class="element-tree" role="tree"></div>
|
|
45
71
|
</aside>
|
|
46
72
|
|
|
@@ -48,8 +74,11 @@
|
|
|
48
74
|
<div id="canvasSurface" class="canvas-surface"></div>
|
|
49
75
|
</section>
|
|
50
76
|
|
|
51
|
-
<aside class="editor-sidebar inspector-panel" aria-label="Properties">
|
|
52
|
-
<
|
|
77
|
+
<aside id="inspectorPanel" class="editor-sidebar inspector-panel" aria-label="Properties">
|
|
78
|
+
<div class="editor-sidebar-head">
|
|
79
|
+
<h2>Properties</h2>
|
|
80
|
+
<button class="editor-panel-close" type="button" data-panel-close="inspector" aria-label="Close Properties">×</button>
|
|
81
|
+
</div>
|
|
53
82
|
<form id="inspector" class="inspector" autocomplete="off"></form>
|
|
54
83
|
</aside>
|
|
55
84
|
</main>
|
|
@@ -91,7 +91,15 @@ const ROUTE_FALLBACK_REMEDIES = Object.freeze({
|
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
const ID_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
94
|
-
const SHAPES = new Set([
|
|
94
|
+
const SHAPES = new Set([
|
|
95
|
+
"rect",
|
|
96
|
+
"rounded-rect",
|
|
97
|
+
"ellipse",
|
|
98
|
+
"diamond",
|
|
99
|
+
"triangle",
|
|
100
|
+
"hexagon",
|
|
101
|
+
"parallelogram",
|
|
102
|
+
]);
|
|
95
103
|
const ROUTINGS = new Set(["straight", "orthogonal", "polyline"]);
|
|
96
104
|
const PORTS = new Set(["auto", "top", "right", "bottom", "left"]);
|
|
97
105
|
const LABEL_LAYERS = new Set(["front", "behind"]);
|
|
@@ -789,6 +797,19 @@ function layoutPlacements(children, group, layout, path, graphEdges = []) {
|
|
|
789
797
|
return placements;
|
|
790
798
|
}
|
|
791
799
|
|
|
800
|
+
export function powerPointDashStyle(value) {
|
|
801
|
+
const pattern = String(value || "").trim();
|
|
802
|
+
if (!pattern) return "solid";
|
|
803
|
+
const values = pattern
|
|
804
|
+
.split(/[ ,]+/)
|
|
805
|
+
.map(Number)
|
|
806
|
+
.filter(Number.isFinite);
|
|
807
|
+
if (values.length >= 2 && values[0] <= 2 && values[1] >= values[0] * 2) {
|
|
808
|
+
return "dotted";
|
|
809
|
+
}
|
|
810
|
+
return "dash";
|
|
811
|
+
}
|
|
812
|
+
|
|
792
813
|
function normalizeBox(element, origin, path, placement) {
|
|
793
814
|
return {
|
|
794
815
|
x:
|
|
@@ -1392,6 +1413,20 @@ function appendText(documentRef, parent, element, text, options = {}) {
|
|
|
1392
1413
|
parent.appendChild(textElement);
|
|
1393
1414
|
}
|
|
1394
1415
|
|
|
1416
|
+
function nodeContentInset(element) {
|
|
1417
|
+
const ratio = {
|
|
1418
|
+
diamond: 0.24,
|
|
1419
|
+
triangle: 0.24,
|
|
1420
|
+
hexagon: 0.14,
|
|
1421
|
+
parallelogram: 0.16,
|
|
1422
|
+
}[element.shape];
|
|
1423
|
+
return ratio ? Math.max(16, element.width * ratio) : 16;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function nodeIconInset(element) {
|
|
1427
|
+
return Math.max(20, nodeContentInset(element), element.width * 0.08);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1395
1430
|
function elementCenter(element) {
|
|
1396
1431
|
return { x: element.x + element.width / 2, y: element.y + element.height / 2 };
|
|
1397
1432
|
}
|
|
@@ -3451,11 +3486,48 @@ function iconShapeAttributes(shape, textColor) {
|
|
|
3451
3486
|
return shape.solid ? { ...shape.attributes, fill: textColor } : shape.attributes;
|
|
3452
3487
|
}
|
|
3453
3488
|
|
|
3489
|
+
function polygonPoints(element) {
|
|
3490
|
+
const { x, y, width, height } = element;
|
|
3491
|
+
const right = x + width;
|
|
3492
|
+
const bottom = y + height;
|
|
3493
|
+
const centerX = x + width / 2;
|
|
3494
|
+
const centerY = y + height / 2;
|
|
3495
|
+
if (element.shape === "diamond") {
|
|
3496
|
+
return `${centerX},${y} ${right},${centerY} ${centerX},${bottom} ${x},${centerY}`;
|
|
3497
|
+
}
|
|
3498
|
+
if (element.shape === "triangle") {
|
|
3499
|
+
return `${centerX},${y} ${right},${bottom} ${x},${bottom}`;
|
|
3500
|
+
}
|
|
3501
|
+
if (element.shape === "hexagon") {
|
|
3502
|
+
const inset = width * 0.25;
|
|
3503
|
+
return `${x + inset},${y} ${right - inset},${y} ${right},${centerY} ${
|
|
3504
|
+
right - inset
|
|
3505
|
+
},${bottom} ${x + inset},${bottom} ${x},${centerY}`;
|
|
3506
|
+
}
|
|
3507
|
+
if (element.shape === "parallelogram") {
|
|
3508
|
+
const inset = Math.min(width * 0.2, height * 0.75);
|
|
3509
|
+
return `${x + inset},${y} ${right},${y} ${right - inset},${bottom} ${x},${bottom}`;
|
|
3510
|
+
}
|
|
3511
|
+
return "";
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function nodePowerPointShape(shape) {
|
|
3515
|
+
return {
|
|
3516
|
+
rect: "rect",
|
|
3517
|
+
"rounded-rect": "roundedRect",
|
|
3518
|
+
ellipse: "ellipse",
|
|
3519
|
+
diamond: "diamond",
|
|
3520
|
+
triangle: "triangle",
|
|
3521
|
+
hexagon: "hexagon",
|
|
3522
|
+
parallelogram: "parallelogram",
|
|
3523
|
+
}[shape];
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3454
3526
|
function renderIcon(documentRef, element) {
|
|
3455
3527
|
if (!element.icon) return null;
|
|
3456
3528
|
const size = Math.min(58, element.height * 0.36, element.width * 0.2);
|
|
3457
3529
|
const x = element.text
|
|
3458
|
-
? element.x +
|
|
3530
|
+
? element.x + nodeIconInset(element)
|
|
3459
3531
|
: element.x + element.width / 2 - size / 2;
|
|
3460
3532
|
const y = element.y + element.height / 2 - size / 2;
|
|
3461
3533
|
const shapes = ICON_SHAPES[element.icon];
|
|
@@ -3481,6 +3553,7 @@ function renderIcon(documentRef, element) {
|
|
|
3481
3553
|
);
|
|
3482
3554
|
return { group, size, x };
|
|
3483
3555
|
}
|
|
3556
|
+
|
|
3484
3557
|
const group = svgElement(documentRef, "g", {
|
|
3485
3558
|
"data-architecture-icon": element.icon,
|
|
3486
3559
|
"data-architecture-icon-source": "builtin",
|
|
@@ -3578,6 +3651,13 @@ function renderNode(documentRef, element) {
|
|
|
3578
3651
|
...common,
|
|
3579
3652
|
}),
|
|
3580
3653
|
);
|
|
3654
|
+
} else if (["diamond", "triangle", "hexagon", "parallelogram"].includes(element.shape)) {
|
|
3655
|
+
group.appendChild(
|
|
3656
|
+
svgElement(documentRef, "polygon", {
|
|
3657
|
+
points: polygonPoints(element),
|
|
3658
|
+
...common,
|
|
3659
|
+
}),
|
|
3660
|
+
);
|
|
3581
3661
|
} else {
|
|
3582
3662
|
group.appendChild(
|
|
3583
3663
|
svgElement(documentRef, "rect", {
|
|
@@ -3594,13 +3674,16 @@ function renderNode(documentRef, element) {
|
|
|
3594
3674
|
if (icon) group.appendChild(icon.group);
|
|
3595
3675
|
if (icon && element.text) {
|
|
3596
3676
|
const textLeft = icon.x + icon.size + 16;
|
|
3597
|
-
const textRight = element.x + element.width -
|
|
3677
|
+
const textRight = element.x + element.width - nodeContentInset(element);
|
|
3598
3678
|
appendText(documentRef, group, element, element.text, {
|
|
3599
3679
|
centerX: (textLeft + textRight) / 2,
|
|
3600
3680
|
availableWidth: Math.max(32, textRight - textLeft),
|
|
3601
3681
|
});
|
|
3602
3682
|
} else {
|
|
3603
|
-
|
|
3683
|
+
const inset = nodeContentInset(element);
|
|
3684
|
+
appendText(documentRef, group, element, element.text, {
|
|
3685
|
+
availableWidth: Math.max(32, element.width - inset * 2),
|
|
3686
|
+
});
|
|
3604
3687
|
}
|
|
3605
3688
|
return group;
|
|
3606
3689
|
}
|
|
@@ -3908,12 +3991,15 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
|
|
|
3908
3991
|
const iconSize = element.icon
|
|
3909
3992
|
? Math.min(58, element.height * 0.36, element.width * 0.2)
|
|
3910
3993
|
: 0;
|
|
3911
|
-
const
|
|
3994
|
+
const contentInset = nodeContentInset(element);
|
|
3995
|
+
const iconX = element.text
|
|
3996
|
+
? element.x + nodeIconInset(element)
|
|
3997
|
+
: element.x + element.width / 2 - iconSize / 2;
|
|
3912
3998
|
const iconY = element.y + element.height / 2 - iconSize / 2;
|
|
3913
3999
|
const textInset = element.icon
|
|
3914
|
-
?
|
|
3915
|
-
:
|
|
3916
|
-
const availableTextWidth = Math.max(32, element.width - textInset -
|
|
4000
|
+
? nodeIconInset(element) + iconSize + 16
|
|
4001
|
+
: contentInset;
|
|
4002
|
+
const availableTextWidth = Math.max(32, element.width - textInset - contentInset);
|
|
3917
4003
|
const longestLine = Math.max(
|
|
3918
4004
|
...element.text.split(/\r?\n/).slice(0, 8).map(textWidthUnits),
|
|
3919
4005
|
1,
|
|
@@ -3924,12 +4010,7 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
|
|
|
3924
4010
|
);
|
|
3925
4011
|
objects.push({
|
|
3926
4012
|
type: "shape",
|
|
3927
|
-
shape:
|
|
3928
|
-
element.shape === "ellipse"
|
|
3929
|
-
? "ellipse"
|
|
3930
|
-
: element.shape === "rounded-rect"
|
|
3931
|
-
? "roundedRect"
|
|
3932
|
-
: "rect",
|
|
4013
|
+
shape: nodePowerPointShape(element.shape),
|
|
3933
4014
|
x: element.x,
|
|
3934
4015
|
y: element.y,
|
|
3935
4016
|
width: element.width,
|
|
@@ -3943,7 +4024,7 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
|
|
|
3943
4024
|
textInsets: {
|
|
3944
4025
|
left: textInset,
|
|
3945
4026
|
top: 12,
|
|
3946
|
-
right:
|
|
4027
|
+
right: contentInset,
|
|
3947
4028
|
bottom: 12,
|
|
3948
4029
|
},
|
|
3949
4030
|
icon: element.icon || "",
|
|
@@ -138,6 +138,17 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
+
<div id="architecturePicker" class="overview" hidden>
|
|
142
|
+
<div class="overview-panel" role="dialog" aria-modal="true" aria-label="Choose an Architecture diagram">
|
|
143
|
+
<div class="overview-head">
|
|
144
|
+
<span>Choose a diagram</span>
|
|
145
|
+
<button id="architecturePickerClose" class="nav-btn" type="button" title="Close (Esc)" aria-label="Close">✕</button>
|
|
146
|
+
</div>
|
|
147
|
+
<p class="import-message">This slide contains multiple Architecture diagrams.</p>
|
|
148
|
+
<ol id="architecturePickerList" class="overview-list"></ol>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
141
152
|
<script src="./vendor/marked.min.js"></script>
|
|
142
153
|
<script src="./vendor/purify.min.js"></script>
|
|
143
154
|
<script src="./vendor/highlight.min.js"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderArchitectureBlock } from "./architecture.mjs";
|
|
1
|
+
import { powerPointDashStyle, renderArchitectureBlock } from "./architecture.mjs";
|
|
2
2
|
import { attachArchitectureEditor } from "./architecture-editor.mjs";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_THEME,
|
|
@@ -703,7 +703,11 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
703
703
|
const source = code.textContent;
|
|
704
704
|
const slideIndex = navIndex;
|
|
705
705
|
if (!architectureEditMode) {
|
|
706
|
-
|
|
706
|
+
const wrapper = renderArchitectureBlock(source, document);
|
|
707
|
+
wrapper.dataset.architectureBlock = String(blockIndex);
|
|
708
|
+
const title = wrapper.__presentationPptxSnapshot?.title;
|
|
709
|
+
if (title) wrapper.dataset.architectureTitle = title;
|
|
710
|
+
target.replaceWith(wrapper);
|
|
707
711
|
return;
|
|
708
712
|
}
|
|
709
713
|
// Insert the editing UI only in editing mode. Normal view never takes this path,
|
|
@@ -780,6 +784,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
780
784
|
}
|
|
781
785
|
|
|
782
786
|
function renderSlide(markdown) {
|
|
787
|
+
closeArchitecturePicker();
|
|
783
788
|
lastMarkdown = typeof markdown === "string" ? markdown : "";
|
|
784
789
|
document.body.classList.toggle("markdstage-empty", !nonEmpty(markdown));
|
|
785
790
|
// Always detach the previous slide's editing UI because it owns document listeners.
|
|
@@ -940,6 +945,60 @@ function textContentBounds(element, deck) {
|
|
|
940
945
|
};
|
|
941
946
|
}
|
|
942
947
|
|
|
948
|
+
function textInsetsFor(element) {
|
|
949
|
+
const style = getComputedStyle(element);
|
|
950
|
+
const metric = (padding, border) => {
|
|
951
|
+
const paddingValue = Number.parseFloat(style[padding]);
|
|
952
|
+
const borderValue = Number.parseFloat(style[border]);
|
|
953
|
+
return roundedMetric(
|
|
954
|
+
Math.max(0, Number.isFinite(paddingValue) ? paddingValue : 0) +
|
|
955
|
+
Math.max(0, Number.isFinite(borderValue) ? borderValue : 0),
|
|
956
|
+
);
|
|
957
|
+
};
|
|
958
|
+
const insets = {
|
|
959
|
+
left: metric("paddingLeft", "borderLeftWidth"),
|
|
960
|
+
top: metric("paddingTop", "borderTopWidth"),
|
|
961
|
+
right: metric("paddingRight", "borderRightWidth"),
|
|
962
|
+
bottom: metric("paddingBottom", "borderBottomWidth"),
|
|
963
|
+
};
|
|
964
|
+
return Object.values(insets).some((value) => value > 0) ? insets : null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function singleLineTextLayout(element, deck, bounds, textInsets, alignment) {
|
|
968
|
+
const content = textContentBounds(element, deck);
|
|
969
|
+
const fontSize = Number.parseFloat(getComputedStyle(element).fontSize);
|
|
970
|
+
const widthBuffer = Math.max(2, (Number.isFinite(fontSize) ? fontSize : 0) * 0.5);
|
|
971
|
+
const fittedInsets = textInsets ? { ...textInsets } : null;
|
|
972
|
+
const requiredWidth = () =>
|
|
973
|
+
content.width +
|
|
974
|
+
(fittedInsets?.left || 0) +
|
|
975
|
+
(fittedInsets?.right || 0) +
|
|
976
|
+
widthBuffer;
|
|
977
|
+
const resolvedAlignment =
|
|
978
|
+
alignment || pptxAlignment(getComputedStyle(element).textAlign);
|
|
979
|
+
// Keep the leading CSS inset, but release a trailing inset when browser and
|
|
980
|
+
// PowerPoint font metrics would otherwise force a visually single line to wrap.
|
|
981
|
+
if (fittedInsets && requiredWidth() > bounds.width) {
|
|
982
|
+
if (resolvedAlignment === "right") fittedInsets.left = 0;
|
|
983
|
+
if (resolvedAlignment === "left") fittedInsets.right = 0;
|
|
984
|
+
}
|
|
985
|
+
const extraWidth = Math.max(0, requiredWidth() - bounds.width);
|
|
986
|
+
const offset =
|
|
987
|
+
resolvedAlignment === "right"
|
|
988
|
+
? extraWidth
|
|
989
|
+
: resolvedAlignment === "center"
|
|
990
|
+
? extraWidth / 2
|
|
991
|
+
: 0;
|
|
992
|
+
return {
|
|
993
|
+
bounds: {
|
|
994
|
+
...bounds,
|
|
995
|
+
x: roundedMetric(bounds.x - offset),
|
|
996
|
+
width: roundedMetric(bounds.width + extraWidth),
|
|
997
|
+
},
|
|
998
|
+
textInsets: fittedInsets,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
943
1002
|
function rasterImageSupported(image) {
|
|
944
1003
|
const source = image.currentSrc || image.getAttribute("src") || "";
|
|
945
1004
|
if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
|
|
@@ -1028,12 +1087,22 @@ function paragraphFor(element, options = {}) {
|
|
|
1028
1087
|
function renderedTextLineCount(element) {
|
|
1029
1088
|
const range = document.createRange();
|
|
1030
1089
|
range.selectNodeContents(element);
|
|
1031
|
-
const
|
|
1090
|
+
const lines = [];
|
|
1032
1091
|
for (const rect of range.getClientRects()) {
|
|
1033
1092
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1034
|
-
|
|
1093
|
+
const line = lines.find(
|
|
1094
|
+
(candidate) =>
|
|
1095
|
+
rect.top < candidate.bottom - 1 &&
|
|
1096
|
+
rect.bottom > candidate.top + 1,
|
|
1097
|
+
);
|
|
1098
|
+
if (line) {
|
|
1099
|
+
line.top = Math.min(line.top, rect.top);
|
|
1100
|
+
line.bottom = Math.max(line.bottom, rect.bottom);
|
|
1101
|
+
} else {
|
|
1102
|
+
lines.push({ top: rect.top, bottom: rect.bottom });
|
|
1103
|
+
}
|
|
1035
1104
|
}
|
|
1036
|
-
return Math.max(1,
|
|
1105
|
+
return Math.max(1, lines.length);
|
|
1037
1106
|
}
|
|
1038
1107
|
|
|
1039
1108
|
function unsupportedEffects(element) {
|
|
@@ -1228,7 +1297,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
|
1228
1297
|
bold: Number(run.fontWeight) >= 600,
|
|
1229
1298
|
})),
|
|
1230
1299
|
}));
|
|
1231
|
-
if (mapped.dash !== undefined) mapped.dash = mapped.dash
|
|
1300
|
+
if (mapped.dash !== undefined) mapped.dash = powerPointDashStyle(mapped.dash);
|
|
1232
1301
|
for (const key of ["fill", "stroke", "color"]) {
|
|
1233
1302
|
if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
|
|
1234
1303
|
}
|
|
@@ -1587,17 +1656,29 @@ async function collectPptxSlide(slide, index) {
|
|
|
1587
1656
|
: undefined,
|
|
1588
1657
|
});
|
|
1589
1658
|
if (!paragraph.runs.some((run) => run.text.trim())) continue;
|
|
1590
|
-
const disableTextWrap =
|
|
1591
|
-
|
|
1592
|
-
|
|
1659
|
+
const disableTextWrap = renderedTextLineCount(element) === 1;
|
|
1660
|
+
const textInsets = textInsetsFor(element);
|
|
1661
|
+
const baseBounds = element.classList.contains("kicker")
|
|
1662
|
+
? textContentBounds(element, deck)
|
|
1663
|
+
: relativeBounds(element, deck);
|
|
1664
|
+
const singleLineLayout = disableTextWrap
|
|
1665
|
+
? singleLineTextLayout(
|
|
1666
|
+
element,
|
|
1667
|
+
deck,
|
|
1668
|
+
baseBounds,
|
|
1669
|
+
textInsets,
|
|
1670
|
+
element.classList.contains("kicker") ? "left" : undefined,
|
|
1671
|
+
)
|
|
1672
|
+
: null;
|
|
1673
|
+
const bounds = singleLineLayout?.bounds || baseBounds;
|
|
1674
|
+
const fittedTextInsets = singleLineLayout?.textInsets || textInsets;
|
|
1593
1675
|
elements.push({
|
|
1594
1676
|
type: "text",
|
|
1595
1677
|
path: elementPath(element, deck),
|
|
1596
|
-
...
|
|
1597
|
-
? textContentBounds(element, deck)
|
|
1598
|
-
: relativeBounds(element, deck)),
|
|
1678
|
+
...bounds,
|
|
1599
1679
|
paragraphs: [paragraph],
|
|
1600
1680
|
opacity: Number(getComputedStyle(element).opacity) || 1,
|
|
1681
|
+
...(fittedTextInsets ? { textInsets: fittedTextInsets } : {}),
|
|
1601
1682
|
...(disableTextWrap ? { textWrap: "none" } : {}),
|
|
1602
1683
|
});
|
|
1603
1684
|
element.setAttribute("data-pptx-native", "text");
|
|
@@ -2072,6 +2153,7 @@ let navTotal = 0;
|
|
|
2072
2153
|
let navMode = "deck";
|
|
2073
2154
|
let overviewOpen = false;
|
|
2074
2155
|
let importOpen = false;
|
|
2156
|
+
let architecturePickerOpen = false;
|
|
2075
2157
|
let importPending = false;
|
|
2076
2158
|
let importFiles = [];
|
|
2077
2159
|
let sourceBacked = false;
|
|
@@ -2149,7 +2231,11 @@ function updateArchitectureEditButton(enabled = architectureEditMode) {
|
|
|
2149
2231
|
if (!button) return;
|
|
2150
2232
|
button.hidden = presenterMode || !architectureEditAvailable;
|
|
2151
2233
|
button.dataset.state = enabled && !presenterMode ? "active" : "";
|
|
2152
|
-
button.title =
|
|
2234
|
+
button.title = architectureDetailedEdit
|
|
2235
|
+
? "Open Architecture Designer"
|
|
2236
|
+
: enabled
|
|
2237
|
+
? "Exit shape editing mode"
|
|
2238
|
+
: "Shape editing mode";
|
|
2153
2239
|
button.setAttribute("aria-label", button.title);
|
|
2154
2240
|
syncMoreControls();
|
|
2155
2241
|
}
|
|
@@ -2238,8 +2324,113 @@ async function requestArchitectureEditMode(enabled) {
|
|
|
2238
2324
|
}
|
|
2239
2325
|
}
|
|
2240
2326
|
|
|
2327
|
+
function architectureDiagramOptions() {
|
|
2328
|
+
return [...document.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
|
|
2329
|
+
(wrapper, index) => ({
|
|
2330
|
+
block: Number(wrapper.dataset.architectureBlock),
|
|
2331
|
+
title: wrapper.dataset.architectureTitle || `Diagram ${index + 1}`,
|
|
2332
|
+
}),
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function closeArchitecturePicker({ restoreFocus = false } = {}) {
|
|
2337
|
+
architecturePickerOpen = false;
|
|
2338
|
+
const picker = document.getElementById("architecturePicker");
|
|
2339
|
+
if (picker) picker.hidden = true;
|
|
2340
|
+
if (restoreFocus) document.getElementById("navEdit")?.focus();
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
async function openArchitectureDesigner(block) {
|
|
2344
|
+
const status = document.getElementById("sourceStatus");
|
|
2345
|
+
if (status) status.textContent = "Opening the Architecture Designer.";
|
|
2346
|
+
const result = await openDetailedArchitectureEditor(navIndex, block);
|
|
2347
|
+
if (!result?.ok && status) {
|
|
2348
|
+
status.textContent = result?.message || "Could not open the Architecture Designer.";
|
|
2349
|
+
}
|
|
2350
|
+
return result;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function openArchitecturePicker(options) {
|
|
2354
|
+
const picker = document.getElementById("architecturePicker");
|
|
2355
|
+
const list = document.getElementById("architecturePickerList");
|
|
2356
|
+
if (!picker || !list) return;
|
|
2357
|
+
list.replaceChildren();
|
|
2358
|
+
for (const [index, option] of options.entries()) {
|
|
2359
|
+
const item = document.createElement("li");
|
|
2360
|
+
item.className = "overview-item";
|
|
2361
|
+
const button = document.createElement("button");
|
|
2362
|
+
button.type = "button";
|
|
2363
|
+
button.className = "overview-link";
|
|
2364
|
+
const number = document.createElement("span");
|
|
2365
|
+
number.className = "overview-num";
|
|
2366
|
+
number.textContent = String(index + 1);
|
|
2367
|
+
const label = document.createElement("span");
|
|
2368
|
+
label.className = "overview-label";
|
|
2369
|
+
label.textContent = option.title;
|
|
2370
|
+
button.append(number, label);
|
|
2371
|
+
button.addEventListener("click", () => {
|
|
2372
|
+
closeArchitecturePicker();
|
|
2373
|
+
void openArchitectureDesigner(option.block);
|
|
2374
|
+
});
|
|
2375
|
+
item.appendChild(button);
|
|
2376
|
+
list.appendChild(item);
|
|
2377
|
+
}
|
|
2378
|
+
architecturePickerOpen = true;
|
|
2379
|
+
picker.hidden = false;
|
|
2380
|
+
list.querySelector("button")?.focus();
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function handleArchitecturePickerKey(event) {
|
|
2384
|
+
const picker = document.getElementById("architecturePicker");
|
|
2385
|
+
const list = document.getElementById("architecturePickerList");
|
|
2386
|
+
if (!picker || !list) return;
|
|
2387
|
+
const focusable = [...picker.querySelectorAll("button:not([disabled])")];
|
|
2388
|
+
const options = [...list.querySelectorAll("button:not([disabled])")];
|
|
2389
|
+
if (event.key === "Tab") {
|
|
2390
|
+
const current = focusable.indexOf(document.activeElement);
|
|
2391
|
+
if (current < 0 || (!event.shiftKey && current === focusable.length - 1)) {
|
|
2392
|
+
event.preventDefault();
|
|
2393
|
+
focusable[0]?.focus();
|
|
2394
|
+
} else if (event.shiftKey && current === 0) {
|
|
2395
|
+
event.preventDefault();
|
|
2396
|
+
focusable.at(-1)?.focus();
|
|
2397
|
+
}
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const current = options.indexOf(document.activeElement);
|
|
2401
|
+
let next = null;
|
|
2402
|
+
if (event.key === "ArrowDown" || event.key === "ArrowRight") {
|
|
2403
|
+
next = options[(Math.max(current, -1) + 1) % options.length];
|
|
2404
|
+
} else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
|
|
2405
|
+
next = options[(current <= 0 ? options.length : current) - 1];
|
|
2406
|
+
} else if (event.key === "Home") {
|
|
2407
|
+
next = options[0];
|
|
2408
|
+
} else if (event.key === "End") {
|
|
2409
|
+
next = options.at(-1);
|
|
2410
|
+
} else if (event.key === "PageDown" || event.key === "PageUp") {
|
|
2411
|
+
event.preventDefault();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (next) {
|
|
2415
|
+
event.preventDefault();
|
|
2416
|
+
next.focus();
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2241
2420
|
async function toggleArchitectureEditMode() {
|
|
2242
2421
|
if (presenterMode || !architectureEditAvailable) return;
|
|
2422
|
+
if (architectureDetailedEdit) {
|
|
2423
|
+
const options = architectureDiagramOptions();
|
|
2424
|
+
if (options.length === 1) {
|
|
2425
|
+
await openArchitectureDesigner(options[0].block);
|
|
2426
|
+
} else if (options.length > 1) {
|
|
2427
|
+
openArchitecturePicker(options);
|
|
2428
|
+
} else {
|
|
2429
|
+
const status = document.getElementById("sourceStatus");
|
|
2430
|
+
if (status) status.textContent = "The current slide has no Architecture diagram to edit.";
|
|
2431
|
+
}
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2243
2434
|
await requestArchitectureEditMode(!architectureEditMode);
|
|
2244
2435
|
await fetchState();
|
|
2245
2436
|
}
|
|
@@ -2422,6 +2613,8 @@ async function fetchState() {
|
|
|
2422
2613
|
if (!architectureEditAvailable) {
|
|
2423
2614
|
availabilityDisabledEditMode = setArchitectureEditMode(false);
|
|
2424
2615
|
}
|
|
2616
|
+
}
|
|
2617
|
+
if (editAvailabilityChanged || detailedEditChanged) {
|
|
2425
2618
|
updateArchitectureEditButton();
|
|
2426
2619
|
}
|
|
2427
2620
|
if (
|
|
@@ -2610,6 +2803,7 @@ function updateHostActionButtons() {
|
|
|
2610
2803
|
if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
|
|
2611
2804
|
if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
|
|
2612
2805
|
if (!markdownImportAvailable && importOpen) closeImportPicker();
|
|
2806
|
+
if (!architectureDetailedEdit && architecturePickerOpen) closeArchitecturePicker();
|
|
2613
2807
|
syncMoreControls();
|
|
2614
2808
|
}
|
|
2615
2809
|
|
|
@@ -3141,6 +3335,7 @@ function wireControls() {
|
|
|
3141
3335
|
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
3142
3336
|
bind("overviewClose", closeOverview);
|
|
3143
3337
|
bind("importClose", closeImportPicker);
|
|
3338
|
+
bind("architecturePickerClose", () => closeArchitecturePicker({ restoreFocus: true }));
|
|
3144
3339
|
bind("presenterPrevButton", goPrev);
|
|
3145
3340
|
bind("presenterNextButton", goNext);
|
|
3146
3341
|
bind("presenterListButton", openOverview);
|
|
@@ -3166,6 +3361,13 @@ function wireControls() {
|
|
|
3166
3361
|
});
|
|
3167
3362
|
}
|
|
3168
3363
|
|
|
3364
|
+
const architecturePicker = document.getElementById("architecturePicker");
|
|
3365
|
+
if (architecturePicker) {
|
|
3366
|
+
architecturePicker.addEventListener("click", (e) => {
|
|
3367
|
+
if (e.target === architecturePicker) closeArchitecturePicker({ restoreFocus: true });
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3169
3371
|
document.addEventListener("pointerdown", (e) => {
|
|
3170
3372
|
if (!moreControlsOpen) return;
|
|
3171
3373
|
const tools = document.getElementById("navTools");
|
|
@@ -3196,6 +3398,15 @@ function wireControls() {
|
|
|
3196
3398
|
e.preventDefault();
|
|
3197
3399
|
return;
|
|
3198
3400
|
}
|
|
3401
|
+
if (e.key === "Escape" && architecturePickerOpen) {
|
|
3402
|
+
closeArchitecturePicker({ restoreFocus: true });
|
|
3403
|
+
e.preventDefault();
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
if (architecturePickerOpen) {
|
|
3407
|
+
handleArchitecturePickerKey(e);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3199
3410
|
if (e.key === "Escape" && moreControlsOpen) {
|
|
3200
3411
|
setMoreControlsOpen(false, { restoreFocus: true });
|
|
3201
3412
|
e.preventDefault();
|
|
@@ -3220,6 +3431,9 @@ function wireControls() {
|
|
|
3220
3431
|
if (importOpen) {
|
|
3221
3432
|
closeImportPicker();
|
|
3222
3433
|
e.preventDefault();
|
|
3434
|
+
} else if (architecturePickerOpen) {
|
|
3435
|
+
closeArchitecturePicker({ restoreFocus: true });
|
|
3436
|
+
e.preventDefault();
|
|
3223
3437
|
} else if (overviewOpen) {
|
|
3224
3438
|
closeOverview();
|
|
3225
3439
|
e.preventDefault();
|