@upstart.gg/vite-plugins 0.1.84 → 0.1.85
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/upstart-editor-api.d.ts +58 -1
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +224 -1
- package/dist/upstart-editor-api.js.map +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +65 -2
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +2 -24
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +7 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js +33 -0
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +23 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.js +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js +434 -0
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +6 -3
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tests/move-controls.test.ts +86 -0
- package/src/tests/upstart-editor-api-move-element.test.ts +220 -0
- package/src/tests/vite-plugin-upstart-attrs.test.ts +165 -4
- package/src/upstart-editor-api.ts +263 -0
- package/src/vite-plugin-upstart-attrs.ts +93 -2
- package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +2 -40
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +17 -3
- package/src/vite-plugin-upstart-editor/runtime/controls-ui.ts +48 -0
- package/src/vite-plugin-upstart-editor/runtime/index.ts +31 -0
- package/src/vite-plugin-upstart-editor/runtime/labels.ts +10 -0
- package/src/vite-plugin-upstart-editor/runtime/move-controls.ts +536 -0
- package/src/vite-plugin-upstart-editor/runtime/types.ts +7 -2
|
@@ -70,7 +70,8 @@ export interface EditableSegment {
|
|
|
70
70
|
// Registry entry for editable elements (text, rich-text, className, mixed-text)
|
|
71
71
|
export interface EditableEntry {
|
|
72
72
|
file: string;
|
|
73
|
-
|
|
73
|
+
// "element": the full source range of a JSX element the editor can move (drag & drop).
|
|
74
|
+
type: "text" | "rich-text" | "className" | "mixed-text" | "image" | "element";
|
|
74
75
|
startOffset: number;
|
|
75
76
|
endOffset: number;
|
|
76
77
|
originalContent: string;
|
|
@@ -120,6 +121,8 @@ interface TransformState {
|
|
|
120
121
|
* `function Hero({ heroImageSrc = "/images/hero.webp" })`.
|
|
121
122
|
*/
|
|
122
123
|
paramDefaults: Map<string, StringLiteralNode>[];
|
|
124
|
+
/** Start offsets of the enclosing functions, innermost last — used as the drag & drop scope. */
|
|
125
|
+
functionStack: number[];
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
type StringLiteralNode = NodeWithRange & { type: "Literal"; value: string };
|
|
@@ -290,13 +293,14 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
290
293
|
tFunctions: new Map(),
|
|
291
294
|
constantNodes: new Map(),
|
|
292
295
|
paramDefaults: [],
|
|
296
|
+
functionStack: [],
|
|
293
297
|
};
|
|
294
298
|
|
|
295
299
|
let modified = false;
|
|
296
300
|
|
|
297
301
|
// Use zimmerframe to walk and transform the AST
|
|
298
302
|
walk(ast.program as Program, state, {
|
|
299
|
-
_(node: Node, { state, next }: { state: TransformState; next: () => void }) {
|
|
303
|
+
_(node: Node, { state, next, path }: { state: TransformState; next: () => void; path: Node[] }) {
|
|
300
304
|
// Track useTranslation() calls to resolve t() function namespaces
|
|
301
305
|
if (node.type === "VariableDeclaration") {
|
|
302
306
|
trackUseTranslation(node, state);
|
|
@@ -326,7 +330,9 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
326
330
|
node.type === "ArrowFunctionExpression"
|
|
327
331
|
) {
|
|
328
332
|
state.paramDefaults.push(collectParamStringDefaults((node as any).params ?? []));
|
|
333
|
+
state.functionStack.push(hasRange(node) ? node.start : -1);
|
|
329
334
|
next();
|
|
335
|
+
state.functionStack.pop();
|
|
330
336
|
state.paramDefaults.pop();
|
|
331
337
|
return;
|
|
332
338
|
}
|
|
@@ -715,6 +721,44 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
715
721
|
}
|
|
716
722
|
}
|
|
717
723
|
|
|
724
|
+
// Movable element (editor drag & drop): any HTML element that is a direct child of
|
|
725
|
+
// another JSX element, so moving its source range keeps valid JSX. Elements rendered in
|
|
726
|
+
// a loop or conditionally are never movable, nor components (their DOM may not carry
|
|
727
|
+
// the attributes) and what lives in <head>, an <svg> or a <Trans>. The only content of an
|
|
728
|
+
// HTML element is not movable either: its parent is what gets moved (one handle per
|
|
729
|
+
// visual block, and no empty wrapper left behind).
|
|
730
|
+
const scopeStart = state.functionStack[state.functionStack.length - 1];
|
|
731
|
+
const parent = path[path.length - 1];
|
|
732
|
+
if (
|
|
733
|
+
tagName &&
|
|
734
|
+
/^[a-z]/.test(tagName) &&
|
|
735
|
+
!NON_MOVABLE_TAGS.has(tagName) &&
|
|
736
|
+
!isInsideNonMovableContainer(path) &&
|
|
737
|
+
!isOnlyContentOfHtmlElement(jsxNode, parent) &&
|
|
738
|
+
state.loopStack.length === 0 &&
|
|
739
|
+
scopeStart !== undefined &&
|
|
740
|
+
scopeStart >= 0 &&
|
|
741
|
+
(parent?.type === "JSXElement" || parent?.type === "JSXFragment") &&
|
|
742
|
+
hasRange(jsxNode)
|
|
743
|
+
) {
|
|
744
|
+
const id = generateId(state.filePath, jsxNode);
|
|
745
|
+
editableRegistry.set(id, {
|
|
746
|
+
file: state.filePath,
|
|
747
|
+
type: "element",
|
|
748
|
+
startOffset: jsxNode.start,
|
|
749
|
+
endOffset: jsxNode.end,
|
|
750
|
+
originalContent: state.code.slice(jsxNode.start, jsxNode.end),
|
|
751
|
+
context: {
|
|
752
|
+
parentTag:
|
|
753
|
+
parent.type === "JSXElement"
|
|
754
|
+
? (getJSXElementName(parent.openingElement) ?? "unknown")
|
|
755
|
+
: "fragment",
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
attributes.push(`data-upstart-move-id="${escapeProp(id)}"`);
|
|
759
|
+
attributes.push(`data-upstart-move-scope="${escapeProp(`${state.filePath}:${scopeStart}`)}"`);
|
|
760
|
+
}
|
|
761
|
+
|
|
718
762
|
// Inject attributes if any
|
|
719
763
|
if (insertPos !== -1 && attributes.length > 0) {
|
|
720
764
|
const attrString = " " + attributes.join(" ");
|
|
@@ -737,6 +781,53 @@ export function transformWithOxc(code: string, filePath: string) {
|
|
|
737
781
|
};
|
|
738
782
|
}
|
|
739
783
|
|
|
784
|
+
// Elements the editor never offers to move (document structure, invisible or meaningless alone).
|
|
785
|
+
const NON_MOVABLE_TAGS = new Set([
|
|
786
|
+
"html",
|
|
787
|
+
"head",
|
|
788
|
+
"body",
|
|
789
|
+
"meta",
|
|
790
|
+
"link",
|
|
791
|
+
"script",
|
|
792
|
+
"style",
|
|
793
|
+
"title",
|
|
794
|
+
"base",
|
|
795
|
+
"br",
|
|
796
|
+
"wbr",
|
|
797
|
+
"source",
|
|
798
|
+
"track",
|
|
799
|
+
"option",
|
|
800
|
+
"optgroup",
|
|
801
|
+
]);
|
|
802
|
+
|
|
803
|
+
// Ancestors whose children are not movable: <head> content, SVG internals, <Trans> slots.
|
|
804
|
+
const NON_MOVABLE_CONTAINERS = new Set(["head", "svg", "Trans"]);
|
|
805
|
+
|
|
806
|
+
function isInsideNonMovableContainer(path: Node[]): boolean {
|
|
807
|
+
return path.some((node) => {
|
|
808
|
+
if (node.type !== "JSXElement") return false;
|
|
809
|
+
const name = getJSXElementName((node as JSXElement).openingElement);
|
|
810
|
+
return name !== null && NON_MOVABLE_CONTAINERS.has(name);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* True when `node` is the only meaningful child of an HTML element: no sibling element,
|
|
816
|
+
* non-blank text or expression. Children of components are not concerned (a component is
|
|
817
|
+
* never movable itself, so its child stays the way to move that content).
|
|
818
|
+
*/
|
|
819
|
+
function isOnlyContentOfHtmlElement(node: JSXElement, parent: Node | undefined): boolean {
|
|
820
|
+
if (parent?.type !== "JSXElement") return false;
|
|
821
|
+
const parentName = getJSXElementName((parent as JSXElement).openingElement);
|
|
822
|
+
if (!parentName || !/^[a-z]/.test(parentName)) return false;
|
|
823
|
+
return (parent as JSXElement).children.every((child) => {
|
|
824
|
+
if (child === node) return true;
|
|
825
|
+
if (child.type === "JSXText") return (child as { value: string }).value.trim() === "";
|
|
826
|
+
if (child.type === "JSXExpressionContainer") return child.expression.type === "JSXEmptyExpression";
|
|
827
|
+
return false;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
740
831
|
// Helper: Get JSX element name
|
|
741
832
|
function getJSXElementName(opening: JSXOpeningElement): string | null {
|
|
742
833
|
if (opening.name.type === "JSXIdentifier") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
import { getLabels } from "./labels.js";
|
|
4
|
+
import { baseButton, createControlsLayer, cssAttr, setIcon } from "./controls-ui.js";
|
|
4
5
|
|
|
5
6
|
// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY
|
|
6
7
|
// (no server round-trip per action): "×" marks an item for removal, "+" inserts an
|
|
@@ -191,12 +192,7 @@ export function hideArrayControls(): void {
|
|
|
191
192
|
|
|
192
193
|
function ensureLayer(): HTMLDivElement {
|
|
193
194
|
if (layer) return layer;
|
|
194
|
-
layer =
|
|
195
|
-
layer.id = "upstart-array-controls";
|
|
196
|
-
layer.style.cssText =
|
|
197
|
-
"position: absolute; top: 0; left: 0; width: 0; height: 0; " +
|
|
198
|
-
"pointer-events: none; z-index: 2147483646;";
|
|
199
|
-
document.body.appendChild(layer);
|
|
195
|
+
layer = createControlsLayer("upstart-array-controls");
|
|
200
196
|
return layer;
|
|
201
197
|
}
|
|
202
198
|
|
|
@@ -237,10 +233,6 @@ function isReordered(arrayId: string): boolean {
|
|
|
237
233
|
return false;
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
function cssAttr(value: string): string {
|
|
241
|
-
return value.replace(/["\\]/g, "\\$&");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
236
|
function textOf(el: HTMLElement): string {
|
|
245
237
|
return (el.textContent ?? "").trim();
|
|
246
238
|
}
|
|
@@ -261,36 +253,6 @@ function applyDeletionStyle(item: HTMLElement, marked: boolean): void {
|
|
|
261
253
|
// Buttons
|
|
262
254
|
// ---------------------------------------------------------------------------
|
|
263
255
|
|
|
264
|
-
// Geometric SVG icons — centred by the flex container + viewBox, so they don't
|
|
265
|
-
// depend on the host page's font metrics (which left text glyphs mis-aligned).
|
|
266
|
-
const ICON_PATHS: Record<string, string> = {
|
|
267
|
-
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
268
|
-
cross: '<path d="M6 6 18 18M18 6 6 18"/>',
|
|
269
|
-
check: '<path d="M5 12.5 10 17.5 19 7"/>',
|
|
270
|
-
restore: '<path d="M3 4v5h5"/><path d="M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9"/>',
|
|
271
|
-
grip: '<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
function setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {
|
|
275
|
-
const dots = name === "grip";
|
|
276
|
-
btn.innerHTML =
|
|
277
|
-
`<svg width="${px}" height="${px}" viewBox="0 0 24 24" ` +
|
|
278
|
-
`fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" ` +
|
|
279
|
-
'stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" ' +
|
|
280
|
-
`style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {
|
|
284
|
-
const btn = document.createElement("button");
|
|
285
|
-
btn.type = "button";
|
|
286
|
-
btn.style.cssText =
|
|
287
|
-
`position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +
|
|
288
|
-
"padding: 0; display: flex; align-items: center; justify-content: center; " +
|
|
289
|
-
`background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +
|
|
290
|
-
"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);";
|
|
291
|
-
return btn;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
256
|
function makeItemDeleteButton(
|
|
295
257
|
arrayId: string,
|
|
296
258
|
index: number,
|
|
@@ -206,6 +206,15 @@ function handleSelectClick(event: MouseEvent, target: HTMLElement): void {
|
|
|
206
206
|
|
|
207
207
|
function handleClick(event: MouseEvent): void {
|
|
208
208
|
const mode = getCurrentMode();
|
|
209
|
+
if (mode === "move") {
|
|
210
|
+
// The page is inert while moving elements: following a link or submitting a form would
|
|
211
|
+
// reload the page and drop the staged moves. The move controls handle their own clicks.
|
|
212
|
+
const target = event.target as HTMLElement | null;
|
|
213
|
+
if (target?.closest("#upstart-move-controls, #upstart-move-bar")) return;
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
event.stopPropagation();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
209
218
|
if (mode !== "edit" && mode !== "select") {
|
|
210
219
|
return;
|
|
211
220
|
}
|
|
@@ -225,9 +234,14 @@ function handleClick(event: MouseEvent): void {
|
|
|
225
234
|
return;
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
// Editor-only chrome (the array ×/+/✓ controls
|
|
229
|
-
// items handle their own clicks and must never be treated
|
|
230
|
-
|
|
237
|
+
// Editor-only chrome (the array ×/+/✓ controls, the move handles and pending-moves bar)
|
|
238
|
+
// and locally-staged draft list items handle their own clicks and must never be treated
|
|
239
|
+
// as element selection.
|
|
240
|
+
if (
|
|
241
|
+
target.closest(
|
|
242
|
+
"#upstart-array-controls, #upstart-move-controls, #upstart-move-bar, [data-upstart-draft-item]",
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
231
245
|
return;
|
|
232
246
|
}
|
|
233
247
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Shared building blocks of the editor-only controls (array list buttons, move handles).
|
|
2
|
+
|
|
3
|
+
// Geometric SVG icons — centred by the flex container + viewBox, so they don't
|
|
4
|
+
// depend on the host page's font metrics (which left text glyphs mis-aligned).
|
|
5
|
+
const ICON_PATHS = {
|
|
6
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
7
|
+
cross: '<path d="M6 6 18 18M18 6 6 18"/>',
|
|
8
|
+
check: '<path d="M5 12.5 10 17.5 19 7"/>',
|
|
9
|
+
restore: '<path d="M3 4v5h5"/><path d="M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9"/>',
|
|
10
|
+
grip: '<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type IconName = keyof typeof ICON_PATHS;
|
|
14
|
+
|
|
15
|
+
export function setIcon(btn: HTMLElement, name: IconName, px: number): void {
|
|
16
|
+
const dots = name === "grip";
|
|
17
|
+
btn.innerHTML =
|
|
18
|
+
`<svg width="${px}" height="${px}" viewBox="0 0 24 24" ` +
|
|
19
|
+
`fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" ` +
|
|
20
|
+
'stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" ' +
|
|
21
|
+
`style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {
|
|
25
|
+
const btn = document.createElement("button");
|
|
26
|
+
btn.type = "button";
|
|
27
|
+
btn.style.cssText =
|
|
28
|
+
`position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +
|
|
29
|
+
"padding: 0; display: flex; align-items: center; justify-content: center; " +
|
|
30
|
+
`background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +
|
|
31
|
+
"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);";
|
|
32
|
+
return btn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Absolutely positioned, click-through layer appended to <body> that hosts editor controls. */
|
|
36
|
+
export function createControlsLayer(id: string): HTMLDivElement {
|
|
37
|
+
const layer = document.createElement("div");
|
|
38
|
+
layer.id = id;
|
|
39
|
+
layer.style.cssText =
|
|
40
|
+
"position: absolute; top: 0; left: 0; width: 0; height: 0; " +
|
|
41
|
+
"pointer-events: none; z-index: 2147483646;";
|
|
42
|
+
document.body.appendChild(layer);
|
|
43
|
+
return layer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function cssAttr(value: string): string {
|
|
47
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
48
|
+
}
|
|
@@ -4,6 +4,12 @@ import { initErrorHandler } from "./error-handler.js";
|
|
|
4
4
|
import { installDomGuard } from "./dom-guard.js";
|
|
5
5
|
import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./text-editor.js";
|
|
6
6
|
import { initArrayControls, refreshArrayControls, hideArrayControls } from "./array-controls.js";
|
|
7
|
+
import {
|
|
8
|
+
initMoveControls,
|
|
9
|
+
refreshMoveControls,
|
|
10
|
+
hideMoveControls,
|
|
11
|
+
onMovesApplyFailed,
|
|
12
|
+
} from "./move-controls.js";
|
|
7
13
|
import { initFormGuard } from "./form-guard.js";
|
|
8
14
|
import { sendToParent } from "./utils.js";
|
|
9
15
|
import { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from "./state.js";
|
|
@@ -22,13 +28,20 @@ export function setMode(mode: EditorMode): void {
|
|
|
22
28
|
console.log(`[Upstart Editor] Setting mode to: ${mode}`);
|
|
23
29
|
if (mode === "edit") {
|
|
24
30
|
disableSelectMode();
|
|
31
|
+
disableMoveMode();
|
|
25
32
|
enableEditMode();
|
|
26
33
|
} else if (mode === "select") {
|
|
27
34
|
disableEditMode();
|
|
35
|
+
disableMoveMode();
|
|
28
36
|
enableSelectMode();
|
|
37
|
+
} else if (mode === "move") {
|
|
38
|
+
disableEditMode();
|
|
39
|
+
disableSelectMode();
|
|
40
|
+
enableMoveMode();
|
|
29
41
|
} else {
|
|
30
42
|
disableEditMode();
|
|
31
43
|
disableSelectMode();
|
|
44
|
+
disableMoveMode();
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
|
|
@@ -156,6 +169,7 @@ export function initUpstartEditor(): void {
|
|
|
156
169
|
initClickHandler();
|
|
157
170
|
initHoverOverlay();
|
|
158
171
|
initArrayControls();
|
|
172
|
+
initMoveControls();
|
|
159
173
|
initFormGuard();
|
|
160
174
|
initErrorHandler();
|
|
161
175
|
initRouteReporter();
|
|
@@ -266,6 +280,8 @@ function handleParentMessage(event: MessageEvent): void {
|
|
|
266
280
|
sendToParent({ type: "selection-changed", selection: getSelection() });
|
|
267
281
|
} else if (message.type === "set-labels") {
|
|
268
282
|
setLabels(message.labels);
|
|
283
|
+
} else if (message.type === "moves-apply-failed") {
|
|
284
|
+
onMovesApplyFailed();
|
|
269
285
|
}
|
|
270
286
|
}
|
|
271
287
|
|
|
@@ -303,6 +319,21 @@ function enableSelectMode(): void {
|
|
|
303
319
|
document.documentElement.setAttribute("data-upstart-select-mode", "");
|
|
304
320
|
}
|
|
305
321
|
|
|
322
|
+
function enableMoveMode(): void {
|
|
323
|
+
console.log("[Upstart Editor] Move mode enabled");
|
|
324
|
+
document.documentElement.setAttribute("data-upstart-move-mode", "");
|
|
325
|
+
refreshMoveControls();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Staged moves are kept when leaving the mode (the moved elements stay where they were
|
|
330
|
+
* dropped); the pending-moves bar comes back with the mode.
|
|
331
|
+
*/
|
|
332
|
+
function disableMoveMode(): void {
|
|
333
|
+
document.documentElement.removeAttribute("data-upstart-move-mode");
|
|
334
|
+
hideMoveControls();
|
|
335
|
+
}
|
|
336
|
+
|
|
306
337
|
function disableSelectMode(): void {
|
|
307
338
|
document.documentElement.removeAttribute("data-upstart-select-mode");
|
|
308
339
|
hideOverlays();
|
|
@@ -11,6 +11,11 @@ export interface RuntimeLabels {
|
|
|
11
11
|
addItem: string;
|
|
12
12
|
applyChanges: string;
|
|
13
13
|
dragToReorder: string;
|
|
14
|
+
dragToMove: string;
|
|
15
|
+
pendingMoves: string;
|
|
16
|
+
undoMove: string;
|
|
17
|
+
discardMoves: string;
|
|
18
|
+
applyMoves: string;
|
|
14
19
|
clickToEdit: string;
|
|
15
20
|
startTyping: string;
|
|
16
21
|
recoveryTitle: string;
|
|
@@ -34,6 +39,11 @@ export const DEFAULT_LABELS: Readonly<RuntimeLabels> = Object.freeze({
|
|
|
34
39
|
addItem: "Add item",
|
|
35
40
|
applyChanges: "Apply changes",
|
|
36
41
|
dragToReorder: "Drag to reorder",
|
|
42
|
+
dragToMove: "Drag to move",
|
|
43
|
+
pendingMoves: "Moves not saved yet:",
|
|
44
|
+
undoMove: "Undo",
|
|
45
|
+
discardMoves: "Discard all",
|
|
46
|
+
applyMoves: "Apply",
|
|
37
47
|
clickToEdit: "Click to edit...",
|
|
38
48
|
startTyping: "Start typing...",
|
|
39
49
|
recoveryTitle: "The preview needs a refresh",
|