@lvce-editor/extension-host-worker 8.34.0 → 8.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,50 @@ import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
4
4
  const views = Object.create(null);
5
5
  const instances = Object.create(null);
6
6
  const renderedDoms = Object.create(null);
7
+ const contexts = Object.create(null);
8
+ const contextViewIds = Object.create(null);
9
+ const assertBoolean = (value, message) => {
10
+ if (value !== undefined && typeof value !== 'boolean') {
11
+ throw new ExtensionApiError(message);
12
+ }
13
+ };
14
+ const assertNumber = (value, message) => {
15
+ if (typeof value !== 'number') {
16
+ throw new ExtensionApiError(message);
17
+ }
18
+ };
19
+ const assertString = (value, message) => {
20
+ if (typeof value !== 'string' || value.length === 0) {
21
+ throw new ExtensionApiError(message);
22
+ }
23
+ };
24
+ const assertEventListener = (viewId, listener, index) => {
25
+ if (!listener || typeof listener !== 'object') {
26
+ throw new ExtensionApiError(`view ${viewId} event listener ${index} must be an object`);
27
+ }
28
+ const eventListener = listener;
29
+ if (typeof eventListener.name !== 'string' && typeof eventListener.name !== 'number') {
30
+ throw new ExtensionApiError(`view ${viewId} event listener ${index} is missing name`);
31
+ }
32
+ if (!Array.isArray(eventListener.params) || eventListener.params.some((param) => typeof param !== 'string')) {
33
+ throw new ExtensionApiError(`view ${viewId} event listener ${index} is missing params`);
34
+ }
35
+ assertBoolean(eventListener.capture, `view ${viewId} event listener ${index} has invalid capture`);
36
+ assertBoolean(eventListener.passive, `view ${viewId} event listener ${index} has invalid passive`);
37
+ assertBoolean(eventListener.preventDefault, `view ${viewId} event listener ${index} has invalid preventDefault`);
38
+ assertBoolean(eventListener.stopPropagation, `view ${viewId} event listener ${index} has invalid stopPropagation`);
39
+ };
40
+ const assertEventListeners = (view) => {
41
+ if (view.eventListeners === undefined) {
42
+ return;
43
+ }
44
+ if (!Array.isArray(view.eventListeners)) {
45
+ throw new ExtensionApiError(`view ${view.id} eventListeners must be an array`);
46
+ }
47
+ for (const [index, listener] of view.eventListeners.entries()) {
48
+ assertEventListener(view.id, listener, index);
49
+ }
50
+ };
7
51
  const assertView = (view) => {
8
52
  if (!view) {
9
53
  throw new ExtensionApiError('view is not defined');
@@ -17,11 +61,13 @@ const assertView = (view) => {
17
61
  if (view.id in views) {
18
62
  throw new ExtensionApiError(`view ${view.id} is already registered`);
19
63
  }
64
+ assertEventListeners(view);
20
65
  };
21
66
  const toRegisteredView = (view) => {
22
67
  const displayName = view.displayName || view.name || view.title;
23
68
  const registeredView = {
24
69
  displayName,
70
+ ...(view.eventListeners && { eventListeners: view.eventListeners }),
25
71
  icon: view.icon,
26
72
  id: view.id,
27
73
  name: view.name,
@@ -73,6 +119,34 @@ const renderDom = async (instance) => {
73
119
  }
74
120
  return dom;
75
121
  };
122
+ const normalizeMenuEntry = (entry, index) => {
123
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
124
+ throw new ExtensionApiError(`menu entry ${index} must be an object`);
125
+ }
126
+ const menuEntry = entry;
127
+ assertString(menuEntry.id, `menu entry ${index} is missing id`);
128
+ assertString(menuEntry.label, `menu entry ${index} is missing label`);
129
+ assertString(menuEntry.command, `menu entry ${index} is missing command`);
130
+ if (menuEntry.flags !== undefined && typeof menuEntry.flags !== 'number') {
131
+ throw new ExtensionApiError(`menu entry ${index} has invalid flags`);
132
+ }
133
+ if (menuEntry.args !== undefined && !Array.isArray(menuEntry.args)) {
134
+ throw new ExtensionApiError(`menu entry ${index} has invalid args`);
135
+ }
136
+ return {
137
+ command: menuEntry.command,
138
+ flags: menuEntry.flags ?? 0,
139
+ id: menuEntry.id,
140
+ label: menuEntry.label,
141
+ ...(menuEntry.args && { args: menuEntry.args }),
142
+ };
143
+ };
144
+ const normalizeMenuEntries = (entries) => {
145
+ if (!Array.isArray(entries)) {
146
+ throw new ExtensionApiError('view menu entries must be an array');
147
+ }
148
+ return entries.map(normalizeMenuEntry);
149
+ };
76
150
  const renderPatches = async (uid, instance) => {
77
151
  const oldDom = renderedDoms[uid] || [];
78
152
  const newDom = await renderDom(instance);
@@ -83,6 +157,55 @@ const renderPatches = async (uid, instance) => {
83
157
  type: 'setPatches',
84
158
  };
85
159
  };
160
+ const normalizeContext = (context) => {
161
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
162
+ return {};
163
+ }
164
+ const normalized = {};
165
+ for (const [key, value] of Object.entries(context)) {
166
+ if (typeof value === 'boolean') {
167
+ normalized[key] = value;
168
+ }
169
+ }
170
+ return normalized;
171
+ };
172
+ const isSameContext = (oldContext, newContext) => {
173
+ const oldKeys = Object.keys(oldContext);
174
+ const newKeys = Object.keys(newContext);
175
+ if (oldKeys.length !== newKeys.length) {
176
+ return false;
177
+ }
178
+ for (const key of oldKeys) {
179
+ if (oldContext[key] !== newContext[key]) {
180
+ return false;
181
+ }
182
+ }
183
+ return true;
184
+ };
185
+ const maybeNotifyContextChanged = async (uid, viewId, instance) => {
186
+ if (typeof instance.getContext !== 'function') {
187
+ return;
188
+ }
189
+ const oldContext = contexts[uid] || {};
190
+ const newContext = normalizeContext(instance.getContext());
191
+ if (isSameContext(oldContext, newContext)) {
192
+ return;
193
+ }
194
+ if (Object.keys(newContext).length === 0) {
195
+ delete contexts[uid];
196
+ }
197
+ else {
198
+ contexts[uid] = newContext;
199
+ }
200
+ await ExtensionManagementWorker.invoke('Extensions.handleViewContextChange', uid, viewId, newContext);
201
+ };
202
+ const maybeClearContext = async (uid, viewId) => {
203
+ if (!contexts[uid]) {
204
+ return;
205
+ }
206
+ delete contexts[uid];
207
+ await ExtensionManagementWorker.invoke('Extensions.handleViewContextChange', uid, viewId, {});
208
+ };
86
209
  export const createViewInstance = async (viewId, uid, context) => {
87
210
  const view = views[viewId];
88
211
  if (!view) {
@@ -96,13 +219,21 @@ export const createViewInstance = async (viewId, uid, context) => {
96
219
  requestRerender() {
97
220
  return ExtensionManagementWorker.invoke('Extensions.requestViewRerender', uid);
98
221
  },
222
+ showContextMenu(menuId, x, y) {
223
+ assertString(menuId, 'menuId must be a string');
224
+ assertNumber(x, 'x must be a number');
225
+ assertNumber(y, 'y must be a number');
226
+ return ExtensionManagementWorker.invoke('Extensions.showViewContextMenu', uid, viewId, menuId, x, y);
227
+ },
99
228
  uid,
100
229
  viewId,
101
230
  });
102
231
  assertVirtualDomViewInstance(viewId, instance);
103
232
  instances[uid] = instance;
233
+ contextViewIds[uid] = viewId;
104
234
  const dom = await renderDom(instance);
105
235
  renderedDoms[uid] = dom;
236
+ await maybeNotifyContextChanged(uid, viewId, instance);
106
237
  return {
107
238
  dom,
108
239
  type: 'setDom',
@@ -113,19 +244,25 @@ export const dispatchViewEvent = async (uid, event) => {
113
244
  if (typeof instance.handleEvent === 'function') {
114
245
  await instance.handleEvent(event);
115
246
  }
116
- return renderPatches(uid, instance);
247
+ const result = await renderPatches(uid, instance);
248
+ await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
249
+ return result;
117
250
  };
118
251
  export const renderViewInstance = async (uid) => {
119
252
  const instance = getVirtualDomInstance(uid);
120
- return renderPatches(uid, instance);
253
+ const result = await renderPatches(uid, instance);
254
+ await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
255
+ return result;
121
256
  };
122
257
  export const disposeViewInstance = async (uid) => {
123
258
  const instance = instances[uid];
124
259
  if (instance && typeof instance.dispose === 'function') {
125
260
  await instance.dispose();
126
261
  }
262
+ await maybeClearContext(uid, contextViewIds[uid]);
127
263
  delete instances[uid];
128
264
  delete renderedDoms[uid];
265
+ delete contextViewIds[uid];
129
266
  };
130
267
  export const saveViewInstanceState = async (uid) => {
131
268
  const instance = getVirtualDomInstance(uid);
@@ -134,6 +271,14 @@ export const saveViewInstanceState = async (uid) => {
134
271
  }
135
272
  return instance.saveState();
136
273
  };
274
+ export const getViewMenuEntries = async (uid, menuId) => {
275
+ assertString(menuId, 'menuId must be a string');
276
+ const instance = getVirtualDomInstance(uid);
277
+ if (typeof instance.getMenuEntries !== 'function') {
278
+ return [];
279
+ }
280
+ return normalizeMenuEntries(await instance.getMenuEntries(menuId));
281
+ };
137
282
  export const getViewRegistrySnapshot = () => {
138
283
  return {
139
284
  views: Object.values(views).map(toRegisteredView),
@@ -149,4 +294,10 @@ export const resetViewRegistry = () => {
149
294
  for (const uid of Object.keys(renderedDoms)) {
150
295
  delete renderedDoms[Number(uid)];
151
296
  }
297
+ for (const uid of Object.keys(contexts)) {
298
+ delete contexts[Number(uid)];
299
+ }
300
+ for (const uid of Object.keys(contextViewIds)) {
301
+ delete contextViewIds[Number(uid)];
302
+ }
152
303
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/api",
3
- "version": "8.34.0",
3
+ "version": "8.35.0",
4
4
  "description": "Tree-shakeable extension API for Lvce Editor extensions.",
5
5
  "keywords": [
6
6
  "lvce-editor",