@formicoidea/labre-core 0.38.2 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_pkgs/affine-block-database/properties/rich-text/cell-renderer.js +14 -9
- package/dist/_pkgs/affine-block-frame/frame-manager.js +10 -9
- package/dist/_pkgs/affine-gfx-connector/connector-manager.d.ts +13 -1
- package/dist/_pkgs/affine-gfx-connector/connector-manager.js +9 -5
- package/dist/_pkgs/affine-gfx-connector/connector-watcher.js +48 -17
- package/dist/_pkgs/affine-gfx-connector/view/view.js +4 -0
- package/dist/_pkgs/affine-gfx-mindmap/view/view.js +9 -1
- package/dist/_pkgs/affine-gfx-mindmap/view.js +7 -2
- package/dist/_pkgs/affine-gfx-template/contribute.d.ts +32 -7
- package/dist/_pkgs/affine-gfx-template/contribute.js +66 -22
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.d.ts +7 -0
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +19 -4
- package/dist/_pkgs/affine-model/elements/mindmap/mindmap.js +10 -1
- package/dist/_pkgs/affine-widget-keyboard-toolbar/widget.js +18 -3
- package/dist/_pkgs/affine-widget-linked-doc/mobile-linked-doc-menu.d.ts +7 -1
- package/dist/_pkgs/affine-widget-linked-doc/mobile-linked-doc-menu.js +8 -2
- package/dist/_pkgs/std/gfx/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -167,14 +167,17 @@ export class RichTextCell extends BaseCellRenderer {
|
|
|
167
167
|
e.stopPropagation();
|
|
168
168
|
};
|
|
169
169
|
this._onPaste = (e) => {
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
e.stopPropagation();
|
|
172
170
|
const inlineEditor = this.inlineEditor$.value;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
const inlineRange = inlineEditor?.getInlineRange();
|
|
172
|
+
// Bail out before cancelling the event: with no resolvable inline range
|
|
173
|
+
// there is nothing to insert into, and swallowing the paste here would
|
|
174
|
+
// drop the clipboard silently instead of letting the default path act.
|
|
175
|
+
if (!inlineEditor || !inlineRange)
|
|
177
176
|
return;
|
|
177
|
+
// The cell owns the paste: stop it before the document clipboard sees it,
|
|
178
|
+
// whichever branch below ends up handling the payload.
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
e.stopPropagation();
|
|
178
181
|
if (e.clipboardData) {
|
|
179
182
|
try {
|
|
180
183
|
const getDeltas = (snapshot) => {
|
|
@@ -290,9 +293,11 @@ export class RichTextCell extends BaseCellRenderer {
|
|
|
290
293
|
richText.addEventListener('cut', this._onCut, true);
|
|
291
294
|
richText.addEventListener('paste', this._onPaste, true);
|
|
292
295
|
return () => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
richText.removeEventListener('
|
|
296
|
+
// the capture flag is part of the listener identity: without it
|
|
297
|
+
// these three would never be removed.
|
|
298
|
+
richText.removeEventListener('copy', this._onCopy, true);
|
|
299
|
+
richText.removeEventListener('cut', this._onCut, true);
|
|
300
|
+
richText.removeEventListener('paste', this._onPaste, true);
|
|
296
301
|
};
|
|
297
302
|
}
|
|
298
303
|
return;
|
|
@@ -259,6 +259,15 @@ export class EdgelessFrameManager extends GfxExtension {
|
|
|
259
259
|
}
|
|
260
260
|
}));
|
|
261
261
|
this._disposable.add(doc.slots.blockUpdated.subscribe(payload => {
|
|
262
|
+
// Local adds only, exactly like the canvas-element half above. A block
|
|
263
|
+
// added by a remote peer was already adopted THERE: the frame's
|
|
264
|
+
// `childElementIds` arrives with the same sync. Adopting it again here
|
|
265
|
+
// is a duplicate write on every receiving peer — and a readonly viewer
|
|
266
|
+
// cannot write at all, which is how this surfaced (#251). See #242 for
|
|
267
|
+
// the rule: a cascade reacts to local transactions, the receiving peer
|
|
268
|
+
// trusts the sync.
|
|
269
|
+
if (!payload.isLocal)
|
|
270
|
+
return;
|
|
262
271
|
if (payload.type === 'add' &&
|
|
263
272
|
payload.model instanceof GfxBlockElementModel &&
|
|
264
273
|
renderableInEdgeless(doc, surfaceModel, payload.model)) {
|
|
@@ -275,15 +284,7 @@ export class EdgelessFrameManager extends GfxExtension {
|
|
|
275
284
|
if (this._enclosesFrame(payload.model, frame)) {
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
278
|
-
|
|
279
|
-
this._adoptNewlyCreatedElement(frame, payload.model);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
// Remote adds: adopt only. The originating client reassigns the
|
|
283
|
-
// index and syncs it — reindexing here too would produce
|
|
284
|
-
// competing concurrent writes for the same element.
|
|
285
|
-
this.addElementsToFrame(frame, [payload.model]);
|
|
286
|
-
}
|
|
287
|
+
this._adoptNewlyCreatedElement(frame, payload.model);
|
|
287
288
|
}
|
|
288
289
|
}));
|
|
289
290
|
}
|
|
@@ -84,7 +84,19 @@ export declare class ConnectorPathGenerator extends PathGenerator {
|
|
|
84
84
|
constructor(options: {
|
|
85
85
|
getElementById: (id: string) => GfxModel | null;
|
|
86
86
|
});
|
|
87
|
-
static updatePath(connector: ConnectorElementModel | LocalConnectorElementModel, path: PointLocation[] | null, elementGetter?: (id: string) => GfxModel | null
|
|
87
|
+
static updatePath(connector: ConnectorElementModel | LocalConnectorElementModel, path: PointLocation[] | null, elementGetter?: (id: string) => GfxModel | null, options?: {
|
|
88
|
+
/**
|
|
89
|
+
* Whether this peer may persist the derived label box. `labelXYWH` is a
|
|
90
|
+
* `@field()`: unlike `path` / `xywh`, which are `@local()` and must be
|
|
91
|
+
* recomputed everywhere to repaint, writing it makes the CRDT grow on
|
|
92
|
+
* every peer that merely WATCHES a connector move — and throws on a
|
|
93
|
+
* readonly viewer. The authoring peer's value arrives with the sync, so
|
|
94
|
+
* a peer reacting to a remote change passes `false`. Defaults to `true`
|
|
95
|
+
* for the direct callers (paste, mindmap layout, shape resize), which
|
|
96
|
+
* are local gestures by construction.
|
|
97
|
+
*/
|
|
98
|
+
persistLabelXYWH?: boolean;
|
|
99
|
+
}): void;
|
|
88
100
|
private _computeStartEndPoint;
|
|
89
101
|
private _generateConnectorPath;
|
|
90
102
|
private _generateCurveConnectorPath;
|
|
@@ -978,7 +978,7 @@ export class ConnectorPathGenerator extends PathGenerator {
|
|
|
978
978
|
super();
|
|
979
979
|
this.options = options;
|
|
980
980
|
}
|
|
981
|
-
static updatePath(connector, path, elementGetter) {
|
|
981
|
+
static updatePath(connector, path, elementGetter, options) {
|
|
982
982
|
const instance = new ConnectorPathGenerator({
|
|
983
983
|
getElementById: elementGetter ?? (() => null),
|
|
984
984
|
});
|
|
@@ -1004,12 +1004,16 @@ export class ConnectorPathGenerator extends PathGenerator {
|
|
|
1004
1004
|
// the property assignment order matters
|
|
1005
1005
|
connector.xywh = bound.serialize();
|
|
1006
1006
|
connector.path = relativePoints;
|
|
1007
|
-
// Updates Connector's Label position
|
|
1007
|
+
// Updates Connector's Label position — a PERSISTED write, unlike
|
|
1008
|
+
// everything above it.
|
|
1008
1009
|
if (isConnectorWithLabel(connector)) {
|
|
1009
1010
|
const model = connector;
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1011
|
+
const mayPersist = (options?.persistLabelXYWH ?? true) && !model.surface?.store?.readonly;
|
|
1012
|
+
if (mayPersist) {
|
|
1013
|
+
const [cx, cy] = model.getPointByOffsetDistance(model.labelOffset.distance);
|
|
1014
|
+
const [, , w, h] = model.labelXYWH;
|
|
1015
|
+
model.labelXYWH = [cx - w / 2, cy - h / 2, w, h];
|
|
1016
|
+
}
|
|
1013
1017
|
}
|
|
1014
1018
|
connector.updatingPath = false;
|
|
1015
1019
|
}
|
|
@@ -62,49 +62,74 @@ function reanchorConnectorsForPolygon(surface, polygonId, elementGetter) {
|
|
|
62
62
|
export const connectorWatcher = (surface) => {
|
|
63
63
|
const hasElementById = (id) => surface.hasElementById(id) || surface.store.hasBlock(id);
|
|
64
64
|
const elementGetter = (id) => surface.getElementById(id) ?? surface.store.getModelById(id);
|
|
65
|
-
const updateConnectorPath = (connector) => {
|
|
65
|
+
const updateConnectorPath = (connector, local) => {
|
|
66
66
|
if (((connector.source?.id && hasElementById(connector.source.id)) ||
|
|
67
67
|
(!connector.source?.id && connector.source?.position)) &&
|
|
68
68
|
((connector.target?.id && hasElementById(connector.target.id)) ||
|
|
69
69
|
(!connector.target?.id && connector.target?.position))) {
|
|
70
|
-
|
|
70
|
+
// The path itself is `@local()` — every peer recomputes it to repaint.
|
|
71
|
+
// The label box is not: only the peer that authored the change persists
|
|
72
|
+
// it, the others read the value that arrives with the sync.
|
|
73
|
+
ConnectorPathGenerator.updatePath(connector, null, elementGetter, {
|
|
74
|
+
persistLabelXYWH: local,
|
|
75
|
+
});
|
|
71
76
|
}
|
|
72
77
|
};
|
|
73
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Connectors whose path must be recomputed in the next microtask, mapped to
|
|
80
|
+
* whether ANY of the changes that queued them was local — a single local
|
|
81
|
+
* gesture is enough to let the peer persist the derived label box.
|
|
82
|
+
*/
|
|
83
|
+
const pendingList = new Map();
|
|
74
84
|
let pendingFlag = false;
|
|
75
|
-
const addToUpdateList = (connector) => {
|
|
76
|
-
pendingList.
|
|
85
|
+
const addToUpdateList = (connector, local) => {
|
|
86
|
+
pendingList.set(connector, (pendingList.get(connector) ?? false) || local);
|
|
77
87
|
if (!pendingFlag) {
|
|
78
88
|
pendingFlag = true;
|
|
79
89
|
queueMicrotask(() => {
|
|
80
|
-
pendingList.forEach(updateConnectorPath);
|
|
90
|
+
pendingList.forEach((isLocal, connector) => updateConnectorPath(connector, isLocal));
|
|
81
91
|
pendingList.clear();
|
|
82
92
|
pendingFlag = false;
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
};
|
|
86
96
|
const disposables = [
|
|
87
|
-
surface.elementAdded.subscribe(({ id }) => {
|
|
97
|
+
surface.elementAdded.subscribe(({ id, local }) => {
|
|
88
98
|
const element = elementGetter(id);
|
|
89
99
|
if (!element)
|
|
90
100
|
return;
|
|
91
101
|
if ('type' in element && element.type === 'connector') {
|
|
92
|
-
addToUpdateList(element);
|
|
102
|
+
addToUpdateList(element, local);
|
|
93
103
|
}
|
|
94
104
|
else {
|
|
95
|
-
surface
|
|
105
|
+
surface
|
|
106
|
+
.getConnectors(id)
|
|
107
|
+
.forEach(connector => addToUpdateList(connector, local));
|
|
96
108
|
}
|
|
97
109
|
}),
|
|
98
|
-
surface.elementUpdated.subscribe(({ id, props }) => {
|
|
110
|
+
surface.elementUpdated.subscribe(({ id, props, local }) => {
|
|
99
111
|
const element = elementGetter(id);
|
|
100
|
-
|
|
112
|
+
// The element may already be gone by the time the update is delivered
|
|
113
|
+
// (a remote peer that moved and then deleted it, an undo…).
|
|
114
|
+
if (!element)
|
|
115
|
+
return;
|
|
116
|
+
if (local && props['vertices']) {
|
|
101
117
|
// When polygon vertices change, re-anchor connected connectors to the
|
|
102
118
|
// nearest valid boundary point BEFORE scheduling the path update, so
|
|
103
119
|
// the path generator uses the corrected anchor positions.
|
|
120
|
+
//
|
|
121
|
+
// LOCAL ONLY: re-anchoring writes `source`/`target`, both persisted
|
|
122
|
+
// `@field()`s. The peer that edited the vertices re-anchors and syncs
|
|
123
|
+
// the result, so doing it again on a receiving peer is a duplicate
|
|
124
|
+
// write — and an illegal one on a readonly viewer (see #242).
|
|
104
125
|
reanchorConnectorsForPolygon(surface, id, elementGetter);
|
|
105
126
|
}
|
|
106
127
|
if (props['xywh'] || props['rotate'] || props['vertices']) {
|
|
107
|
-
|
|
128
|
+
// Not gated on `local`: `path`, `absolutePath` and the connector's own
|
|
129
|
+
// `xywh` are `@local()`, so every peer must recompute them to repaint.
|
|
130
|
+
surface
|
|
131
|
+
.getConnectors(id)
|
|
132
|
+
.forEach(connector => addToUpdateList(connector, local));
|
|
108
133
|
}
|
|
109
134
|
if ('type' in element &&
|
|
110
135
|
element.type === 'connector' &&
|
|
@@ -113,25 +138,31 @@ export const connectorWatcher = (surface) => {
|
|
|
113
138
|
props['source'] ||
|
|
114
139
|
props['curveControlPoint'] !== undefined)) {
|
|
115
140
|
const connector = element;
|
|
116
|
-
// Clear custom handle data when connector mode changes
|
|
117
|
-
|
|
141
|
+
// Clear custom handle data when connector mode changes. `local` only:
|
|
142
|
+
// `curveControlPoint` is persisted, and the peer that changed the mode
|
|
143
|
+
// cleared it in the same gesture.
|
|
144
|
+
if (local && props['mode'] !== undefined) {
|
|
118
145
|
if (connector.curveControlPoint !== null) {
|
|
119
146
|
connector.curveControlPoint = null;
|
|
120
147
|
}
|
|
121
148
|
}
|
|
122
|
-
addToUpdateList(connector);
|
|
149
|
+
addToUpdateList(connector, local);
|
|
123
150
|
}
|
|
124
151
|
}),
|
|
125
152
|
surface.store.slots.blockUpdated.subscribe(payload => {
|
|
126
153
|
if (payload.type === 'add' ||
|
|
127
154
|
(payload.type === 'update' && payload.props.key === 'xywh')) {
|
|
128
|
-
surface
|
|
155
|
+
surface
|
|
156
|
+
.getConnectors(payload.id)
|
|
157
|
+
.forEach(connector => addToUpdateList(connector, payload.isLocal));
|
|
129
158
|
}
|
|
130
159
|
}),
|
|
131
160
|
];
|
|
161
|
+
// Mount-time recompute of the `@local()` geometry. Not a local gesture:
|
|
162
|
+
// merely OPENING a document must not rewrite every connector's label box.
|
|
132
163
|
surface
|
|
133
164
|
.getElementsByType('connector')
|
|
134
|
-
.forEach(connector => updateConnectorPath(connector));
|
|
165
|
+
.forEach(connector => updateConnectorPath(connector, false));
|
|
135
166
|
return () => {
|
|
136
167
|
disposables.forEach(d => d.unsubscribe());
|
|
137
168
|
};
|
|
@@ -44,7 +44,11 @@ export class ConnectorElementView extends GfxElementModelView {
|
|
|
44
44
|
};
|
|
45
45
|
};
|
|
46
46
|
const watchEvent = (labelModel) => {
|
|
47
|
+
// `ViewManager.get` returns null for an element that has no view yet —
|
|
48
|
+
// the local label element is registered a beat before its view exists.
|
|
47
49
|
const view = this.gfx.view.get(labelModel);
|
|
50
|
+
if (!view)
|
|
51
|
+
return;
|
|
48
52
|
const connectorModel = this.model;
|
|
49
53
|
let labelBound = null;
|
|
50
54
|
let startPoint = {
|
|
@@ -173,12 +173,18 @@ export class MindMapView extends GfxElementModelView {
|
|
|
173
173
|
button: false,
|
|
174
174
|
node: false,
|
|
175
175
|
};
|
|
176
|
+
this._hoveredState = this._hoveredState.set(node.id, hoveredState);
|
|
177
|
+
// `ViewManager.get` returns null for an element whose view is not
|
|
178
|
+
// mounted yet — the button model exists a beat before its view does.
|
|
179
|
+
// The button is created and tracked either way; only the hover wiring
|
|
180
|
+
// is skipped.
|
|
176
181
|
const buttonView = this.gfx.view.get(id);
|
|
182
|
+
if (!buttonView)
|
|
183
|
+
return collapseButton;
|
|
177
184
|
const isOnElementBound = (evt) => {
|
|
178
185
|
const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
|
|
179
186
|
return buttonView.model.includesPoint(x, y, { useElementBound: true }, this.gfx.std.host);
|
|
180
187
|
};
|
|
181
|
-
this._hoveredState = this._hoveredState.set(node.id, hoveredState);
|
|
182
188
|
buttonView.on('pointerenter', () => {
|
|
183
189
|
hoveredState.button = true;
|
|
184
190
|
this._updateButtonVisibility(node.id);
|
|
@@ -216,6 +222,8 @@ export class MindMapView extends GfxElementModelView {
|
|
|
216
222
|
}
|
|
217
223
|
});
|
|
218
224
|
const nodeView = this.gfx.view.get(node.id);
|
|
225
|
+
if (!nodeView)
|
|
226
|
+
return collapseButton;
|
|
219
227
|
nodeView.on('pointerenter', () => {
|
|
220
228
|
hoveredState.node = true;
|
|
221
229
|
this._updateButtonVisibility(node.id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { TemplateCategoryExtension } from '../affine-gfx-template/index.js';
|
|
3
3
|
import { effects } from './effects.js';
|
|
4
4
|
import { MindmapElementRendererExtension } from './element-renderer.js';
|
|
5
5
|
import { MindMapIndicatorOverlay } from './indicator-overlay/index.js';
|
|
@@ -22,7 +22,6 @@ export class MindmapRenderViewExtension extends ViewExtensionProvider {
|
|
|
22
22
|
effect() {
|
|
23
23
|
super.effect();
|
|
24
24
|
effects();
|
|
25
|
-
extendTemplateCategory(mindmapTemplateCategory);
|
|
26
25
|
}
|
|
27
26
|
setup(context) {
|
|
28
27
|
super.setup(context);
|
|
@@ -33,6 +32,12 @@ export class MindmapRenderViewExtension extends ViewExtensionProvider {
|
|
|
33
32
|
context.register(MindMapDragExtension);
|
|
34
33
|
context.register(MindMapIndicatorOverlay);
|
|
35
34
|
context.register(MindMapInteraction);
|
|
35
|
+
if (this.isEdgeless(context.scope)) {
|
|
36
|
+
// The Mind Map Templates-panel category stays on the ALWAYS-ON half, on
|
|
37
|
+
// purpose: it must survive the `mindmap` flag, which only hides the
|
|
38
|
+
// senior button ({@link MindmapToolViewExtension}).
|
|
39
|
+
context.register(TemplateCategoryExtension(mindmapTemplateCategory));
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
/** The dedicated "Mind Map" senior button — flag-gated (`mindmap`). */
|
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BlockStdScope } from '../std/index.js';
|
|
2
|
+
import type { ExtensionType } from '../store/index.js';
|
|
3
|
+
import type { TemplateCategory, TemplateManager } from './toolbar/template-type.js';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A framework registers its Templates-panel categories here; the panel reads
|
|
6
|
+
* them back from the std of the edgeless it opens on. One identifier per
|
|
7
|
+
* category NAME, so two extensions claiming the same category collide at
|
|
8
|
+
* setup (`DuplicateServiceDefinitionError`, caught by `di-mount.unit.spec.ts`)
|
|
9
|
+
* rather than listing it twice.
|
|
8
10
|
*/
|
|
9
|
-
export declare
|
|
11
|
+
export declare const TemplateCategoryIdentifier: import("../global/di/types.js").ServiceIdentifier<TemplateCategory> & (<U extends TemplateCategory = TemplateCategory>(variant: import("../global/di/types.js").ServiceVariant) => import("../global/di/types.js").ServiceIdentifier<U>);
|
|
12
|
+
/**
|
|
13
|
+
* Register a framework's template categories. Call it from `setup()` of the
|
|
14
|
+
* FLAG-GATED view extension, beside the senior tool — a category is creation
|
|
15
|
+
* tooling, so a disabled framework must contribute none (`docs/adr/0009`):
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* context.register(TemplateCategoryExtension(wardleyTemplateCategory));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Replaces the module-level registry of 0.38.0–0.38.2 (`extendTemplateCategory`)
|
|
22
|
+
* that appended from `effect()` and never removed: a category outlived the
|
|
23
|
+
* editor whose flags had admitted it, so a framework switched off in the host
|
|
24
|
+
* kept its category until a full reload (#244). The DI container is per editor;
|
|
25
|
+
* nothing has to be un-registered.
|
|
26
|
+
*/
|
|
27
|
+
export declare function TemplateCategoryExtension(...categories: TemplateCategory[]): ExtensionType;
|
|
28
|
+
/**
|
|
29
|
+
* The catalogue ONE editor sees: the built-in categories (`Other`, plus
|
|
30
|
+
* whatever a host appended through `EdgelessTemplatePanel.templates.extend`)
|
|
31
|
+
* followed by the categories the view extensions mounted on this `std`
|
|
32
|
+
* registered — i.e. exactly the frameworks whose tooling is on.
|
|
33
|
+
*/
|
|
34
|
+
export declare function templateManagerFor(std: BlockStdScope): TemplateManager;
|
|
@@ -1,32 +1,76 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createIdentifier } from '../global/di/index.js';
|
|
2
|
+
import { builtInTemplates } from './toolbar/builtin-templates.js';
|
|
3
|
+
/**
|
|
4
|
+
* A framework registers its Templates-panel categories here; the panel reads
|
|
5
|
+
* them back from the std of the edgeless it opens on. One identifier per
|
|
6
|
+
* category NAME, so two extensions claiming the same category collide at
|
|
7
|
+
* setup (`DuplicateServiceDefinitionError`, caught by `di-mount.unit.spec.ts`)
|
|
8
|
+
* rather than listing it twice.
|
|
9
|
+
*/
|
|
10
|
+
export const TemplateCategoryIdentifier = createIdentifier('TemplateCategory');
|
|
11
|
+
/**
|
|
12
|
+
* Register a framework's template categories. Call it from `setup()` of the
|
|
13
|
+
* FLAG-GATED view extension, beside the senior tool — a category is creation
|
|
14
|
+
* tooling, so a disabled framework must contribute none (`docs/adr/0009`):
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* context.register(TemplateCategoryExtension(wardleyTemplateCategory));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Replaces the module-level registry of 0.38.0–0.38.2 (`extendTemplateCategory`)
|
|
21
|
+
* that appended from `effect()` and never removed: a category outlived the
|
|
22
|
+
* editor whose flags had admitted it, so a framework switched off in the host
|
|
23
|
+
* kept its category until a full reload (#244). The DI container is per editor;
|
|
24
|
+
* nothing has to be un-registered.
|
|
25
|
+
*/
|
|
26
|
+
export function TemplateCategoryExtension(...categories) {
|
|
27
|
+
return {
|
|
28
|
+
setup: di => {
|
|
29
|
+
for (const category of categories) {
|
|
30
|
+
di.addImpl(TemplateCategoryIdentifier(category.name), () => category);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
2
35
|
function loadCategory(category) {
|
|
3
36
|
return Array.isArray(category.templates)
|
|
4
37
|
? Promise.resolve(category.templates)
|
|
5
38
|
: category.templates();
|
|
6
39
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The catalogue ONE editor sees: the built-in categories (`Other`, plus
|
|
42
|
+
* whatever a host appended through `EdgelessTemplatePanel.templates.extend`)
|
|
43
|
+
* followed by the categories the view extensions mounted on this `std`
|
|
44
|
+
* registered — i.e. exactly the frameworks whose tooling is on.
|
|
45
|
+
*/
|
|
46
|
+
export function templateManagerFor(std) {
|
|
47
|
+
const contributed = [
|
|
48
|
+
...std.provider.getAll(TemplateCategoryIdentifier).values(),
|
|
49
|
+
];
|
|
50
|
+
const byName = (name) => contributed.find(c => c.name === name);
|
|
9
51
|
return {
|
|
10
|
-
categories: () => [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
52
|
+
categories: async () => [
|
|
53
|
+
...new Set([
|
|
54
|
+
...(await builtInTemplates.categories()),
|
|
55
|
+
...contributed.map(c => c.name),
|
|
56
|
+
]),
|
|
57
|
+
],
|
|
58
|
+
// Both halves, so a host's `extend(...)` can still append to a framework's
|
|
59
|
+
// own category (the global half answers `[]` for a name it does not know).
|
|
60
|
+
list: async (name) => {
|
|
61
|
+
const category = byName(name);
|
|
62
|
+
return [
|
|
63
|
+
...(category ? await loadCategory(category) : []),
|
|
64
|
+
...(await builtInTemplates.list(name)),
|
|
65
|
+
];
|
|
66
|
+
},
|
|
67
|
+
search: async (keyword, name) => {
|
|
14
68
|
const k = keyword.trim().toLocaleLowerCase();
|
|
15
|
-
|
|
69
|
+
const pool = name ? [byName(name)].filter(c => !!c) : contributed;
|
|
70
|
+
const own = (await Promise.all(pool.map(loadCategory)))
|
|
71
|
+
.flat()
|
|
72
|
+
.filter(t => t.name?.toLocaleLowerCase().includes(k));
|
|
73
|
+
return [...(await builtInTemplates.search(keyword, name)), ...own];
|
|
16
74
|
},
|
|
17
75
|
};
|
|
18
76
|
}
|
|
19
|
-
const registered = new Set();
|
|
20
|
-
/**
|
|
21
|
-
* Contribute a template category to the edgeless template panel. Idempotent per
|
|
22
|
-
* category name (a framework's ViewExtension `effect()` may run more than once).
|
|
23
|
-
* Lets each framework package own its template definitions while the panel
|
|
24
|
-
* aggregates every contributed category. The future favorites feature reads the
|
|
25
|
-
* same registry to decide which framework toolbars to show.
|
|
26
|
-
*/
|
|
27
|
-
export function extendTemplateCategory(category) {
|
|
28
|
-
if (registered.has(category.name))
|
|
29
|
-
return;
|
|
30
|
-
registered.add(category.name);
|
|
31
|
-
EdgelessTemplatePanel.templates.extend(categoryManager(category));
|
|
32
|
-
}
|
|
@@ -5,12 +5,19 @@ import type { Template } from './template-type.js';
|
|
|
5
5
|
declare const EdgelessTemplatePanel_base: typeof LitElement & import("../../global/utils/types.js").Constructor<import("../../global/lit/with-disposable.js").DisposableClass>;
|
|
6
6
|
export declare class EdgelessTemplatePanel extends EdgelessTemplatePanel_base {
|
|
7
7
|
static styles: import("lit").CSSResult;
|
|
8
|
+
/**
|
|
9
|
+
* The GLOBAL half of the catalogue — `Other` and a host's `extend(...)`.
|
|
10
|
+
* Framework categories are not here: they live in the editor's DI container
|
|
11
|
+
* (`TemplateCategoryExtension`) and are read through {@link catalogue}.
|
|
12
|
+
*/
|
|
8
13
|
static templates: {
|
|
9
14
|
list: (category: string) => Promise<Template[]>;
|
|
10
15
|
categories: () => Promise<string[]>;
|
|
11
16
|
search: (keyword: string, cateName?: string) => Promise<Template[]>;
|
|
12
17
|
extend(manager: import("./template-type.js").TemplateManager): void;
|
|
13
18
|
};
|
|
19
|
+
/** What THIS edgeless offers: the global half plus its std's categories. */
|
|
20
|
+
get catalogue(): import("./template-type.js").TemplateManager;
|
|
14
21
|
private _fetchJob;
|
|
15
22
|
draggableController: EdgelessDraggableElementController<Template>;
|
|
16
23
|
private _closePanel;
|
|
@@ -45,6 +45,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
45
45
|
import { repeat } from 'lit/directives/repeat.js';
|
|
46
46
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
47
47
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
48
|
+
import { templateManagerFor } from '../contribute.js';
|
|
48
49
|
import { createTemplateJob } from '../services/template.js';
|
|
49
50
|
import { builtInTemplates } from './builtin-templates.js';
|
|
50
51
|
import { defaultPreview, Triangle } from './cards.js';
|
|
@@ -261,7 +262,16 @@ let EdgelessTemplatePanel = (() => {
|
|
|
261
262
|
color: var(--affine-background-overlay-panel-color);
|
|
262
263
|
}
|
|
263
264
|
`; }
|
|
265
|
+
/**
|
|
266
|
+
* The GLOBAL half of the catalogue — `Other` and a host's `extend(...)`.
|
|
267
|
+
* Framework categories are not here: they live in the editor's DI container
|
|
268
|
+
* (`TemplateCategoryExtension`) and are read through {@link catalogue}.
|
|
269
|
+
*/
|
|
264
270
|
static { this.templates = builtInTemplates; }
|
|
271
|
+
/** What THIS edgeless offers: the global half plus its std's categories. */
|
|
272
|
+
get catalogue() {
|
|
273
|
+
return templateManagerFor(this.edgeless.std);
|
|
274
|
+
}
|
|
265
275
|
_closePanel() {
|
|
266
276
|
if (this.isDragging)
|
|
267
277
|
return;
|
|
@@ -293,9 +303,14 @@ let EdgelessTemplatePanel = (() => {
|
|
|
293
303
|
}
|
|
294
304
|
async _initCategory() {
|
|
295
305
|
try {
|
|
296
|
-
this._categories = await
|
|
306
|
+
this._categories = await this.catalogue.categories();
|
|
307
|
+
// The remembered tab may belong to a framework this editor no longer
|
|
308
|
+
// registers (switched off since): fall back rather than show an empty list.
|
|
309
|
+
const remembered = this._getLocalSelectedCategory();
|
|
297
310
|
this._currentCategory =
|
|
298
|
-
|
|
311
|
+
remembered && this._categories.includes(remembered)
|
|
312
|
+
? remembered
|
|
313
|
+
: this._categories[0];
|
|
299
314
|
this._updateTemplates();
|
|
300
315
|
}
|
|
301
316
|
catch (e) {
|
|
@@ -366,8 +381,8 @@ let EdgelessTemplatePanel = (() => {
|
|
|
366
381
|
this._fetch(async (state) => {
|
|
367
382
|
try {
|
|
368
383
|
const templates = this._searchKeyword
|
|
369
|
-
? await
|
|
370
|
-
: await
|
|
384
|
+
? await this.catalogue.search(this._searchKeyword)
|
|
385
|
+
: await this.catalogue.list(this._currentCategory);
|
|
371
386
|
if (state.canceled)
|
|
372
387
|
return;
|
|
373
388
|
this._templates = templates;
|
|
@@ -57,7 +57,16 @@ function isNodeType(node) {
|
|
|
57
57
|
}
|
|
58
58
|
function observeChildren(_, instance, transaction) {
|
|
59
59
|
if (instance.children.doc) {
|
|
60
|
-
|
|
60
|
+
// `?? false`, like every other group-like element (see `group.ts`): a
|
|
61
|
+
// missing transaction is NOT a local gesture. `startObserve` re-runs this
|
|
62
|
+
// observer with `transaction === null` whenever the `children` key itself
|
|
63
|
+
// is rewritten — by a remote peer, a snapshot import or an undo — and
|
|
64
|
+
// reporting that as local makes the group watchers treat a received change
|
|
65
|
+
// as one this peer just made: they delete an emptied mindmap locally,
|
|
66
|
+
// which a readonly viewer cannot do (#251, rule from #242).
|
|
67
|
+
instance.setChildIds(Array.from(instance.children.keys()), transaction?.local ?? false);
|
|
68
|
+
// Pure local recompute, no CRDT write: unconditional on purpose, every
|
|
69
|
+
// peer must rebuild its tree to repaint.
|
|
61
70
|
instance.buildTree();
|
|
62
71
|
instance.connectors.clear();
|
|
63
72
|
}
|
|
@@ -7,6 +7,13 @@ import { html, nothing } from 'lit';
|
|
|
7
7
|
import { literal, unsafeStatic } from 'lit/static-html.js';
|
|
8
8
|
import { defaultKeyboardToolbarConfig, KeyboardToolbarConfigExtension, } from './config.js';
|
|
9
9
|
export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget';
|
|
10
|
+
/**
|
|
11
|
+
* Signals backing the fallback keyboard, at module level on purpose: the
|
|
12
|
+
* `keyboard` getter is read from `render()`, so a fresh `signal()` per call
|
|
13
|
+
* would hand `PositionController`'s effect a brand new object every frame.
|
|
14
|
+
*/
|
|
15
|
+
const fallbackKeyboardVisible$ = signal(false);
|
|
16
|
+
const fallbackKeyboardHeight$ = signal(0);
|
|
10
17
|
export class AffineKeyboardToolbarWidget extends WidgetComponent {
|
|
11
18
|
constructor() {
|
|
12
19
|
super(...arguments);
|
|
@@ -26,12 +33,20 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent {
|
|
|
26
33
|
this._initialInputMode = '';
|
|
27
34
|
}
|
|
28
35
|
get keyboard() {
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// The provider is host-supplied — a native mobile shell reports the
|
|
37
|
+
// on-screen keyboard. Labre ships no such shell, so a plain web host
|
|
38
|
+
// registers nothing and this lookup must stay optional; a hard `get` throws
|
|
39
|
+
// and takes the whole document open down with it (issue #247).
|
|
40
|
+
const provider = this.std.getOptional(VirtualKeyboardProvider);
|
|
41
|
+
if (provider && isVirtualKeyboardProviderWithAction(provider))
|
|
31
42
|
return provider;
|
|
32
43
|
return {
|
|
33
|
-
//
|
|
44
|
+
// Fallback keyboard, complete WITHOUT a provider: the signals come first
|
|
45
|
+
// so that a provider without actions still wins on them through the
|
|
46
|
+
// spread below, while its `show`/`hide` stay ours.
|
|
34
47
|
fallback: true,
|
|
48
|
+
visible$: fallbackKeyboardVisible$,
|
|
49
|
+
height$: fallbackKeyboardHeight$,
|
|
35
50
|
show: () => {
|
|
36
51
|
const rootComponent = this.block?.rootComponent;
|
|
37
52
|
if (rootComponent && rootComponent === document.activeElement) {
|
|
@@ -13,7 +13,13 @@ export declare class AffineMobileLinkedDocMenu extends AffineMobileLinkedDocMenu
|
|
|
13
13
|
private readonly _updateLinkedDocGroup;
|
|
14
14
|
private _updateLinkedDocGroupAbortController;
|
|
15
15
|
private get _query();
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The virtual keyboard provider is host-supplied — a native mobile shell
|
|
18
|
+
* reports the on-screen keyboard. Labre ships no such shell, so a plain web
|
|
19
|
+
* host registers nothing and this lookup must stay optional; a hard `get`
|
|
20
|
+
* throws and takes the whole document open down with it (issue #247).
|
|
21
|
+
*/
|
|
22
|
+
get keyboard(): VirtualKeyboardProvider | null;
|
|
17
23
|
connectedCallback(): void;
|
|
18
24
|
firstUpdated(): void;
|
|
19
25
|
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
@@ -71,8 +71,14 @@ let AffineMobileLinkedDocMenu = (() => {
|
|
|
71
71
|
get _query() {
|
|
72
72
|
return getTextContentFromInlineRange(this.context.inlineEditor, this.context.startRange);
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The virtual keyboard provider is host-supplied — a native mobile shell
|
|
76
|
+
* reports the on-screen keyboard. Labre ships no such shell, so a plain web
|
|
77
|
+
* host registers nothing and this lookup must stay optional; a hard `get`
|
|
78
|
+
* throws and takes the whole document open down with it (issue #247).
|
|
79
|
+
*/
|
|
74
80
|
get keyboard() {
|
|
75
|
-
return this.context.std.
|
|
81
|
+
return this.context.std.getOptional(VirtualKeyboardProvider) ?? null;
|
|
76
82
|
}
|
|
77
83
|
connectedCallback() {
|
|
78
84
|
super.connectedCallback();
|
|
@@ -114,7 +120,7 @@ let AffineMobileLinkedDocMenu = (() => {
|
|
|
114
120
|
if (groups.length === 0) {
|
|
115
121
|
return nothing;
|
|
116
122
|
}
|
|
117
|
-
this.style.bottom = `${this.keyboard
|
|
123
|
+
this.style.bottom = `${this.keyboard?.height$.value ?? 0}px`;
|
|
118
124
|
return html `
|
|
119
125
|
${join(groups.map(this._renderGroup), html `<div class="divider"></div>`)}
|
|
120
126
|
`;
|
|
@@ -20,7 +20,7 @@ export { GfxLocalElementModel, prop, } from './model/surface/local-element-model
|
|
|
20
20
|
export { collectPivotOccurrences, isPivotBound, type PivotBoundElement, type PivotOccurrence, resolvePivotBinding, } from './model/surface/pivot.js';
|
|
21
21
|
export { type EdgeDirectionDef, findRoleDef, isTypedEdgeRole, type RoleDef, type RoleDefs, type RoleId, roleIsA, type RoleKind, RoleVocabularyExtension, RoleVocabularyIdentifier, } from './model/surface/role.js';
|
|
22
22
|
export { ELEMENT_TAGS_FIELD, type ElementTags, elementTagValues, hasElementTagValue, readElementTags, setElementTag, tagsPropToY, } from './model/surface/tags.js';
|
|
23
|
-
export { SURFACE_TEXT_UNIQ_IDENTIFIER, SURFACE_YMAP_UNIQ_IDENTIFIER, SurfaceBlockModel, type SurfaceBlockProps, type SurfaceMiddleware, } from './model/surface/surface-model.js';
|
|
23
|
+
export { type ElementUpdatedData, SURFACE_TEXT_UNIQ_IDENTIFIER, SURFACE_YMAP_UNIQ_IDENTIFIER, SurfaceBlockModel, type SurfaceBlockProps, type SurfaceMiddleware, } from './model/surface/surface-model.js';
|
|
24
24
|
export { measureOperation } from './perf.js';
|
|
25
25
|
export { GfxSelectionManager } from './selection.js';
|
|
26
26
|
export { SurfaceMiddlewareBuilder, SurfaceMiddlewareExtension, } from './surface-middleware.js';
|