@contello/extension 8.21.0 → 8.21.3

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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @contello/extension
2
+
3
+ Client SDK for building Contello CMS extensions and custom properties. Communicates with the Contello host application via postMessage channels.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @contello/extension
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Extension
14
+
15
+ Extensions are full-page iframe apps embedded in the Contello UI.
16
+
17
+ ```ts
18
+ import { ContelloExtension } from '@contello/extension';
19
+
20
+ const extension = await ContelloExtension.connect({
21
+ trustedOrigins: ['https://your-contello-instance.com'],
22
+ });
23
+
24
+ await extension.ready();
25
+
26
+ // get URL path and query data
27
+ const { path, query } = await extension.getUrlData();
28
+
29
+ // set breadcrumbs
30
+ await extension.setBreadcrumbs([
31
+ { label: 'Home', url: '/' },
32
+ { label: 'Current Page' },
33
+ ]);
34
+
35
+ // navigate
36
+ await extension.navigate(extension.createEntityDetailUrl('articles', { mode: 'edit', id: '123' }));
37
+
38
+ // show notifications
39
+ await extension.displayNotification('success', 'Saved successfully');
40
+ ```
41
+
42
+ ### Custom property
43
+
44
+ Custom properties are smaller iframe widgets used inside entity forms.
45
+
46
+ ```ts
47
+ import { ContelloCustomProperty } from '@contello/extension';
48
+
49
+ const property = await ContelloCustomProperty.connect({
50
+ trustedOrigins: ['https://your-contello-instance.com'],
51
+ validator: () => true,
52
+ newValue: (value) => console.log('new value:', value),
53
+ });
54
+
55
+ await property.ready();
56
+
57
+ // get / set the property value
58
+ const value = await property.getValue();
59
+ await property.setValue('new value');
60
+
61
+ // get / set values by path (for complex entities)
62
+ const nested = await property.getValueByPath('some.path');
63
+ await property.setValueByPath('some.path', 'new value');
64
+ ```
65
+
66
+ ### Dialogs
67
+
68
+ Open modal dialogs from extensions or custom properties.
69
+
70
+ ```ts
71
+ const dialog = extension.openDialog<InputData, ResultData>({
72
+ url: 'https://my-dialog.example.com',
73
+ width: 800,
74
+ data: { someInput: 'value' },
75
+ });
76
+
77
+ await dialog.open;
78
+ await dialog.ready;
79
+
80
+ const result = await dialog.complete;
81
+ ```
82
+
83
+ Inside the dialog iframe:
84
+
85
+ ```ts
86
+ import { ContelloDialog } from '@contello/extension';
87
+
88
+ const dialog = await ContelloDialog.connect<InputData, ResultData>({
89
+ trustedOrigins: ['https://your-contello-instance.com'],
90
+ });
91
+
92
+ await dialog.ready();
93
+
94
+ // access data passed from the opener
95
+ console.log(dialog.data);
96
+
97
+ // close with a result
98
+ await dialog.close({ selectedId: '456' });
99
+ ```
100
+
101
+ ### Auth token
102
+
103
+ Retrieve the current authentication token from the host:
104
+
105
+ ```ts
106
+ const token = await extension.getAuthToken();
107
+ ```
108
+
109
+ ## API
110
+
111
+ ### Classes
112
+
113
+ | Class | Description |
114
+ |-------|-------------|
115
+ | `ContelloExtension` | Full-page extension client |
116
+ | `ContelloCustomProperty` | Custom property widget client |
117
+ | `ContelloDialog` | Dialog iframe client |
118
+ | `ContelloDialogRef` | Handle returned by `openDialog()` |
119
+ | `ExtensionChannel` | Low-level postMessage channel (advanced use) |
120
+
121
+ ## License
122
+
123
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ContelloCustomProperty: () => ContelloCustomProperty,
24
+ ContelloDialog: () => ContelloDialog,
25
+ ContelloDialogRef: () => ContelloDialogRef,
26
+ ContelloExtension: () => ContelloExtension,
27
+ ExtensionChannel: () => ExtensionChannel
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/channel.ts
32
+ var channelIdIterator = 0;
33
+ var requestIdIterator = 0;
34
+ var ExtensionChannel = class {
35
+ constructor(params) {
36
+ this.params = params;
37
+ }
38
+ handlers = /* @__PURE__ */ new Map();
39
+ listeners = /* @__PURE__ */ new Map();
40
+ targetWindow;
41
+ channelId;
42
+ targetOrigin;
43
+ isParent;
44
+ populateChannelId() {
45
+ if (!this.channelId) {
46
+ this.channelId = this.createChannelId();
47
+ }
48
+ return this.channelId;
49
+ }
50
+ connectParent(targetOrigin, channelId) {
51
+ this.channelId = channelId;
52
+ this.targetOrigin = targetOrigin;
53
+ this.targetWindow = window.parent;
54
+ this.isParent = false;
55
+ this.connect();
56
+ }
57
+ connectChild(targetWindow) {
58
+ this.populateChannelId();
59
+ this.targetWindow = targetWindow;
60
+ this.targetOrigin = "*";
61
+ this.isParent = true;
62
+ this.connect();
63
+ }
64
+ getChannelId() {
65
+ return this.channelId;
66
+ }
67
+ getIsDebug() {
68
+ return this.params.debug;
69
+ }
70
+ getTargetOrigin() {
71
+ return this.targetOrigin;
72
+ }
73
+ getTargetWindow() {
74
+ return this.targetWindow;
75
+ }
76
+ connect() {
77
+ window.addEventListener("message", this.handler);
78
+ }
79
+ disconnect() {
80
+ window.removeEventListener("message", this.handler);
81
+ }
82
+ handler = (event) => {
83
+ if (event.data.channelId !== this.channelId) {
84
+ return;
85
+ }
86
+ if (this.targetOrigin === "*" || event.origin === this.targetOrigin) {
87
+ const { channelId: requestChannelId, requestId, method } = event.data;
88
+ if (requestChannelId === this.channelId) {
89
+ if (this.params.debug) {
90
+ console.log(this.isParent ? "Parent received" : "Child received", event.data);
91
+ }
92
+ if (this.handlers.has(requestId)) {
93
+ this.handlers.get(requestId)(event.data);
94
+ } else if (this.listeners.has(method)) {
95
+ Promise.resolve(this.listeners.get(method)?.(event.data.payload)).then((responsePayload) => this.respond(event.data, responsePayload)).catch((err) => this.respondError(event.data, err));
96
+ }
97
+ }
98
+ }
99
+ };
100
+ respond(request, payload) {
101
+ this.send({ channelId: request.channelId, requestId: request.requestId, method: request.method, payload });
102
+ }
103
+ respondError(request, error) {
104
+ this.send({ channelId: request.channelId, requestId: request.requestId, method: request.method, error });
105
+ }
106
+ on(method, handler) {
107
+ this.listeners.set(method, handler);
108
+ }
109
+ call(method, message) {
110
+ return new Promise((resolve, reject) => {
111
+ const requestId = this.createRequestId();
112
+ this.send({ channelId: this.channelId, requestId, method, payload: message });
113
+ this.handlers.set(requestId, (data) => {
114
+ this.handlers.delete(requestId);
115
+ if (data.error) {
116
+ return reject(data.error);
117
+ }
118
+ resolve(data.payload);
119
+ });
120
+ });
121
+ }
122
+ send(data) {
123
+ this.targetWindow?.postMessage(data, this.targetOrigin);
124
+ }
125
+ createChannelId() {
126
+ return `contello-channel-${++channelIdIterator}-${Math.random().toString(36).substring(2)}`;
127
+ }
128
+ createRequestId() {
129
+ return `${this.isParent ? "parent" : "child"}-request-${++requestIdIterator}-${Math.random().toString(36).substring(2)}`;
130
+ }
131
+ };
132
+
133
+ // src/dialog-ref.ts
134
+ var ContelloDialogRef = class {
135
+ _id;
136
+ open;
137
+ connected;
138
+ ready;
139
+ complete;
140
+ close;
141
+ constructor({ channel, options, controller }) {
142
+ this.open = channel.call("openDialog", options).then(({ id }) => this._id = id);
143
+ this.connected = controller.connected.promise;
144
+ this.ready = controller.ready.promise;
145
+ this.complete = controller.complete.promise;
146
+ this.close = controller.close;
147
+ }
148
+ get id() {
149
+ return this._id;
150
+ }
151
+ };
152
+
153
+ // src/utils.ts
154
+ var Deferred = class {
155
+ resolve;
156
+ reject;
157
+ promise = new Promise((resolve, reject) => {
158
+ this.resolve = resolve;
159
+ this.reject = reject;
160
+ });
161
+ };
162
+
163
+ // src/client.ts
164
+ var ContelloClient = class {
165
+ channel;
166
+ projectId;
167
+ resizeObserver;
168
+ targetOrigin;
169
+ data;
170
+ dialogs = /* @__PURE__ */ new Map();
171
+ constructor(targetOrigin, channelId, projectId, debug) {
172
+ this.channel = new ExtensionChannel({ debug });
173
+ this.channel.connectParent(targetOrigin, channelId);
174
+ this.projectId = projectId;
175
+ this.targetOrigin = targetOrigin;
176
+ }
177
+ connect() {
178
+ return this.channel.call("connect").then(({ data }) => {
179
+ this.channel.on("dialogConnect", ({ id }) => this.getDialogController(id)?.connected.resolve());
180
+ this.channel.on("dialogReady", ({ id }) => this.getDialogController(id)?.ready.resolve());
181
+ this.channel.on("dialogComplete", ({ id, value }) => this.getDialogController(id)?.complete.resolve(value));
182
+ this.data = data;
183
+ });
184
+ }
185
+ ready() {
186
+ this.listenForResize();
187
+ return this.channel.call("ready", { height: this.getWindowHeight() });
188
+ }
189
+ getAuthToken() {
190
+ return this.channel.call("getAuthToken").then(({ token }) => token);
191
+ }
192
+ createProjectUrl() {
193
+ return `${this.targetOrigin}/ui/projects/${this.projectId}`;
194
+ }
195
+ createEntityEntryUrl(referenceName) {
196
+ return `${this.createProjectUrl()}/entities/${referenceName}`;
197
+ }
198
+ createSingletonEntityUrl(referenceName) {
199
+ return this.createEntityEntryUrl(referenceName);
200
+ }
201
+ createEntityDetailUrl(referenceName, params) {
202
+ const base = this.createEntityEntryUrl(referenceName);
203
+ if (params.mode === "create") {
204
+ return `${base}/create`;
205
+ }
206
+ return `${base}/${params.mode}/${params.id}`;
207
+ }
208
+ /**
209
+ * @deprecated Use createEntityDetailUrl instead
210
+ */
211
+ createEntityUrl(referenceName, entityId) {
212
+ return this.createEntityDetailUrl(referenceName, { mode: "edit", id: entityId });
213
+ }
214
+ createExtensionUrl(referenceName, params) {
215
+ const path = params?.path?.join("/") || "";
216
+ const query = new URLSearchParams(params?.query || {}).toString();
217
+ return `${this.createProjectUrl()}/extensions/${referenceName}${path ? `/${path}` : ""}${query ? `?${query}` : ""}`;
218
+ }
219
+ navigate(url) {
220
+ return this.channel.call("navigate", { url });
221
+ }
222
+ displayNotification(type, message) {
223
+ return this.channel.call("displayNotification", { type, message });
224
+ }
225
+ openDialog(options) {
226
+ const controller = {
227
+ connected: new Deferred(),
228
+ ready: new Deferred(),
229
+ complete: new Deferred(),
230
+ close: () => {
231
+ if (!this.channel) {
232
+ throw new Error("The channel is not yet initialized");
233
+ }
234
+ this.channel.call("closeDialog", { id: dialog.id });
235
+ this.dialogs.delete(dialog);
236
+ }
237
+ };
238
+ const dialog = new ContelloDialogRef({ channel: this.channel, options, controller });
239
+ this.dialogs.set(dialog, controller);
240
+ dialog.complete.then(() => this.dialogs.delete(dialog));
241
+ return dialog;
242
+ }
243
+ listenForResize() {
244
+ this.resizeObserver = new ResizeObserver(() => {
245
+ this.channel.call("resize", { height: this.getWindowHeight() });
246
+ });
247
+ this.resizeObserver.observe(document.documentElement);
248
+ }
249
+ getWindowHeight() {
250
+ return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.body.clientHeight);
251
+ }
252
+ getDialogController(id) {
253
+ const key = Array.from(this.dialogs.keys()).find((dialog) => dialog.id === id);
254
+ if (!key) {
255
+ return;
256
+ }
257
+ return this.dialogs.get(key);
258
+ }
259
+ };
260
+
261
+ // src/url-parser.ts
262
+ function parseUrl(trustedOrigins) {
263
+ const url = new URL(location.href);
264
+ const channelId = url.searchParams.get("channelId");
265
+ const targetOrigin = url.searchParams.get("origin");
266
+ const applicationId = url.searchParams.get("applicationId");
267
+ const debug = url.searchParams.get("debug") === "true";
268
+ if (!channelId || !targetOrigin || !applicationId) {
269
+ throw new Error("Missing required URL parameters");
270
+ }
271
+ if (!trustedOrigins?.length) {
272
+ throw new Error("No trusted origins provided");
273
+ }
274
+ if (!trustedOrigins.includes(targetOrigin)) {
275
+ throw new Error(`Origin ${targetOrigin} is not trusted`);
276
+ }
277
+ return { channelId, targetOrigin, applicationId, debug };
278
+ }
279
+
280
+ // src/custom-property.ts
281
+ var ContelloCustomProperty = class _ContelloCustomProperty extends ContelloClient {
282
+ static connect(options) {
283
+ const { targetOrigin, channelId, applicationId, debug } = parseUrl(options.trustedOrigins);
284
+ const customProperty = new _ContelloCustomProperty(targetOrigin, channelId, applicationId, debug);
285
+ customProperty.validate = options.validator || (() => true);
286
+ customProperty.newValue = options.newValue || (() => null);
287
+ return customProperty.connect().then(() => customProperty);
288
+ }
289
+ validate = () => true;
290
+ newValue = () => null;
291
+ constructor(targetOrigin, channelId, applicationId, debug) {
292
+ super(targetOrigin, channelId, applicationId, debug);
293
+ this.channel.on("validate", async () => {
294
+ const valid = await Promise.resolve(this.validate());
295
+ return { valid };
296
+ });
297
+ this.channel.on("newValue", async (msg) => {
298
+ await Promise.resolve(this.newValue(msg.value));
299
+ });
300
+ }
301
+ async getValue() {
302
+ const r = await this.channel.call("getValue");
303
+ return r.value;
304
+ }
305
+ async setValue(value) {
306
+ const valid = await Promise.resolve(this.validate());
307
+ return await this.channel.call("setValue", { value, valid });
308
+ }
309
+ async getValueByPath(path) {
310
+ const r = await this.channel.call("getValueByPath", { path });
311
+ return r.value;
312
+ }
313
+ async setValueByPath(path, value) {
314
+ return this.channel.call("setValueByPath", { value, path });
315
+ }
316
+ };
317
+
318
+ // src/dialog.ts
319
+ var ContelloDialog = class _ContelloDialog extends ContelloClient {
320
+ static connect({ trustedOrigins }) {
321
+ const { targetOrigin, channelId, applicationId, debug } = parseUrl(trustedOrigins);
322
+ const dialog = new _ContelloDialog(targetOrigin, channelId, applicationId, debug);
323
+ return dialog.connect().then(() => dialog);
324
+ }
325
+ constructor(targetOrigin, channelId, applicationId, debug) {
326
+ super(targetOrigin, channelId, applicationId, debug);
327
+ }
328
+ close(value) {
329
+ return this.channel.call("complete", { value });
330
+ }
331
+ };
332
+
333
+ // src/extension.ts
334
+ var ContelloExtension = class _ContelloExtension extends ContelloClient {
335
+ static connect({ trustedOrigins }) {
336
+ const { targetOrigin, channelId, applicationId, debug } = parseUrl(trustedOrigins);
337
+ const extension = new _ContelloExtension(targetOrigin, channelId, applicationId, debug);
338
+ return extension.connect().then(() => extension);
339
+ }
340
+ constructor(targetOrigin, channelId, applicationId, debug) {
341
+ super(targetOrigin, channelId, applicationId, debug);
342
+ }
343
+ getUrlData() {
344
+ return this.channel.call("getUrlData");
345
+ }
346
+ setBreadcrumbs(breadcrumbs) {
347
+ return this.channel.call("setBreadcrumbs", { breadcrumbs });
348
+ }
349
+ };
350
+ // Annotate the CommonJS export names for ESM import in node:
351
+ 0 && (module.exports = {
352
+ ContelloCustomProperty,
353
+ ContelloDialog,
354
+ ContelloDialogRef,
355
+ ContelloExtension,
356
+ ExtensionChannel
357
+ });