@jxsuite/studio 0.7.0 → 0.9.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/dist/studio.js +125373 -124927
- package/dist/studio.js.map +87 -73
- package/package.json +2 -2
- package/src/canvas/canvas-utils.js +313 -0
- package/src/editor/component-inline-edit.js +316 -0
- package/src/editor/content-inline-edit.js +220 -0
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/canvas-dnd.js +165 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/left-panel.js +3 -2
- package/src/panels/panel-events.js +263 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/pseudo-preview.js +64 -0
- package/src/panels/right-panel.js +2 -1
- package/src/panels/shared.js +74 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/studio.js +121 -4874
- package/src/utils/edit-display.js +197 -0
package/src/editor/slash-menu.js
CHANGED
|
@@ -34,12 +34,16 @@ const SLASH_COMMANDS = [
|
|
|
34
34
|
const host = document.createElement("div");
|
|
35
35
|
host.style.display = "contents";
|
|
36
36
|
|
|
37
|
-
/** @type {{ onSelect: (cmd: any) => void } | null} */
|
|
37
|
+
/** @type {{ onSelect: (cmd: any) => void; showFilter?: boolean } | null} */
|
|
38
38
|
let callbacks = null;
|
|
39
39
|
let activeIdx = 0;
|
|
40
40
|
/** @type {any[]} */
|
|
41
41
|
let filteredItems = [];
|
|
42
42
|
let open = false;
|
|
43
|
+
/** @type {HTMLElement | null} */
|
|
44
|
+
let _anchorEl = null;
|
|
45
|
+
/** @type {DOMRect | null} */
|
|
46
|
+
let _anchorRect = null;
|
|
43
47
|
|
|
44
48
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
45
49
|
|
|
@@ -53,7 +57,7 @@ export function isSlashMenuOpen() {
|
|
|
53
57
|
*
|
|
54
58
|
* @param {HTMLElement} anchorEl — the element being edited (for positioning)
|
|
55
59
|
* @param {string} filter — current typed filter text (after the "/")
|
|
56
|
-
* @param {{ onSelect: (cmd: any) => void }} cbs
|
|
60
|
+
* @param {{ onSelect: (cmd: any) => void; showFilter?: boolean }} cbs
|
|
57
61
|
*/
|
|
58
62
|
export function showSlashMenu(anchorEl, filter, cbs) {
|
|
59
63
|
// Lazily attach host to sp-theme
|
|
@@ -62,6 +66,8 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
callbacks = cbs;
|
|
69
|
+
_anchorEl = anchorEl;
|
|
70
|
+
_anchorRect = anchorEl.getBoundingClientRect();
|
|
65
71
|
|
|
66
72
|
filteredItems = filter
|
|
67
73
|
? SLASH_COMMANDS.filter(
|
|
@@ -69,49 +75,28 @@ export function showSlashMenu(anchorEl, filter, cbs) {
|
|
|
69
75
|
)
|
|
70
76
|
: SLASH_COMMANDS;
|
|
71
77
|
|
|
72
|
-
if (!filteredItems.length) {
|
|
78
|
+
if (!filteredItems.length && !cbs.showFilter) {
|
|
73
79
|
dismissSlashMenu();
|
|
74
80
|
return;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
activeIdx = 0;
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
litRender(
|
|
82
|
-
html`
|
|
83
|
-
<sp-popover
|
|
84
|
-
open
|
|
85
|
-
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
86
|
-
4}px;z-index:9999;max-height:280px;overflow-y:auto"
|
|
87
|
-
>
|
|
88
|
-
<sp-menu style="min-width:220px">
|
|
89
|
-
${filteredItems.map(
|
|
90
|
-
(cmd, i) => html`
|
|
91
|
-
<sp-menu-item
|
|
92
|
-
?focused=${i === 0}
|
|
93
|
-
@click=${(/** @type {Event} */ e) => {
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
e.stopPropagation();
|
|
96
|
-
select(cmd);
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
${cmd.label}
|
|
100
|
-
${cmd.description
|
|
101
|
-
? html`<span slot="description">${cmd.description}</span>`
|
|
102
|
-
: nothing}
|
|
103
|
-
</sp-menu-item>
|
|
104
|
-
`,
|
|
105
|
-
)}
|
|
106
|
-
</sp-menu>
|
|
107
|
-
</sp-popover>
|
|
108
|
-
`,
|
|
109
|
-
host,
|
|
110
|
-
);
|
|
85
|
+
render(anchorEl, cbs.showFilter || false);
|
|
111
86
|
|
|
112
87
|
if (!open) {
|
|
113
88
|
open = true;
|
|
114
89
|
document.addEventListener("keydown", onKeydown, true); // capture phase
|
|
90
|
+
requestAnimationFrame(() => {
|
|
91
|
+
document.addEventListener("mousedown", onOutsideClick, true);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cbs.showFilter) {
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
const input = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
|
|
98
|
+
if (input) input.focus();
|
|
99
|
+
});
|
|
115
100
|
}
|
|
116
101
|
}
|
|
117
102
|
|
|
@@ -119,13 +104,75 @@ export function dismissSlashMenu() {
|
|
|
119
104
|
if (!open) return;
|
|
120
105
|
open = false;
|
|
121
106
|
callbacks = null;
|
|
107
|
+
_anchorEl = null;
|
|
108
|
+
_anchorRect = null;
|
|
122
109
|
filteredItems = [];
|
|
123
110
|
document.removeEventListener("keydown", onKeydown, true);
|
|
111
|
+
document.removeEventListener("mousedown", onOutsideClick, true);
|
|
124
112
|
litRender(nothing, host);
|
|
125
113
|
}
|
|
126
114
|
|
|
127
115
|
// ─── Internal ─────────────────────────────────────────────────────────────────
|
|
128
116
|
|
|
117
|
+
/**
|
|
118
|
+
* @param {HTMLElement} anchorEl
|
|
119
|
+
* @param {boolean} showFilter
|
|
120
|
+
*/
|
|
121
|
+
function render(anchorEl, showFilter) {
|
|
122
|
+
const rect = _anchorRect || anchorEl.getBoundingClientRect();
|
|
123
|
+
|
|
124
|
+
litRender(
|
|
125
|
+
html`
|
|
126
|
+
<sp-popover
|
|
127
|
+
open
|
|
128
|
+
style="position:fixed;left:${rect.left}px;top:${rect.bottom +
|
|
129
|
+
4}px;z-index:9999;max-height:320px;overflow-y:auto"
|
|
130
|
+
>
|
|
131
|
+
${showFilter
|
|
132
|
+
? html`<input
|
|
133
|
+
class="slash-filter"
|
|
134
|
+
type="text"
|
|
135
|
+
placeholder="Filter…"
|
|
136
|
+
autocomplete="off"
|
|
137
|
+
style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
|
|
138
|
+
@input=${onFilterInput}
|
|
139
|
+
/>`
|
|
140
|
+
: nothing}
|
|
141
|
+
<sp-menu style="min-width:220px">
|
|
142
|
+
${filteredItems.length
|
|
143
|
+
? filteredItems.map(
|
|
144
|
+
(cmd, i) => html`
|
|
145
|
+
<sp-menu-item
|
|
146
|
+
?focused=${i === 0}
|
|
147
|
+
@click=${(/** @type {Event} */ e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
select(cmd);
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
${cmd.label}
|
|
154
|
+
${cmd.description
|
|
155
|
+
? html`<span slot="description">${cmd.description}</span>`
|
|
156
|
+
: nothing}
|
|
157
|
+
</sp-menu-item>
|
|
158
|
+
`,
|
|
159
|
+
)
|
|
160
|
+
: html`<sp-menu-item disabled>No matches</sp-menu-item>`}
|
|
161
|
+
</sp-menu>
|
|
162
|
+
</sp-popover>
|
|
163
|
+
`,
|
|
164
|
+
host,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {MouseEvent} e */
|
|
169
|
+
function onOutsideClick(e) {
|
|
170
|
+
const popover = host.querySelector("sp-popover");
|
|
171
|
+
if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
|
|
172
|
+
dismissSlashMenu();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
/** @param {any} cmd */
|
|
130
177
|
function select(cmd) {
|
|
131
178
|
const cbs = callbacks;
|
|
@@ -133,16 +180,42 @@ function select(cmd) {
|
|
|
133
180
|
cbs?.onSelect(cmd);
|
|
134
181
|
}
|
|
135
182
|
|
|
183
|
+
/** @param {Event} e */
|
|
184
|
+
function onFilterInput(e) {
|
|
185
|
+
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
186
|
+
const filter = input.value.toLowerCase();
|
|
187
|
+
|
|
188
|
+
filteredItems = filter
|
|
189
|
+
? SLASH_COMMANDS.filter(
|
|
190
|
+
(c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
|
|
191
|
+
)
|
|
192
|
+
: SLASH_COMMANDS;
|
|
193
|
+
|
|
194
|
+
activeIdx = 0;
|
|
195
|
+
if (_anchorEl) render(_anchorEl, true);
|
|
196
|
+
|
|
197
|
+
// Re-focus input after re-render
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
const el = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
|
|
200
|
+
if (el && el !== document.activeElement) {
|
|
201
|
+
el.focus();
|
|
202
|
+
el.selectionStart = el.selectionEnd = el.value.length;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
136
207
|
/** @param {KeyboardEvent} e */
|
|
137
208
|
function onKeydown(e) {
|
|
138
209
|
if (!open) return;
|
|
139
210
|
|
|
140
|
-
const items = /** @type {NodeListOf<Element>} */ (
|
|
141
|
-
|
|
211
|
+
const items = /** @type {NodeListOf<Element>} */ (
|
|
212
|
+
host.querySelectorAll("sp-menu-item:not([disabled])")
|
|
213
|
+
);
|
|
142
214
|
|
|
143
215
|
if (e.key === "ArrowDown") {
|
|
144
216
|
e.preventDefault();
|
|
145
217
|
e.stopPropagation();
|
|
218
|
+
if (!items.length) return;
|
|
146
219
|
items[activeIdx]?.removeAttribute("focused");
|
|
147
220
|
activeIdx = (activeIdx + 1) % items.length;
|
|
148
221
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -150,6 +223,7 @@ function onKeydown(e) {
|
|
|
150
223
|
} else if (e.key === "ArrowUp") {
|
|
151
224
|
e.preventDefault();
|
|
152
225
|
e.stopPropagation();
|
|
226
|
+
if (!items.length) return;
|
|
153
227
|
items[activeIdx]?.removeAttribute("focused");
|
|
154
228
|
activeIdx = (activeIdx - 1 + items.length) % items.length;
|
|
155
229
|
items[activeIdx]?.setAttribute("focused", "");
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block action bar — extracted from studio.js (Phase 4h). Floating toolbar above selected elements
|
|
3
|
+
* with parent selector, move arrows, drag handle, component actions, and inline formatting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
7
|
+
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
getState,
|
|
11
|
+
update,
|
|
12
|
+
updateSession,
|
|
13
|
+
renderOnly,
|
|
14
|
+
selectNode,
|
|
15
|
+
moveNode,
|
|
16
|
+
getNodeAtPath,
|
|
17
|
+
nodeLabel,
|
|
18
|
+
parentElementPath,
|
|
19
|
+
childIndex,
|
|
20
|
+
} from "../store.js";
|
|
21
|
+
import { view } from "../view.js";
|
|
22
|
+
import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
|
|
23
|
+
import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
|
|
24
|
+
import { componentRegistry } from "../files/components.js";
|
|
25
|
+
import { convertToComponent } from "../editor/convert-to-component.js";
|
|
26
|
+
|
|
27
|
+
/** @type {any} */
|
|
28
|
+
let _ctx = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize the block action bar module.
|
|
32
|
+
*
|
|
33
|
+
* @param {{
|
|
34
|
+
* getCanvasMode: () => string;
|
|
35
|
+
* findCanvasElement: Function;
|
|
36
|
+
* getActivePanel: Function;
|
|
37
|
+
* navigateToComponent: Function;
|
|
38
|
+
* createFloatingContainer: Function;
|
|
39
|
+
* }} ctx
|
|
40
|
+
*/
|
|
41
|
+
export function initBlockActionBar(ctx) {
|
|
42
|
+
_ctx = ctx;
|
|
43
|
+
view.linkPopoverHost = document.createElement("div");
|
|
44
|
+
view.linkPopoverHost.style.display = "contents";
|
|
45
|
+
(document.querySelector("sp-theme") || document.body).appendChild(view.linkPopoverHost);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
|
|
49
|
+
const formatIconMap = /** @type {Record<string, any>} */ ({
|
|
50
|
+
"sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
|
|
51
|
+
"sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
|
|
52
|
+
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
53
|
+
"sp-icon-text-strikethrough": html`<sp-icon-text-strikethrough
|
|
54
|
+
slot="icon"
|
|
55
|
+
></sp-icon-text-strikethrough>`,
|
|
56
|
+
"sp-icon-text-superscript": html`<sp-icon-text-superscript
|
|
57
|
+
slot="icon"
|
|
58
|
+
></sp-icon-text-superscript>`,
|
|
59
|
+
"sp-icon-text-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
|
|
60
|
+
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
61
|
+
"sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prevent the bar from stealing focus from contenteditable
|
|
66
|
+
*
|
|
67
|
+
* @param {any} e
|
|
68
|
+
*/
|
|
69
|
+
function onBarMousedown(e) {
|
|
70
|
+
if (e.target.closest("sp-textfield")) return;
|
|
71
|
+
if (e.target.closest(".bar-drag-handle")) return;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Saved selection range for format button mousedown→click flow */
|
|
76
|
+
function captureSelectionRange() {
|
|
77
|
+
const sel = window.getSelection();
|
|
78
|
+
if (sel && sel.rangeCount) view.savedRange = sel.getRangeAt(0).cloneRange();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {any} e
|
|
83
|
+
* @param {any} action
|
|
84
|
+
*/
|
|
85
|
+
function onFormatClick(e, action) {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
if (action.command === "link") {
|
|
88
|
+
showLinkPopover(e.target.closest("sp-action-button"));
|
|
89
|
+
} else if (view.savedRange) {
|
|
90
|
+
const sel = /** @type {any} */ (window.getSelection());
|
|
91
|
+
const anchor = view.savedRange.startContainer;
|
|
92
|
+
const editableRoot = (
|
|
93
|
+
anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement
|
|
94
|
+
)?.closest("[contenteditable]");
|
|
95
|
+
if (editableRoot) {
|
|
96
|
+
editableRoot.focus();
|
|
97
|
+
sel.removeAllRanges();
|
|
98
|
+
sel.addRange(view.savedRange);
|
|
99
|
+
applyInlineFormat(action);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderParentSelector() {
|
|
105
|
+
const S = getState();
|
|
106
|
+
const pPath = parentElementPath(S.selection);
|
|
107
|
+
if (!pPath) return nothing;
|
|
108
|
+
const parentNode = getNodeAtPath(S.document, pPath);
|
|
109
|
+
return html`
|
|
110
|
+
<sp-action-button
|
|
111
|
+
size="xs"
|
|
112
|
+
quiet
|
|
113
|
+
title="Select parent: ${nodeLabel(parentNode)}"
|
|
114
|
+
@click=${(/** @type {any} */ e) => {
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
const S = getState();
|
|
117
|
+
update(selectNode(S, pPath));
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<sp-icon-back slot="icon"></sp-icon-back>
|
|
121
|
+
</sp-action-button>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderMoveArrows() {
|
|
126
|
+
const S = getState();
|
|
127
|
+
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
128
|
+
const pPath = parentElementPath(S.selection);
|
|
129
|
+
const parentNode = getNodeAtPath(S.document, /** @type {any} */ (pPath));
|
|
130
|
+
const siblings = parentNode?.children;
|
|
131
|
+
return html`
|
|
132
|
+
<sp-action-button
|
|
133
|
+
size="xs"
|
|
134
|
+
quiet
|
|
135
|
+
title="Move up"
|
|
136
|
+
?disabled=${idx <= 0}
|
|
137
|
+
@click=${(/** @type {any} */ e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
moveSelectionUp();
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
|
|
143
|
+
</sp-action-button>
|
|
144
|
+
<sp-action-button
|
|
145
|
+
size="xs"
|
|
146
|
+
quiet
|
|
147
|
+
title="Move down"
|
|
148
|
+
?disabled=${!siblings || idx >= siblings.length - 1}
|
|
149
|
+
@click=${(/** @type {any} */ e) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
moveSelectionDown();
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
|
|
155
|
+
</sp-action-button>
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Apply an inline format action.
|
|
161
|
+
*
|
|
162
|
+
* @param {any} action
|
|
163
|
+
*/
|
|
164
|
+
function applyInlineFormat(action) {
|
|
165
|
+
/** @type {Record<string, any>} */
|
|
166
|
+
const cmdToTag = {
|
|
167
|
+
bold: "strong",
|
|
168
|
+
italic: "em",
|
|
169
|
+
underline: "u",
|
|
170
|
+
strikethrough: "del",
|
|
171
|
+
superscript: "sup",
|
|
172
|
+
subscript: "sub",
|
|
173
|
+
code: "code",
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const tag = cmdToTag[action.command];
|
|
177
|
+
if (tag) {
|
|
178
|
+
const editableRoot = getActiveElement();
|
|
179
|
+
toggleInlineFormat(tag, editableRoot);
|
|
180
|
+
}
|
|
181
|
+
requestAnimationFrame(() => renderBlockActionBar());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Dismiss the link popover if open. */
|
|
185
|
+
export function dismissLinkPopover() {
|
|
186
|
+
if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @param {any} anchorBtn */
|
|
190
|
+
function showLinkPopover(anchorBtn) {
|
|
191
|
+
litRender(nothing, view.linkPopoverHost);
|
|
192
|
+
|
|
193
|
+
const sel = window.getSelection();
|
|
194
|
+
/** @type {any} */
|
|
195
|
+
let existingLink = null;
|
|
196
|
+
if (sel?.rangeCount) {
|
|
197
|
+
/** @type {any} */
|
|
198
|
+
let node = sel.anchorNode;
|
|
199
|
+
while (node && node !== document.body) {
|
|
200
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "a") {
|
|
201
|
+
existingLink = node;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
node = node.parentNode;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const rect = anchorBtn.getBoundingClientRect();
|
|
209
|
+
|
|
210
|
+
const onApply = () => {
|
|
211
|
+
const field = view.linkPopoverHost.querySelector("sp-textfield");
|
|
212
|
+
const url = /** @type {any} */ (field)?.value;
|
|
213
|
+
if (existingLink) {
|
|
214
|
+
existingLink.setAttribute("href", url);
|
|
215
|
+
} else if (url) {
|
|
216
|
+
document.execCommand("createLink", false, url);
|
|
217
|
+
}
|
|
218
|
+
litRender(nothing, view.linkPopoverHost);
|
|
219
|
+
renderBlockActionBar();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onRemove = () => {
|
|
223
|
+
const frag = document.createDocumentFragment();
|
|
224
|
+
while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
|
|
225
|
+
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
226
|
+
litRender(nothing, view.linkPopoverHost);
|
|
227
|
+
renderBlockActionBar();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const onKeydown = (/** @type {any} */ e) => {
|
|
231
|
+
if (e.key === "Enter") onApply();
|
|
232
|
+
else if (e.key === "Escape") {
|
|
233
|
+
litRender(nothing, view.linkPopoverHost);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
litRender(
|
|
238
|
+
html`
|
|
239
|
+
<sp-popover
|
|
240
|
+
class="link-popover"
|
|
241
|
+
open
|
|
242
|
+
style="position:fixed; left:${rect.left}px; top:${rect.bottom + 4}px; z-index:30"
|
|
243
|
+
>
|
|
244
|
+
<sp-textfield
|
|
245
|
+
placeholder="https://..."
|
|
246
|
+
size="s"
|
|
247
|
+
style="width:200px"
|
|
248
|
+
value=${existingLink?.getAttribute("href") || ""}
|
|
249
|
+
@keydown=${onKeydown}
|
|
250
|
+
></sp-textfield>
|
|
251
|
+
<sp-action-button size="xs" @click=${onApply}>
|
|
252
|
+
${existingLink ? "Update" : "Apply"}
|
|
253
|
+
</sp-action-button>
|
|
254
|
+
${existingLink
|
|
255
|
+
? html` <sp-action-button size="xs" @click=${onRemove}>Remove</sp-action-button> `
|
|
256
|
+
: nothing}
|
|
257
|
+
</sp-popover>
|
|
258
|
+
`,
|
|
259
|
+
view.linkPopoverHost,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
requestAnimationFrame(
|
|
263
|
+
() =>
|
|
264
|
+
/** @type {HTMLElement | null} */ (
|
|
265
|
+
view.linkPopoverHost?.querySelector("sp-textfield")
|
|
266
|
+
)?.focus(),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Move the selected node up (swap with previous sibling). */
|
|
271
|
+
function moveSelectionUp() {
|
|
272
|
+
const S = getState();
|
|
273
|
+
if (!S.selection || S.selection.length < 2) return;
|
|
274
|
+
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
275
|
+
if (idx <= 0) return;
|
|
276
|
+
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
277
|
+
update(moveNode(S, S.selection, pPath, idx - 1));
|
|
278
|
+
updateSession({ selection: [...pPath, "children", idx - 1] });
|
|
279
|
+
renderOnly("overlays");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Move the selected node down (swap with next sibling). */
|
|
283
|
+
function moveSelectionDown() {
|
|
284
|
+
const S = getState();
|
|
285
|
+
if (!S.selection || S.selection.length < 2) return;
|
|
286
|
+
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
287
|
+
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
288
|
+
const parentNode = getNodeAtPath(S.document, pPath);
|
|
289
|
+
const siblings = parentNode?.children;
|
|
290
|
+
if (!siblings || idx >= siblings.length - 1) return;
|
|
291
|
+
update(moveNode(S, S.selection, pPath, idx + 2));
|
|
292
|
+
updateSession({ selection: [...pPath, "children", idx + 1] });
|
|
293
|
+
renderOnly("overlays");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Render the unified block action bar above the selected element. */
|
|
297
|
+
export function renderBlockActionBar() {
|
|
298
|
+
if (!view.blockActionBarEl) {
|
|
299
|
+
view.blockActionBarEl = _ctx.createFloatingContainer();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (view.selDragCleanup) {
|
|
303
|
+
view.selDragCleanup();
|
|
304
|
+
view.selDragCleanup = null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const S = getState();
|
|
308
|
+
const canvasMode = _ctx.getCanvasMode();
|
|
309
|
+
|
|
310
|
+
if (!S.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
311
|
+
litRender(nothing, view.blockActionBarEl);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const activePanel = _ctx.getActivePanel();
|
|
316
|
+
if (!activePanel) {
|
|
317
|
+
litRender(nothing, view.blockActionBarEl);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const el = _ctx.findCanvasElement(S.selection, activePanel.canvas);
|
|
321
|
+
const node = el && getNodeAtPath(S.document, S.selection);
|
|
322
|
+
if (!el || !node) {
|
|
323
|
+
litRender(nothing, view.blockActionBarEl);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const tag = (node.tagName ?? "div").toLowerCase();
|
|
328
|
+
const elRect = el.getBoundingClientRect();
|
|
329
|
+
const topPos = elRect.top < 80 ? elRect.bottom + 4 : elRect.top - 38;
|
|
330
|
+
|
|
331
|
+
// Inline format state
|
|
332
|
+
const inlineEditing = isEditing() || el.contentEditable === "true";
|
|
333
|
+
const actions = getInlineActions(tag) || [];
|
|
334
|
+
const showFormat = inlineEditing && actions.length > 0;
|
|
335
|
+
const activeValues = showFormat
|
|
336
|
+
? actions.filter((a) => isTagActiveInSelection(a.tag, el)).map((a) => a.tag)
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
litRender(
|
|
340
|
+
html`
|
|
341
|
+
<div
|
|
342
|
+
class="block-action-bar"
|
|
343
|
+
style="left:${elRect.left}px; top:${topPos}px"
|
|
344
|
+
@mousedown=${onBarMousedown}
|
|
345
|
+
>
|
|
346
|
+
${S.selection.length >= 2 ? renderParentSelector() : nothing}
|
|
347
|
+
|
|
348
|
+
<span class="bar-tag">${node.$id || (node.tagName ?? "div")}</span>
|
|
349
|
+
|
|
350
|
+
${S.selection.length >= 2
|
|
351
|
+
? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
|
|
352
|
+
: nothing}
|
|
353
|
+
${S.selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
354
|
+
${S.selection.length >= 2 && node.tagName
|
|
355
|
+
? (() => {
|
|
356
|
+
const isComp =
|
|
357
|
+
node.tagName.includes("-") &&
|
|
358
|
+
componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
359
|
+
if (isComp) {
|
|
360
|
+
const comp = componentRegistry.find(
|
|
361
|
+
(/** @type {any} */ c) => c.tagName === node.tagName,
|
|
362
|
+
);
|
|
363
|
+
return html`<sp-action-button
|
|
364
|
+
size="xs"
|
|
365
|
+
quiet
|
|
366
|
+
title="Edit Component"
|
|
367
|
+
@click=${() => _ctx.navigateToComponent(comp.path)}
|
|
368
|
+
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
369
|
+
></sp-action-button>`;
|
|
370
|
+
}
|
|
371
|
+
return html`<sp-action-button
|
|
372
|
+
size="xs"
|
|
373
|
+
quiet
|
|
374
|
+
title="Convert to Component"
|
|
375
|
+
@click=${() => convertToComponent(S)}
|
|
376
|
+
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
377
|
+
></sp-action-button>`;
|
|
378
|
+
})()
|
|
379
|
+
: nothing}
|
|
380
|
+
${showFormat
|
|
381
|
+
? html`
|
|
382
|
+
<sp-divider size="s" vertical></sp-divider>
|
|
383
|
+
<sp-action-group
|
|
384
|
+
size="xs"
|
|
385
|
+
compact
|
|
386
|
+
emphasized
|
|
387
|
+
selects="multiple"
|
|
388
|
+
selected=${activeValues.length ? JSON.stringify(activeValues) : nothing}
|
|
389
|
+
>
|
|
390
|
+
${actions.map(
|
|
391
|
+
(action) => html`
|
|
392
|
+
<sp-action-button
|
|
393
|
+
size="xs"
|
|
394
|
+
value=${action.tag}
|
|
395
|
+
title="${action.label}${action.shortcut ? ` (${action.shortcut})` : ""}"
|
|
396
|
+
@mousedown=${captureSelectionRange}
|
|
397
|
+
@click=${(/** @type {any} */ e) => onFormatClick(e, action)}
|
|
398
|
+
>
|
|
399
|
+
${formatIconMap[action.icon] ?? nothing}
|
|
400
|
+
</sp-action-button>
|
|
401
|
+
`,
|
|
402
|
+
)}
|
|
403
|
+
</sp-action-group>
|
|
404
|
+
`
|
|
405
|
+
: nothing}
|
|
406
|
+
</div>
|
|
407
|
+
`,
|
|
408
|
+
view.blockActionBarEl,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Post-render side effects
|
|
412
|
+
requestAnimationFrame(() => {
|
|
413
|
+
const bar = view.blockActionBarEl?.firstElementChild;
|
|
414
|
+
if (!bar) return;
|
|
415
|
+
// Clamp to window
|
|
416
|
+
const barRect = bar.getBoundingClientRect();
|
|
417
|
+
if (barRect.right > window.innerWidth) {
|
|
418
|
+
bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
|
|
419
|
+
}
|
|
420
|
+
// Attach drag handle
|
|
421
|
+
const currentS = getState();
|
|
422
|
+
if (currentS.selection?.length >= 2) {
|
|
423
|
+
const handle = bar.querySelector(".bar-drag-handle");
|
|
424
|
+
if (handle) {
|
|
425
|
+
if (view.selDragCleanup) {
|
|
426
|
+
view.selDragCleanup();
|
|
427
|
+
view.selDragCleanup = null;
|
|
428
|
+
}
|
|
429
|
+
view.selDragCleanup = draggable({
|
|
430
|
+
element: handle,
|
|
431
|
+
getInitialData: () => ({ type: "tree-node", path: getState().selection }),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|