@aglyn/besigner 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +48 -0
- package/src/besigner.d.ts +28 -0
- package/src/besigner.js +59 -0
- package/src/besigner.js.map +1 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/api/app.api.d.ts +22 -0
- package/src/lib/api/app.api.js +59 -0
- package/src/lib/api/app.api.js.map +1 -0
- package/src/lib/api/besigner.api.d.ts +24 -0
- package/src/lib/api/besigner.api.js +39 -0
- package/src/lib/api/besigner.api.js.map +1 -0
- package/src/lib/clipboard-manager/clipboard-manager.d.ts +89 -0
- package/src/lib/clipboard-manager/clipboard-manager.js +241 -0
- package/src/lib/clipboard-manager/clipboard-manager.js.map +1 -0
- package/src/lib/clipboard-manager/clipboard-mirror.d.ts +52 -0
- package/src/lib/clipboard-manager/clipboard-mirror.js +75 -0
- package/src/lib/clipboard-manager/clipboard-mirror.js.map +1 -0
- package/src/lib/clipboard-manager/index.d.ts +20 -0
- package/src/lib/clipboard-manager/index.js +23 -0
- package/src/lib/clipboard-manager/index.js.map +1 -0
- package/src/lib/clipboard-manager/style-clipboard.d.ts +77 -0
- package/src/lib/clipboard-manager/style-clipboard.js +173 -0
- package/src/lib/clipboard-manager/style-clipboard.js.map +1 -0
- package/src/lib/constants/_internal.d.ts +20 -0
- package/src/lib/constants/_internal.js +19 -0
- package/src/lib/constants/_internal.js.map +1 -0
- package/src/lib/constants/besigner.d.ts +78 -0
- package/src/lib/constants/besigner.js +69 -0
- package/src/lib/constants/besigner.js.map +1 -0
- package/src/lib/controllers/besigner-app.controller.d.ts +33 -0
- package/src/lib/controllers/besigner-app.controller.js +57 -0
- package/src/lib/controllers/besigner-app.controller.js.map +1 -0
- package/src/lib/controllers/besigner-interface.controller.d.ts +44 -0
- package/src/lib/controllers/besigner-interface.controller.js +130 -0
- package/src/lib/controllers/besigner-interface.controller.js.map +1 -0
- package/src/lib/definitions/besigner-app.types.d.ts +41 -0
- package/src/lib/definitions/besigner-app.types.js +18 -0
- package/src/lib/definitions/besigner-app.types.js.map +1 -0
- package/src/lib/definitions/besigner-interface.types.d.ts +106 -0
- package/src/lib/definitions/besigner-interface.types.js +18 -0
- package/src/lib/definitions/besigner-interface.types.js.map +1 -0
- package/src/lib/definitions/emitter.types.d.ts +33 -0
- package/src/lib/definitions/emitter.types.js +18 -0
- package/src/lib/definitions/emitter.types.js.map +1 -0
- package/src/lib/device-preview-width.d.ts +39 -0
- package/src/lib/device-preview-width.js +57 -0
- package/src/lib/device-preview-width.js.map +1 -0
- package/src/lib/dnd-manager/confirm-valid-lineal-relationship.d.ts +54 -0
- package/src/lib/dnd-manager/confirm-valid-lineal-relationship.js +109 -0
- package/src/lib/dnd-manager/confirm-valid-lineal-relationship.js.map +1 -0
- package/src/lib/dnd-manager/dnd-manager.d.ts +75 -0
- package/src/lib/dnd-manager/dnd-manager.js +337 -0
- package/src/lib/dnd-manager/dnd-manager.js.map +1 -0
- package/src/lib/dnd-manager/droppable-region-utils.d.ts +72 -0
- package/src/lib/dnd-manager/droppable-region-utils.js +115 -0
- package/src/lib/dnd-manager/droppable-region-utils.js.map +1 -0
- package/src/lib/dnd-manager/index.d.ts +20 -0
- package/src/lib/dnd-manager/index.js +21 -0
- package/src/lib/dnd-manager/index.js.map +1 -0
- package/src/lib/focus-manager/focus-manager.d.ts +104 -0
- package/src/lib/focus-manager/focus-manager.js +353 -0
- package/src/lib/focus-manager/focus-manager.js.map +1 -0
- package/src/lib/focus-manager/index.d.ts +18 -0
- package/src/lib/focus-manager/index.js +19 -0
- package/src/lib/focus-manager/index.js.map +1 -0
- package/src/lib/is-root-element-id.d.ts +19 -0
- package/src/lib/is-root-element-id.js +22 -0
- package/src/lib/is-root-element-id.js.map +1 -0
- package/src/lib/managers.d.ts +25 -0
- package/src/lib/managers.js +26 -0
- package/src/lib/managers.js.map +1 -0
- package/src/lib/node-move/index.d.ts +17 -0
- package/src/lib/node-move/index.js +18 -0
- package/src/lib/node-move/index.js.map +1 -0
- package/src/lib/node-move/node-move.d.ts +80 -0
- package/src/lib/node-move/node-move.js +134 -0
- package/src/lib/node-move/node-move.js.map +1 -0
- package/src/lib/node-refs/index.d.ts +1 -0
- package/src/lib/node-refs/index.js +3 -0
- package/src/lib/node-refs/index.js.map +1 -0
- package/src/lib/node-refs/node-refs.d.ts +24 -0
- package/src/lib/node-refs/node-refs.js +30 -0
- package/src/lib/node-refs/node-refs.js.map +1 -0
- package/src/lib/pick-manager/index.d.ts +18 -0
- package/src/lib/pick-manager/index.js +20 -0
- package/src/lib/pick-manager/index.js.map +1 -0
- package/src/lib/pick-manager/pick-manager.d.ts +64 -0
- package/src/lib/pick-manager/pick-manager.js +116 -0
- package/src/lib/pick-manager/pick-manager.js.map +1 -0
- package/src/lib/text-entry/index.d.ts +17 -0
- package/src/lib/text-entry/index.js +18 -0
- package/src/lib/text-entry/index.js.map +1 -0
- package/src/lib/text-entry/text-entry.d.ts +71 -0
- package/src/lib/text-entry/text-entry.js +103 -0
- package/src/lib/text-entry/text-entry.js.map +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type * as Aglyn from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* Besigner element clipboard (AGL-1202): copy an element — or a whole
|
|
20
|
+
* multi-selection — and paste it somewhere else, including into a
|
|
21
|
+
* *different document*.
|
|
22
|
+
*
|
|
23
|
+
* Cross-document paste is the point. Duplicate already covers "another one
|
|
24
|
+
* of these, right here"; what it cannot do is carry a built column out of
|
|
25
|
+
* the nav component and into the footer component, or lift a finished
|
|
26
|
+
* section off one screen onto another. So the clipboard holds detached
|
|
27
|
+
* nested JSON (never live nodes) and mirrors itself into `localStorage`,
|
|
28
|
+
* which is what lets it survive the full page navigation between two
|
|
29
|
+
* besigner documents.
|
|
30
|
+
*
|
|
31
|
+
* A MobX store, mirroring the focus-manager seam, so menus observing
|
|
32
|
+
* {@link hasContent} re-render the moment something is copied.
|
|
33
|
+
*/
|
|
34
|
+
/** `localStorage` key holding the mirrored clipboard entry. */
|
|
35
|
+
export declare const CLIPBOARD_STORAGE_KEY = "aglyn:besigner:clipboard";
|
|
36
|
+
/**
|
|
37
|
+
* Bumped whenever the persisted shape changes. A mirrored entry written by
|
|
38
|
+
* an older build is dropped rather than pasted as something unexpected.
|
|
39
|
+
*/
|
|
40
|
+
export declare const CLIPBOARD_FORMAT_VERSION = 1;
|
|
41
|
+
export interface ClipboardEntry {
|
|
42
|
+
version: number;
|
|
43
|
+
/** Element labels in copy order, for the menu ("Paste Stack"). */
|
|
44
|
+
labels: string[];
|
|
45
|
+
/** Detached nested subtrees, in document order. */
|
|
46
|
+
nodes: Aglyn.NodeSchemaNested<any>[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The current entry, hydrating from the `localStorage` mirror the first
|
|
50
|
+
* time it is asked for in this document.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getEntry(): ClipboardEntry | null;
|
|
53
|
+
/** Whether there is anything to paste. */
|
|
54
|
+
export declare function hasContent(): boolean;
|
|
55
|
+
/** Labels of the copied elements, in copy order. */
|
|
56
|
+
export declare function getLabels(): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Copy one or more live nodes. The document root is skipped — pasting a
|
|
59
|
+
* whole document into itself is never what the gesture meant.
|
|
60
|
+
*
|
|
61
|
+
* Returns the number of elements actually copied.
|
|
62
|
+
*/
|
|
63
|
+
export declare function copyNodes(nodes: Aglyn.NodeSchema<any>[]): number;
|
|
64
|
+
export declare function clear(): void;
|
|
65
|
+
/**
|
|
66
|
+
* A plain result object rather than a discriminated union: an `ok: true |
|
|
67
|
+
* false` union stops narrowing once it crosses a library boundary (the
|
|
68
|
+
* emitted declaration widens the tag to `boolean`), so consumers in the
|
|
69
|
+
* designer would have to cast. Callers test `error`.
|
|
70
|
+
*/
|
|
71
|
+
export interface PasteResult {
|
|
72
|
+
/** What landed on the canvas — empty when `error` is set. */
|
|
73
|
+
nodes: Aglyn.NodeSchema<any>[];
|
|
74
|
+
/** Set when nothing was pasted, explaining why. */
|
|
75
|
+
error?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Paste the clipboard next to / inside `target`, resolved exactly the way
|
|
79
|
+
* the Add-element flow resolves an insertion point: a container receives the
|
|
80
|
+
* elements as children, a leaf gets them as its next siblings.
|
|
81
|
+
*
|
|
82
|
+
* Rejects — rather than half-pasting — when the clipboard came from a
|
|
83
|
+
* document whose element catalog this one does not share (an email besigner
|
|
84
|
+
* has no Screen Link), or when the lineal rules forbid the arrangement.
|
|
85
|
+
* An `error` result means nothing was written to the canvas.
|
|
86
|
+
*/
|
|
87
|
+
export declare function pasteInto(target?: Aglyn.NodeSchema<any>): PasteResult;
|
|
88
|
+
/** Every `componentId` in a nested subtree, root first. */
|
|
89
|
+
export declare function collectComponentIds(node: Aglyn.NodeSchemaNested<any>, out?: string[]): string[];
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
18
|
+
import { canvas, components } from "@aglyn/aglyn";
|
|
19
|
+
import { observable, runInAction } from "mobx";
|
|
20
|
+
import { confirmValidLinealRelationship, describeInvalidLinealRelationship } from "../dnd-manager/index.js";
|
|
21
|
+
import { localStore, parseMirrored, watchMirror } from "./clipboard-mirror.js";
|
|
22
|
+
/**
|
|
23
|
+
* Besigner element clipboard (AGL-1202): copy an element — or a whole
|
|
24
|
+
* multi-selection — and paste it somewhere else, including into a
|
|
25
|
+
* *different document*.
|
|
26
|
+
*
|
|
27
|
+
* Cross-document paste is the point. Duplicate already covers "another one
|
|
28
|
+
* of these, right here"; what it cannot do is carry a built column out of
|
|
29
|
+
* the nav component and into the footer component, or lift a finished
|
|
30
|
+
* section off one screen onto another. So the clipboard holds detached
|
|
31
|
+
* nested JSON (never live nodes) and mirrors itself into `localStorage`,
|
|
32
|
+
* which is what lets it survive the full page navigation between two
|
|
33
|
+
* besigner documents.
|
|
34
|
+
*
|
|
35
|
+
* A MobX store, mirroring the focus-manager seam, so menus observing
|
|
36
|
+
* {@link hasContent} re-render the moment something is copied.
|
|
37
|
+
*/ /** `localStorage` key holding the mirrored clipboard entry. */ export const CLIPBOARD_STORAGE_KEY = 'aglyn:besigner:clipboard';
|
|
38
|
+
/**
|
|
39
|
+
* Bumped whenever the persisted shape changes. A mirrored entry written by
|
|
40
|
+
* an older build is dropped rather than pasted as something unexpected.
|
|
41
|
+
*/ export const CLIPBOARD_FORMAT_VERSION = 1;
|
|
42
|
+
const state = observable({
|
|
43
|
+
entry: null,
|
|
44
|
+
hydrated: false
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* A copied subtree keeps no tie to the canvas it came from: `$id` and
|
|
48
|
+
* `parentId` are re-minted on paste, and carrying stale ones through
|
|
49
|
+
* `localStorage` invites a paste that collides with a live node.
|
|
50
|
+
*/ const detach = (node)=>{
|
|
51
|
+
const { $id, parentId } = node, rest = _object_without_properties_loose(node, [
|
|
52
|
+
"$id",
|
|
53
|
+
"parentId"
|
|
54
|
+
]);
|
|
55
|
+
return _extends({}, rest, {
|
|
56
|
+
nodes: (Array.isArray(rest.nodes) ? rest.nodes : []).map(detach)
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Drop any node that already travels inside another node in the same
|
|
61
|
+
* selection. Selecting a Stack *and* one of its children and hitting copy
|
|
62
|
+
* should yield one column, not a column plus a second loose copy of a row
|
|
63
|
+
* that is already inside it.
|
|
64
|
+
*/ const withoutNestedDuplicates = (nodes)=>{
|
|
65
|
+
const ids = new Set(nodes.map((node)=>node == null ? void 0 : node.$id).filter(Boolean));
|
|
66
|
+
return nodes.filter((node)=>{
|
|
67
|
+
var _node_breadcrumbPath;
|
|
68
|
+
if (!(node == null ? void 0 : node.$id)) return false;
|
|
69
|
+
// breadcrumbPath is [root, ...ancestors, self] — an ancestor also in the
|
|
70
|
+
// selection means this node is already covered by that ancestor's copy.
|
|
71
|
+
const ancestors = ((_node_breadcrumbPath = node.breadcrumbPath) != null ? _node_breadcrumbPath : []).filter((id)=>id !== node.$id);
|
|
72
|
+
return !ancestors.some((id)=>ids.has(id));
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const persist = (entry)=>{
|
|
76
|
+
const store = localStore();
|
|
77
|
+
if (!store) return;
|
|
78
|
+
try {
|
|
79
|
+
if (entry) store.setItem(CLIPBOARD_STORAGE_KEY, JSON.stringify(entry));
|
|
80
|
+
else store.removeItem(CLIPBOARD_STORAGE_KEY);
|
|
81
|
+
} catch (unused) {
|
|
82
|
+
// A full or unavailable quota only costs cross-document paste; the
|
|
83
|
+
// in-memory entry still works for this document.
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const parseEntry = (raw)=>{
|
|
87
|
+
const parsed = parseMirrored(raw, CLIPBOARD_FORMAT_VERSION);
|
|
88
|
+
if (!parsed) return null;
|
|
89
|
+
if (!Array.isArray(parsed.nodes) || !parsed.nodes.length) return null;
|
|
90
|
+
return parsed;
|
|
91
|
+
};
|
|
92
|
+
const readMirror = ()=>{
|
|
93
|
+
try {
|
|
94
|
+
var _ref;
|
|
95
|
+
var _localStore;
|
|
96
|
+
return parseEntry((_ref = (_localStore = localStore()) == null ? void 0 : _localStore.getItem(CLIPBOARD_STORAGE_KEY)) != null ? _ref : null);
|
|
97
|
+
} catch (unused) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* A copy made in another besigner tab replaces this document's entry the
|
|
103
|
+
* moment it happens, exactly as a copy made here would. Hydrating once on
|
|
104
|
+
* first read is what a navigation needs; a second tab needs this, or the
|
|
105
|
+
* document goes on pasting the clipping it read first (AGL-2507).
|
|
106
|
+
*/ watchMirror(CLIPBOARD_STORAGE_KEY, (raw)=>{
|
|
107
|
+
const entry = parseEntry(raw);
|
|
108
|
+
runInAction(()=>{
|
|
109
|
+
state.entry = entry;
|
|
110
|
+
// The mirror has now been consulted, whatever it held.
|
|
111
|
+
state.hydrated = true;
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* The current entry, hydrating from the `localStorage` mirror the first
|
|
116
|
+
* time it is asked for in this document.
|
|
117
|
+
*/ export function getEntry() {
|
|
118
|
+
if (!state.hydrated) {
|
|
119
|
+
const mirrored = readMirror();
|
|
120
|
+
runInAction(()=>{
|
|
121
|
+
state.hydrated = true;
|
|
122
|
+
if (mirrored && !state.entry) state.entry = mirrored;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return state.entry;
|
|
126
|
+
}
|
|
127
|
+
/** Whether there is anything to paste. */ export function hasContent() {
|
|
128
|
+
var _getEntry_nodes, _getEntry;
|
|
129
|
+
return !!((_getEntry = getEntry()) == null ? void 0 : (_getEntry_nodes = _getEntry.nodes) == null ? void 0 : _getEntry_nodes.length);
|
|
130
|
+
}
|
|
131
|
+
/** Labels of the copied elements, in copy order. */ export function getLabels() {
|
|
132
|
+
var _ref;
|
|
133
|
+
var _getEntry;
|
|
134
|
+
return (_ref = (_getEntry = getEntry()) == null ? void 0 : _getEntry.labels) != null ? _ref : [];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Copy one or more live nodes. The document root is skipped — pasting a
|
|
138
|
+
* whole document into itself is never what the gesture meant.
|
|
139
|
+
*
|
|
140
|
+
* Returns the number of elements actually copied.
|
|
141
|
+
*/ export function copyNodes(nodes) {
|
|
142
|
+
const copyable = withoutNestedDuplicates((nodes != null ? nodes : []).filter((node)=>node && !canvas.isRootNode(node)));
|
|
143
|
+
if (!copyable.length) return 0;
|
|
144
|
+
const entry = {
|
|
145
|
+
version: CLIPBOARD_FORMAT_VERSION,
|
|
146
|
+
labels: copyable.map((node)=>{
|
|
147
|
+
var _ref, _node_labelShort;
|
|
148
|
+
return (_ref = (_node_labelShort = node.labelShort) != null ? _node_labelShort : node.componentId) != null ? _ref : 'element';
|
|
149
|
+
}),
|
|
150
|
+
nodes: copyable.map((node)=>detach(canvas.makeNested(node)))
|
|
151
|
+
};
|
|
152
|
+
runInAction(()=>{
|
|
153
|
+
state.entry = entry;
|
|
154
|
+
state.hydrated = true;
|
|
155
|
+
});
|
|
156
|
+
persist(entry);
|
|
157
|
+
return copyable.length;
|
|
158
|
+
}
|
|
159
|
+
export function clear() {
|
|
160
|
+
runInAction(()=>{
|
|
161
|
+
state.entry = null;
|
|
162
|
+
state.hydrated = true;
|
|
163
|
+
});
|
|
164
|
+
persist(null);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Paste the clipboard next to / inside `target`, resolved exactly the way
|
|
168
|
+
* the Add-element flow resolves an insertion point: a container receives the
|
|
169
|
+
* elements as children, a leaf gets them as its next siblings.
|
|
170
|
+
*
|
|
171
|
+
* Rejects — rather than half-pasting — when the clipboard came from a
|
|
172
|
+
* document whose element catalog this one does not share (an email besigner
|
|
173
|
+
* has no Screen Link), or when the lineal rules forbid the arrangement.
|
|
174
|
+
* An `error` result means nothing was written to the canvas.
|
|
175
|
+
*/ export function pasteInto(target) {
|
|
176
|
+
var _entry_nodes, _parent_componentSchema, _parent_componentSchema1;
|
|
177
|
+
const entry = getEntry();
|
|
178
|
+
if (!(entry == null ? void 0 : (_entry_nodes = entry.nodes) == null ? void 0 : _entry_nodes.length)) return {
|
|
179
|
+
nodes: [],
|
|
180
|
+
error: 'Nothing to paste'
|
|
181
|
+
};
|
|
182
|
+
const { parent, index } = canvas.resolveInsertTarget(target);
|
|
183
|
+
if (!parent) return {
|
|
184
|
+
nodes: [],
|
|
185
|
+
error: 'Nowhere to paste this'
|
|
186
|
+
};
|
|
187
|
+
// Every component in every subtree has to exist in THIS app before
|
|
188
|
+
// anything lands, so a rejected paste leaves the canvas untouched.
|
|
189
|
+
const unknown = entry.nodes.flatMap((node)=>collectComponentIds(node)).find((id)=>!components.getFactory(id));
|
|
190
|
+
if (unknown) {
|
|
191
|
+
return {
|
|
192
|
+
nodes: [],
|
|
193
|
+
error: `This copy uses "${unknown}", which isn't available here`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Pasting follows the same lineal placement rules as inserting and dnd —
|
|
197
|
+
// without this, a paste can create an arrangement that drag-and-drop then
|
|
198
|
+
// (correctly) refuses to move.
|
|
199
|
+
const parentActor = {
|
|
200
|
+
componentId: parent.componentId,
|
|
201
|
+
pluginId: parent.pluginId,
|
|
202
|
+
restrictChildren: (_parent_componentSchema = parent.componentSchema) == null ? void 0 : _parent_componentSchema.restrictChildren,
|
|
203
|
+
restrictParent: (_parent_componentSchema1 = parent.componentSchema) == null ? void 0 : _parent_componentSchema1.restrictParent
|
|
204
|
+
};
|
|
205
|
+
for (const nested of entry.nodes){
|
|
206
|
+
const schema = components.getSchema(nested.componentId);
|
|
207
|
+
const item = {
|
|
208
|
+
componentId: nested.componentId,
|
|
209
|
+
pluginId: nested.pluginId,
|
|
210
|
+
restrictChildren: schema == null ? void 0 : schema.restrictChildren,
|
|
211
|
+
restrictParent: schema == null ? void 0 : schema.restrictParent
|
|
212
|
+
};
|
|
213
|
+
const [valid, reason] = confirmValidLinealRelationship(item, parentActor);
|
|
214
|
+
if (!valid) {
|
|
215
|
+
return {
|
|
216
|
+
nodes: [],
|
|
217
|
+
error: describeInvalidLinealRelationship(item, parentActor, reason)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const pasted = [];
|
|
222
|
+
entry.nodes.forEach((nested, i)=>{
|
|
223
|
+
// Sequential indices keep multi-element pastes in copy order; NaN keeps
|
|
224
|
+
// the append-to-end behaviour of a container target.
|
|
225
|
+
const at = isNaN(index) ? NaN : index + i;
|
|
226
|
+
pasted.push(canvas.addNodeFromNested(nested, parent, at));
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
nodes: pasted
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Every `componentId` in a nested subtree, root first. */ export function collectComponentIds(node, out = []) {
|
|
233
|
+
if (!node) return out;
|
|
234
|
+
if (node.componentId) out.push(node.componentId);
|
|
235
|
+
for (const child of Array.isArray(node.nodes) ? node.nodes : []){
|
|
236
|
+
collectComponentIds(child, out);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=clipboard-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/besigner/core/src/lib/clipboard-manager/clipboard-manager.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type * as Aglyn from '@aglyn/aglyn'\nimport { canvas, components } from '@aglyn/aglyn'\nimport { observable, runInAction } from 'mobx'\nimport {\n confirmValidLinealRelationship,\n describeInvalidLinealRelationship,\n type LinealItem,\n} from '../dnd-manager'\nimport { localStore, parseMirrored, watchMirror } from './clipboard-mirror'\n\n/**\n * Besigner element clipboard (AGL-1202): copy an element — or a whole\n * multi-selection — and paste it somewhere else, including into a\n * *different document*.\n *\n * Cross-document paste is the point. Duplicate already covers \"another one\n * of these, right here\"; what it cannot do is carry a built column out of\n * the nav component and into the footer component, or lift a finished\n * section off one screen onto another. So the clipboard holds detached\n * nested JSON (never live nodes) and mirrors itself into `localStorage`,\n * which is what lets it survive the full page navigation between two\n * besigner documents.\n *\n * A MobX store, mirroring the focus-manager seam, so menus observing\n * {@link hasContent} re-render the moment something is copied.\n */\n\n/** `localStorage` key holding the mirrored clipboard entry. */\nexport const CLIPBOARD_STORAGE_KEY = 'aglyn:besigner:clipboard'\n\n/**\n * Bumped whenever the persisted shape changes. A mirrored entry written by\n * an older build is dropped rather than pasted as something unexpected.\n */\nexport const CLIPBOARD_FORMAT_VERSION = 1\n\nexport interface ClipboardEntry {\n version: number\n /** Element labels in copy order, for the menu (\"Paste Stack\"). */\n labels: string[]\n /** Detached nested subtrees, in document order. */\n nodes: Aglyn.NodeSchemaNested<any>[]\n}\n\ninterface ClipboardState {\n entry: ClipboardEntry | null\n /** Set once the mirrored entry has been read back after a navigation. */\n hydrated: boolean\n}\n\nconst state = observable<ClipboardState>({ entry: null, hydrated: false })\n\n/**\n * A copied subtree keeps no tie to the canvas it came from: `$id` and\n * `parentId` are re-minted on paste, and carrying stale ones through\n * `localStorage` invites a paste that collides with a live node.\n */\nconst detach = (\n node: Aglyn.NodeSchemaNested<any>,\n): Aglyn.NodeSchemaNested<any> => {\n const { $id, parentId, ...rest } = node as Record<string, unknown> & {\n $id?: string\n parentId?: string\n nodes?: Aglyn.NodeSchemaNested<any>[]\n }\n return {\n ...rest,\n nodes: (Array.isArray(rest.nodes) ? rest.nodes : []).map(detach),\n } as Aglyn.NodeSchemaNested<any>\n}\n\n/**\n * Drop any node that already travels inside another node in the same\n * selection. Selecting a Stack *and* one of its children and hitting copy\n * should yield one column, not a column plus a second loose copy of a row\n * that is already inside it.\n */\nconst withoutNestedDuplicates = (\n nodes: Aglyn.NodeSchema<any>[],\n): Aglyn.NodeSchema<any>[] => {\n const ids = new Set(nodes.map((node) => node?.$id).filter(Boolean))\n return nodes.filter((node) => {\n if (!node?.$id) return false\n // breadcrumbPath is [root, ...ancestors, self] — an ancestor also in the\n // selection means this node is already covered by that ancestor's copy.\n const ancestors = (node.breadcrumbPath ?? []).filter(\n (id: Aglyn.NodeId) => id !== node.$id,\n )\n return !ancestors.some((id: Aglyn.NodeId) => ids.has(id))\n })\n}\n\nconst persist = (entry: ClipboardEntry | null): void => {\n const store = localStore()\n if (!store) return\n try {\n if (entry) store.setItem(CLIPBOARD_STORAGE_KEY, JSON.stringify(entry))\n else store.removeItem(CLIPBOARD_STORAGE_KEY)\n } catch {\n // A full or unavailable quota only costs cross-document paste; the\n // in-memory entry still works for this document.\n }\n}\n\nconst parseEntry = (raw: string | null): ClipboardEntry | null => {\n const parsed = parseMirrored<ClipboardEntry>(raw, CLIPBOARD_FORMAT_VERSION)\n if (!parsed) return null\n if (!Array.isArray(parsed.nodes) || !parsed.nodes.length) return null\n return parsed\n}\n\nconst readMirror = (): ClipboardEntry | null => {\n try {\n return parseEntry(localStore()?.getItem(CLIPBOARD_STORAGE_KEY) ?? null)\n } catch {\n return null\n }\n}\n\n/**\n * A copy made in another besigner tab replaces this document's entry the\n * moment it happens, exactly as a copy made here would. Hydrating once on\n * first read is what a navigation needs; a second tab needs this, or the\n * document goes on pasting the clipping it read first (AGL-2507).\n */\nwatchMirror(CLIPBOARD_STORAGE_KEY, (raw) => {\n const entry = parseEntry(raw)\n runInAction(() => {\n state.entry = entry\n // The mirror has now been consulted, whatever it held.\n state.hydrated = true\n })\n})\n\n/**\n * The current entry, hydrating from the `localStorage` mirror the first\n * time it is asked for in this document.\n */\nexport function getEntry(): ClipboardEntry | null {\n if (!state.hydrated) {\n const mirrored = readMirror()\n runInAction(() => {\n state.hydrated = true\n if (mirrored && !state.entry) state.entry = mirrored\n })\n }\n return state.entry\n}\n\n/** Whether there is anything to paste. */\nexport function hasContent(): boolean {\n return !!getEntry()?.nodes?.length\n}\n\n/** Labels of the copied elements, in copy order. */\nexport function getLabels(): string[] {\n return getEntry()?.labels ?? []\n}\n\n/**\n * Copy one or more live nodes. The document root is skipped — pasting a\n * whole document into itself is never what the gesture meant.\n *\n * Returns the number of elements actually copied.\n */\nexport function copyNodes(nodes: Aglyn.NodeSchema<any>[]): number {\n const copyable = withoutNestedDuplicates(\n (nodes ?? []).filter((node) => node && !canvas.isRootNode(node)),\n )\n if (!copyable.length) return 0\n\n const entry: ClipboardEntry = {\n version: CLIPBOARD_FORMAT_VERSION,\n labels: copyable.map(\n (node) => node.labelShort ?? node.componentId ?? 'element',\n ),\n nodes: copyable.map((node) => detach(canvas.makeNested(node))),\n }\n runInAction(() => {\n state.entry = entry\n state.hydrated = true\n })\n persist(entry)\n return copyable.length\n}\n\nexport function clear(): void {\n runInAction(() => {\n state.entry = null\n state.hydrated = true\n })\n persist(null)\n}\n\n/**\n * A plain result object rather than a discriminated union: an `ok: true |\n * false` union stops narrowing once it crosses a library boundary (the\n * emitted declaration widens the tag to `boolean`), so consumers in the\n * designer would have to cast. Callers test `error`.\n */\nexport interface PasteResult {\n /** What landed on the canvas — empty when `error` is set. */\n nodes: Aglyn.NodeSchema<any>[]\n /** Set when nothing was pasted, explaining why. */\n error?: string\n}\n\n/**\n * Paste the clipboard next to / inside `target`, resolved exactly the way\n * the Add-element flow resolves an insertion point: a container receives the\n * elements as children, a leaf gets them as its next siblings.\n *\n * Rejects — rather than half-pasting — when the clipboard came from a\n * document whose element catalog this one does not share (an email besigner\n * has no Screen Link), or when the lineal rules forbid the arrangement.\n * An `error` result means nothing was written to the canvas.\n */\nexport function pasteInto(target?: Aglyn.NodeSchema<any>): PasteResult {\n const entry = getEntry()\n if (!entry?.nodes?.length) return { nodes: [], error: 'Nothing to paste' }\n\n const { parent, index } = canvas.resolveInsertTarget(target)\n if (!parent) return { nodes: [], error: 'Nowhere to paste this' }\n\n // Every component in every subtree has to exist in THIS app before\n // anything lands, so a rejected paste leaves the canvas untouched.\n const unknown = entry.nodes\n .flatMap((node) => collectComponentIds(node))\n .find((id) => !components.getFactory(id as any))\n if (unknown) {\n return {\n nodes: [],\n error: `This copy uses \"${unknown}\", which isn't available here`,\n }\n }\n\n // Pasting follows the same lineal placement rules as inserting and dnd —\n // without this, a paste can create an arrangement that drag-and-drop then\n // (correctly) refuses to move.\n const parentActor: LinealItem = {\n componentId: parent.componentId,\n pluginId: parent.pluginId,\n restrictChildren: parent.componentSchema?.restrictChildren,\n restrictParent: parent.componentSchema?.restrictParent,\n }\n for (const nested of entry.nodes) {\n const schema = components.getSchema(nested.componentId as any)\n const item: LinealItem = {\n componentId: nested.componentId as any,\n pluginId: nested.pluginId as any,\n restrictChildren: schema?.restrictChildren,\n restrictParent: schema?.restrictParent,\n }\n const [valid, reason] = confirmValidLinealRelationship(item, parentActor)\n if (!valid) {\n return {\n nodes: [],\n error: describeInvalidLinealRelationship(item, parentActor, reason),\n }\n }\n }\n\n const pasted: Aglyn.NodeSchema<any>[] = []\n entry.nodes.forEach((nested, i) => {\n // Sequential indices keep multi-element pastes in copy order; NaN keeps\n // the append-to-end behaviour of a container target.\n const at = isNaN(index) ? NaN : index + i\n pasted.push(canvas.addNodeFromNested(nested, parent, at))\n })\n return { nodes: pasted }\n}\n\n/** Every `componentId` in a nested subtree, root first. */\nexport function collectComponentIds(\n node: Aglyn.NodeSchemaNested<any>,\n out: string[] = [],\n): string[] {\n if (!node) return out\n if (node.componentId) out.push(node.componentId as string)\n for (const child of Array.isArray(node.nodes) ? node.nodes : []) {\n collectComponentIds(child as Aglyn.NodeSchemaNested<any>, out)\n }\n return out\n}\n"],"names":["canvas","components","observable","runInAction","confirmValidLinealRelationship","describeInvalidLinealRelationship","localStore","parseMirrored","watchMirror","CLIPBOARD_STORAGE_KEY","CLIPBOARD_FORMAT_VERSION","state","entry","hydrated","detach","node","$id","parentId","rest","nodes","Array","isArray","map","withoutNestedDuplicates","ids","Set","filter","Boolean","ancestors","breadcrumbPath","id","some","has","persist","store","setItem","JSON","stringify","removeItem","parseEntry","raw","parsed","length","readMirror","getItem","getEntry","mirrored","hasContent","getLabels","labels","copyNodes","copyable","isRootNode","version","labelShort","componentId","makeNested","clear","pasteInto","target","parent","error","index","resolveInsertTarget","unknown","flatMap","collectComponentIds","find","getFactory","parentActor","pluginId","restrictChildren","componentSchema","restrictParent","nested","schema","getSchema","item","valid","reason","pasted","forEach","i","at","isNaN","NaN","push","addNodeFromNested","out","child"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The `localStorage` seam shared by the two besigner clipboards (elements
|
|
19
|
+
* and styles).
|
|
20
|
+
*
|
|
21
|
+
* The mirror IS the clipboard, not a fallback for it. Hydrating from it once
|
|
22
|
+
* per document is enough to survive a navigation, but not to survive a second
|
|
23
|
+
* tab: two besigner documents open side by side never reload, so a document
|
|
24
|
+
* that only reads the mirror on its first paste keeps serving that first
|
|
25
|
+
* clipping no matter how many times the tab next door copies something new
|
|
26
|
+
* (AGL-2507). Every document therefore follows the mirror for as long as it
|
|
27
|
+
* is open, which is what {@link watchMirror} is for.
|
|
28
|
+
*/
|
|
29
|
+
/** `window.localStorage`, or null when it is unavailable. */
|
|
30
|
+
export declare const localStore: () => Storage | null;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a mirrored entry, dropping anything a different build wrote. A
|
|
33
|
+
* corrupt or foreign entry is no entry — this never throws.
|
|
34
|
+
*
|
|
35
|
+
* @param raw - The stored JSON, or null when the key is absent.
|
|
36
|
+
* @param version - The format version this build understands.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseMirrored<T extends {
|
|
39
|
+
version: number;
|
|
40
|
+
}>(raw: string | null, version: number): T | null;
|
|
41
|
+
/**
|
|
42
|
+
* Call `onValue` whenever another document on this origin writes `key`.
|
|
43
|
+
*
|
|
44
|
+
* `storage` fires only in the OTHER documents, never in the one that wrote,
|
|
45
|
+
* so a copy made here is never round-tripped back through the parser.
|
|
46
|
+
*
|
|
47
|
+
* @returns An unsubscribe function. Nothing calls it today — the clipboards
|
|
48
|
+
* are module singletons that live as long as the document — but a listener
|
|
49
|
+
* with no way off the window is a leak waiting for the first consumer that
|
|
50
|
+
* does need one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function watchMirror(key: string, onValue: (raw: string | null) => void): () => void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The `localStorage` seam shared by the two besigner clipboards (elements
|
|
18
|
+
* and styles).
|
|
19
|
+
*
|
|
20
|
+
* The mirror IS the clipboard, not a fallback for it. Hydrating from it once
|
|
21
|
+
* per document is enough to survive a navigation, but not to survive a second
|
|
22
|
+
* tab: two besigner documents open side by side never reload, so a document
|
|
23
|
+
* that only reads the mirror on its first paste keeps serving that first
|
|
24
|
+
* clipping no matter how many times the tab next door copies something new
|
|
25
|
+
* (AGL-2507). Every document therefore follows the mirror for as long as it
|
|
26
|
+
* is open, which is what {@link watchMirror} is for.
|
|
27
|
+
*/ /** `window.localStorage`, or null when it is unavailable. */ export const localStore = ()=>{
|
|
28
|
+
try {
|
|
29
|
+
// SSR and privacy-mode both make this throw rather than return null.
|
|
30
|
+
return typeof window === 'undefined' ? null : window.localStorage;
|
|
31
|
+
} catch (unused) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Parse a mirrored entry, dropping anything a different build wrote. A
|
|
37
|
+
* corrupt or foreign entry is no entry — this never throws.
|
|
38
|
+
*
|
|
39
|
+
* @param raw - The stored JSON, or null when the key is absent.
|
|
40
|
+
* @param version - The format version this build understands.
|
|
41
|
+
*/ export function parseMirrored(raw, version) {
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return (parsed == null ? void 0 : parsed.version) === version ? parsed : null;
|
|
46
|
+
} catch (unused) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Call `onValue` whenever another document on this origin writes `key`.
|
|
52
|
+
*
|
|
53
|
+
* `storage` fires only in the OTHER documents, never in the one that wrote,
|
|
54
|
+
* so a copy made here is never round-tripped back through the parser.
|
|
55
|
+
*
|
|
56
|
+
* @returns An unsubscribe function. Nothing calls it today — the clipboards
|
|
57
|
+
* are module singletons that live as long as the document — but a listener
|
|
58
|
+
* with no way off the window is a leak waiting for the first consumer that
|
|
59
|
+
* does need one.
|
|
60
|
+
*/ export function watchMirror(key, onValue) {
|
|
61
|
+
if (typeof window === 'undefined') return ()=>undefined;
|
|
62
|
+
const listener = (event)=>{
|
|
63
|
+
// A null key is another tab calling `localStorage.clear()`, which clears
|
|
64
|
+
// this key along with everything else.
|
|
65
|
+
if (event.key !== null && event.key !== key) return;
|
|
66
|
+
// `sessionStorage` raises this event too, and it is a different clipboard.
|
|
67
|
+
const store = localStore();
|
|
68
|
+
if (store && event.storageArea && event.storageArea !== store) return;
|
|
69
|
+
onValue(event.key === null ? null : event.newValue);
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener('storage', listener);
|
|
72
|
+
return ()=>window.removeEventListener('storage', listener);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=clipboard-mirror.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/besigner/core/src/lib/clipboard-manager/clipboard-mirror.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The `localStorage` seam shared by the two besigner clipboards (elements\n * and styles).\n *\n * The mirror IS the clipboard, not a fallback for it. Hydrating from it once\n * per document is enough to survive a navigation, but not to survive a second\n * tab: two besigner documents open side by side never reload, so a document\n * that only reads the mirror on its first paste keeps serving that first\n * clipping no matter how many times the tab next door copies something new\n * (AGL-2507). Every document therefore follows the mirror for as long as it\n * is open, which is what {@link watchMirror} is for.\n */\n\n/** `window.localStorage`, or null when it is unavailable. */\nexport const localStore = (): Storage | null => {\n try {\n // SSR and privacy-mode both make this throw rather than return null.\n return typeof window === 'undefined' ? null : window.localStorage\n } catch {\n return null\n }\n}\n\n/**\n * Parse a mirrored entry, dropping anything a different build wrote. A\n * corrupt or foreign entry is no entry — this never throws.\n *\n * @param raw - The stored JSON, or null when the key is absent.\n * @param version - The format version this build understands.\n */\nexport function parseMirrored<T extends { version: number }>(\n raw: string | null,\n version: number,\n): T | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as T\n return parsed?.version === version ? parsed : null\n } catch {\n return null\n }\n}\n\n/**\n * Call `onValue` whenever another document on this origin writes `key`.\n *\n * `storage` fires only in the OTHER documents, never in the one that wrote,\n * so a copy made here is never round-tripped back through the parser.\n *\n * @returns An unsubscribe function. Nothing calls it today — the clipboards\n * are module singletons that live as long as the document — but a listener\n * with no way off the window is a leak waiting for the first consumer that\n * does need one.\n */\nexport function watchMirror(\n key: string,\n onValue: (raw: string | null) => void,\n): () => void {\n if (typeof window === 'undefined') return () => undefined\n\n const listener = (event: StorageEvent): void => {\n // A null key is another tab calling `localStorage.clear()`, which clears\n // this key along with everything else.\n if (event.key !== null && event.key !== key) return\n // `sessionStorage` raises this event too, and it is a different clipboard.\n const store = localStore()\n if (store && event.storageArea && event.storageArea !== store) return\n onValue(event.key === null ? null : event.newValue)\n }\n\n window.addEventListener('storage', listener)\n return () => window.removeEventListener('storage', listener)\n}\n"],"names":["localStore","window","localStorage","parseMirrored","raw","version","parsed","JSON","parse","watchMirror","key","onValue","undefined","listener","event","store","storageArea","newValue","addEventListener","removeEventListener"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,2DAA2D,GAC3D,OAAO,MAAMA,aAAa;IACxB,IAAI;QACF,qEAAqE;QACrE,OAAO,OAAOC,WAAW,cAAc,OAAOA,OAAOC,YAAY;IACnE,EAAE,eAAM;QACN,OAAO;IACT;AACF,EAAC;AAED;;;;;;CAMC,GACD,OAAO,SAASC,cACdC,GAAkB,EAClBC,OAAe;IAEf,IAAI,CAACD,KAAK,OAAO;IACjB,IAAI;QACF,MAAME,SAASC,KAAKC,KAAK,CAACJ;QAC1B,OAAOE,CAAAA,0BAAAA,OAAQD,OAAO,MAAKA,UAAUC,SAAS;IAChD,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,YACdC,GAAW,EACXC,OAAqC;IAErC,IAAI,OAAOV,WAAW,aAAa,OAAO,IAAMW;IAEhD,MAAMC,WAAW,CAACC;QAChB,yEAAyE;QACzE,uCAAuC;QACvC,IAAIA,MAAMJ,GAAG,KAAK,QAAQI,MAAMJ,GAAG,KAAKA,KAAK;QAC7C,2EAA2E;QAC3E,MAAMK,QAAQf;QACd,IAAIe,SAASD,MAAME,WAAW,IAAIF,MAAME,WAAW,KAAKD,OAAO;QAC/DJ,QAAQG,MAAMJ,GAAG,KAAK,OAAO,OAAOI,MAAMG,QAAQ;IACpD;IAEAhB,OAAOiB,gBAAgB,CAAC,WAAWL;IACnC,OAAO,IAAMZ,OAAOkB,mBAAmB,CAAC,WAAWN;AACrD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export * as clipboard from './clipboard-manager';
|
|
18
|
+
export { CLIPBOARD_FORMAT_VERSION, CLIPBOARD_STORAGE_KEY, type ClipboardEntry, type PasteResult, } from './clipboard-manager';
|
|
19
|
+
export * as styleClipboard from './style-clipboard';
|
|
20
|
+
export { STYLE_CLIPBOARD_FORMAT_VERSION, STYLE_CLIPBOARD_STORAGE_KEY, type StyleClipboardEntry, } from './style-clipboard';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import * as _clipboard from "./clipboard-manager.js";
|
|
17
|
+
export { _clipboard as clipboard };
|
|
18
|
+
export { CLIPBOARD_FORMAT_VERSION, CLIPBOARD_STORAGE_KEY } from "./clipboard-manager.js";
|
|
19
|
+
import * as _styleClipboard from "./style-clipboard.js";
|
|
20
|
+
export { _styleClipboard as styleClipboard };
|
|
21
|
+
export { STYLE_CLIPBOARD_FORMAT_VERSION, STYLE_CLIPBOARD_STORAGE_KEY } from "./style-clipboard.js";
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/besigner/core/src/lib/clipboard-manager/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * as clipboard from './clipboard-manager'\nexport {\n CLIPBOARD_FORMAT_VERSION,\n CLIPBOARD_STORAGE_KEY,\n type ClipboardEntry,\n type PasteResult,\n} from './clipboard-manager'\n\nexport * as styleClipboard from './style-clipboard'\nexport {\n STYLE_CLIPBOARD_FORMAT_VERSION,\n STYLE_CLIPBOARD_STORAGE_KEY,\n type StyleClipboardEntry,\n} from './style-clipboard'\n"],"names":["clipboard","CLIPBOARD_FORMAT_VERSION","CLIPBOARD_STORAGE_KEY","styleClipboard","STYLE_CLIPBOARD_FORMAT_VERSION","STYLE_CLIPBOARD_STORAGE_KEY"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4BAA2B,yBAAqB;AAAhD,SAAO,cAAKA,SAAS,GAA2B;AAChD,SACEC,wBAAwB,EACxBC,qBAAqB,QAGhB,yBAAqB;AAE5B,iCAAgC,uBAAmB;AAAnD,SAAO,mBAAKC,cAAc,GAAyB;AACnD,SACEC,8BAA8B,EAC9BC,2BAA2B,QAEtB,uBAAmB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type * as Aglyn from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* Besigner STYLE clipboard.
|
|
20
|
+
*
|
|
21
|
+
* The element clipboard beside it carries whole subtrees. This carries a
|
|
22
|
+
* look: everything the Styles panel writes to one element, so a heading
|
|
23
|
+
* styled once can be applied to five more without rebuilding it field by
|
|
24
|
+
* field or duplicating an element and re-authoring its content.
|
|
25
|
+
*
|
|
26
|
+
* ## What a "style" is here
|
|
27
|
+
*
|
|
28
|
+
* `node.sx` and only `node.sx` — the Styles panel's entire output, including
|
|
29
|
+
* its responsive, `@scheme` and interaction-state slices, which are keys
|
|
30
|
+
* inside that object rather than separate storage.
|
|
31
|
+
*
|
|
32
|
+
* NOT classes. A class is a name pointing at rules the element does not own,
|
|
33
|
+
* and two of them carry behaviour rather than appearance: `aglyn-hidden`
|
|
34
|
+
* enrols an element in interaction choreography, and a theme utility class
|
|
35
|
+
* may be targeted by an interaction's class steps. Carrying those across on
|
|
36
|
+
* a "paste styles" would move behaviour under a label that promises looks.
|
|
37
|
+
*
|
|
38
|
+
* ## Paste REPLACES
|
|
39
|
+
*
|
|
40
|
+
* Merging would make the result depend on what the target already had, so
|
|
41
|
+
* pasting the same look onto two elements could produce two different
|
|
42
|
+
* elements — and there would be no way to paste "no styles". Undo is the way
|
|
43
|
+
* back, as it is for every other edit.
|
|
44
|
+
*
|
|
45
|
+
* Mirrored into `localStorage` on the same terms as the element clipboard, so
|
|
46
|
+
* a look copied in one document can be pasted in another after the full page
|
|
47
|
+
* navigation between two besigner routes.
|
|
48
|
+
*/
|
|
49
|
+
/** `localStorage` key holding the mirrored style entry. */
|
|
50
|
+
export declare const STYLE_CLIPBOARD_STORAGE_KEY = "aglyn:besigner:style-clipboard";
|
|
51
|
+
/** Bumped when the persisted shape changes; an older entry is dropped. */
|
|
52
|
+
export declare const STYLE_CLIPBOARD_FORMAT_VERSION = 1;
|
|
53
|
+
export interface StyleClipboardEntry {
|
|
54
|
+
version: number;
|
|
55
|
+
/** The element the look was taken from, for the menu ("from Heading"). */
|
|
56
|
+
label: string;
|
|
57
|
+
/** The Styles panel's whole output. `null` is a real value: no styles. */
|
|
58
|
+
sx: Record<string, unknown> | null;
|
|
59
|
+
}
|
|
60
|
+
/** The copied look, or null. */
|
|
61
|
+
export declare function getStyleEntry(): StyleClipboardEntry | null;
|
|
62
|
+
/** Whether anything has been copied. Observed, so menus enable on copy. */
|
|
63
|
+
export declare function hasStyles(): boolean;
|
|
64
|
+
/** The label of the element the look came from, for the menu. */
|
|
65
|
+
export declare function getStyleLabel(): string;
|
|
66
|
+
/** Takes the look off one node. Returns false when there is no node. */
|
|
67
|
+
export declare function copyStyles(node: Aglyn.NodeSchema<any> | undefined): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Applies the copied look to a node, replacing whatever it had.
|
|
70
|
+
*
|
|
71
|
+
* Returns false when there is nothing to paste. A copied entry of `null`
|
|
72
|
+
* styles IS something to paste — it clears the target — which is why the
|
|
73
|
+
* check is on the entry rather than on its `sx`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function pasteStyles(node: Aglyn.NodeSchema<any> | undefined): boolean;
|
|
76
|
+
/** Forgets the copied look. Tests, and a future "clear" affordance. */
|
|
77
|
+
export declare function clearStyles(): void;
|