@getdraft/plugin 1.13.0-beta.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.
- package/.eslintrc +3 -0
- package/README.md +312 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.es.js +313 -0
- package/dist/index.es.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/plugin.d.ts +36 -0
- package/dist/src/plugin.d.ts.map +1 -0
- package/dist/src/routes.d.ts +3 -0
- package/dist/src/routes.d.ts.map +1 -0
- package/dist/src/types.d.ts +200 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +3 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/index.ts +31 -0
- package/src/plugin.ts +429 -0
- package/src/routes.ts +2 -0
- package/src/types.ts +247 -0
- package/src/utils.ts +43 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getdraft/plugin",
|
|
3
|
+
"version": "1.13.0-beta.0",
|
|
4
|
+
"main": "dist/index.cjs.js",
|
|
5
|
+
"module": "dist/index.es.js",
|
|
6
|
+
"types": "src",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prebuild": "rm -rf dist",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"dev-tsc": "npx tsc --noEmit",
|
|
11
|
+
"eslint": "npx eslint src",
|
|
12
|
+
"eslint:fix": "pnpm eslint --fix",
|
|
13
|
+
"prettify": "npx prettier -w ./",
|
|
14
|
+
"prepublish": "pnpm build",
|
|
15
|
+
"watch": "NODE_ENV=development vite build --watch",
|
|
16
|
+
"start": "NODE_ENV=development npx vite"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/letty": "workspace:^",
|
|
21
|
+
"@vitejs/plugin-legacy": "5.4.1",
|
|
22
|
+
"rollup": "4.22.4",
|
|
23
|
+
"terser": "5.31.3",
|
|
24
|
+
"rollup-plugin-dts": "6.1.1",
|
|
25
|
+
"@letty/eslint-config-ts": "workspace:^",
|
|
26
|
+
"@letty/prettier": "workspace:^"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PluginInstance } from './plugin';
|
|
2
|
+
import { PluginConfig } from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG: Partial<PluginConfig> = {
|
|
5
|
+
container: '#draft-plugin-container',
|
|
6
|
+
disableHotkeysPassing: false,
|
|
7
|
+
locale: 'ru',
|
|
8
|
+
on: {},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class PluginWrapper {
|
|
12
|
+
create(config: PluginConfig) {
|
|
13
|
+
return new PluginInstance({
|
|
14
|
+
...DEFAULT_CONFIG,
|
|
15
|
+
...config,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DraftPlugin = new PluginWrapper();
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
DraftPlugin: PluginWrapper;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
window.DraftPlugin = DraftPlugin;
|
|
29
|
+
|
|
30
|
+
export default DraftPlugin;
|
|
31
|
+
export * from './types';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { LOCAL_ENDPOINT, PROD_ENDPOINT } from './routes';
|
|
2
|
+
import {
|
|
3
|
+
EditorMenu,
|
|
4
|
+
ExitParams,
|
|
5
|
+
OnInitParams,
|
|
6
|
+
OpenGalleryParams,
|
|
7
|
+
Handlers,
|
|
8
|
+
PluginConfig,
|
|
9
|
+
SaveParams,
|
|
10
|
+
TemplateJSON,
|
|
11
|
+
UploadImageParams,
|
|
12
|
+
ViewMode,
|
|
13
|
+
HandlerParams,
|
|
14
|
+
} from './types';
|
|
15
|
+
import { getImageParamsFromFileUrl } from './utils';
|
|
16
|
+
import pkg from '../package.json';
|
|
17
|
+
|
|
18
|
+
const getLatinKey = (key: string, code: string) => {
|
|
19
|
+
if (key.length !== 1) {
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const capitalHetaCode = 880;
|
|
24
|
+
const isNonLatin = key.charCodeAt(0) >= capitalHetaCode;
|
|
25
|
+
|
|
26
|
+
if (isNonLatin) {
|
|
27
|
+
if (code.indexOf('Key') === 0 && code.length === 4) {
|
|
28
|
+
return code.charAt(3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (code.indexOf('Digit') === 0 && code.length === 6) {
|
|
32
|
+
return code.charAt(5);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return key;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const constructKeyCode = (event: KeyboardEvent) => {
|
|
40
|
+
let keyCode =
|
|
41
|
+
`${getLatinKey(event.key, event.code)}`.toLowerCase() || event.key;
|
|
42
|
+
const macOs = navigator.userAgent.includes('Mac OS');
|
|
43
|
+
|
|
44
|
+
if (keyCode === 'backspace' && macOs) {
|
|
45
|
+
keyCode = 'delete';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const prefix = [];
|
|
49
|
+
|
|
50
|
+
if ((macOs && event.metaKey) || (!macOs && event.ctrlKey)) {
|
|
51
|
+
prefix.push('mod');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (event.shiftKey) {
|
|
55
|
+
prefix.push('shift');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (prefix.length > 0) {
|
|
59
|
+
keyCode = `${prefix.join('-')}-${getLatinKey(keyCode, event.code)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return keyCode;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const CORE_HOTKEYS = new Set([
|
|
66
|
+
'escape',
|
|
67
|
+
'mod-c',
|
|
68
|
+
'mod-d',
|
|
69
|
+
'mod-z',
|
|
70
|
+
'mod-y',
|
|
71
|
+
'mod-shift-z',
|
|
72
|
+
'mod-v',
|
|
73
|
+
'delete',
|
|
74
|
+
'mod-g',
|
|
75
|
+
'mod-p',
|
|
76
|
+
'mod-s',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const getDeployLink = () => {
|
|
80
|
+
const regex = new RegExp(`(^| )seplugin-dev=([^;]+)`);
|
|
81
|
+
const match = document.cookie.match(regex);
|
|
82
|
+
const value = match?.[2].toLowerCase?.();
|
|
83
|
+
|
|
84
|
+
if (value === 'true') {
|
|
85
|
+
return LOCAL_ENDPOINT;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
value === 'stage' ||
|
|
90
|
+
value?.includes('https://') ||
|
|
91
|
+
window.location.pathname.includes('netlify')
|
|
92
|
+
) {
|
|
93
|
+
return value?.includes('https://') ? value : `${PROD_ENDPOINT}/stage`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const [major, mid] = pkg.version.split('.');
|
|
97
|
+
|
|
98
|
+
return `${PROD_ENDPOINT}/v${major}.${mid}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const verifyConfig = ({ token, pluginId, apiUrl }: PluginConfig) => {
|
|
102
|
+
if (!token) {
|
|
103
|
+
throw new Error('No [token] was provided');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!pluginId) {
|
|
107
|
+
throw new Error('No [pluginId] was provided');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!apiUrl) {
|
|
111
|
+
throw new Error('No [apiUrl] was provided');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const createIframe = () => {
|
|
116
|
+
const iframe = document.createElement('iframe');
|
|
117
|
+
iframe.src = getDeployLink();
|
|
118
|
+
iframe.setAttribute(
|
|
119
|
+
'style',
|
|
120
|
+
'height:100%;width:100%;min-width:960px;border:0px',
|
|
121
|
+
);
|
|
122
|
+
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
|
123
|
+
iframe.setAttribute(
|
|
124
|
+
'sandbox',
|
|
125
|
+
'allow-scripts allow-same-origin allow-forms allow-popups',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return iframe;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export class PluginInstance {
|
|
132
|
+
public iframe: HTMLIFrameElement | null = null;
|
|
133
|
+
private hotkeysAttached = false;
|
|
134
|
+
constructor(public config: PluginConfig) {}
|
|
135
|
+
|
|
136
|
+
private captureHotkeysEnabled() {
|
|
137
|
+
return this.config.disableHotkeysPassing !== true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private onHostHotkey = (event: KeyboardEvent) => {
|
|
141
|
+
if (!this.captureHotkeysEnabled() || !this.iframe?.contentWindow) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const keyCode = constructKeyCode(event);
|
|
146
|
+
|
|
147
|
+
if (!CORE_HOTKEYS.has(keyCode)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const target = event.target as HTMLElement | null;
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
target &&
|
|
155
|
+
(target.isContentEditable ||
|
|
156
|
+
['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName))
|
|
157
|
+
) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
|
|
163
|
+
const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
|
|
164
|
+
|
|
165
|
+
this.postEvent('hostHotkey', {
|
|
166
|
+
key,
|
|
167
|
+
code: event.code,
|
|
168
|
+
metaKey,
|
|
169
|
+
ctrlKey,
|
|
170
|
+
shiftKey,
|
|
171
|
+
altKey,
|
|
172
|
+
keyCode,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
private promisedIframeMethod<T>(eventName: string) {
|
|
177
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
178
|
+
|
|
179
|
+
const res = new Promise<T>((resolve, reject) => {
|
|
180
|
+
const onEvent = (
|
|
181
|
+
e: MessageEvent<{ source: string; event: keyof PluginConfig['on'] }>,
|
|
182
|
+
) => {
|
|
183
|
+
const {
|
|
184
|
+
data: { source, event, ...data },
|
|
185
|
+
} = e;
|
|
186
|
+
if (source === 'DraftPlugin' && event === eventName) {
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
|
|
189
|
+
if (timer) {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
timer = null;
|
|
192
|
+
}
|
|
193
|
+
resolve(data as T);
|
|
194
|
+
window.removeEventListener('message', onEvent, true);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
window.addEventListener('message', onEvent, true);
|
|
199
|
+
|
|
200
|
+
timer = setTimeout(() => {
|
|
201
|
+
window.removeEventListener('message', onEvent, true);
|
|
202
|
+
reject('Request timed out');
|
|
203
|
+
timer = null;
|
|
204
|
+
}, 2000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.postEvent(eventName);
|
|
208
|
+
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private postEvent(event: string, data: object = {}) {
|
|
213
|
+
if (this.iframe && this.iframe.contentWindow) {
|
|
214
|
+
this.iframe.contentWindow.postMessage(
|
|
215
|
+
{
|
|
216
|
+
...data,
|
|
217
|
+
event,
|
|
218
|
+
source: 'DraftPlugin',
|
|
219
|
+
},
|
|
220
|
+
'*',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private onImageUpload = async ({
|
|
226
|
+
id,
|
|
227
|
+
...data
|
|
228
|
+
}: UploadImageParams & { id: string }) => {
|
|
229
|
+
try {
|
|
230
|
+
const url = await this.config.on.uploadImage?.(data);
|
|
231
|
+
let params = {};
|
|
232
|
+
|
|
233
|
+
if (url) {
|
|
234
|
+
params = await getImageParamsFromFileUrl(url);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.postEvent('imageUploaded', { id, img: { ...params, url } });
|
|
238
|
+
} catch (e) {
|
|
239
|
+
this.postEvent('imageUploaded', { id });
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
private onLoadPersonalization = async () => {
|
|
244
|
+
try {
|
|
245
|
+
if (!this.config.on.loadPersonalization) {
|
|
246
|
+
throw new Error('No personalization loader provided');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const personalizationDictionary =
|
|
250
|
+
await this.config.on.loadPersonalization();
|
|
251
|
+
|
|
252
|
+
this.postEvent('loadPersonalization', {
|
|
253
|
+
items: personalizationDictionary,
|
|
254
|
+
});
|
|
255
|
+
} catch (e) {
|
|
256
|
+
this.postEvent('loadPersonalization', {});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
private onGalleryOpen = async ({ id }: { id: string }) => {
|
|
261
|
+
const applyImage = async (url?: string) => {
|
|
262
|
+
const imgParams = { url };
|
|
263
|
+
|
|
264
|
+
if (url) {
|
|
265
|
+
const params = await getImageParamsFromFileUrl(url);
|
|
266
|
+
Object.assign(imgParams, params);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.postEvent('imageUploaded', {
|
|
270
|
+
id,
|
|
271
|
+
img: imgParams,
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
this.config.on.openGallery?.(applyImage);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
private triggerEvent<H extends CallableFunction | undefined>(
|
|
279
|
+
handler: H,
|
|
280
|
+
params: HandlerParams<H>,
|
|
281
|
+
) {
|
|
282
|
+
if (!handler) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return handler(params);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private onMessage = (
|
|
290
|
+
ev: MessageEvent<{ source: string; event: keyof Handlers }>,
|
|
291
|
+
) => {
|
|
292
|
+
const {
|
|
293
|
+
data: { source, event, ...data },
|
|
294
|
+
} = ev;
|
|
295
|
+
|
|
296
|
+
if (source === 'DraftPlugin') {
|
|
297
|
+
if (event === 'openGallery') {
|
|
298
|
+
this.onGalleryOpen(data as OpenGalleryParams);
|
|
299
|
+
} else if (event === 'uploadImage') {
|
|
300
|
+
this.onImageUpload(data as UploadImageParams & { id: string });
|
|
301
|
+
} else if (event === 'loadPersonalization') {
|
|
302
|
+
this.onLoadPersonalization();
|
|
303
|
+
} else {
|
|
304
|
+
this.triggerEvent(
|
|
305
|
+
this.config.on[event],
|
|
306
|
+
data as HandlerParams<Handlers[typeof event]>,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
start({
|
|
313
|
+
template,
|
|
314
|
+
templateName,
|
|
315
|
+
uid,
|
|
316
|
+
}: {
|
|
317
|
+
template: string | TemplateJSON;
|
|
318
|
+
uid: string;
|
|
319
|
+
templateName?: string;
|
|
320
|
+
}) {
|
|
321
|
+
const containerEl = document.querySelector(this.config.container);
|
|
322
|
+
|
|
323
|
+
if (!containerEl) {
|
|
324
|
+
throw new Error('Specified container not found');
|
|
325
|
+
} else {
|
|
326
|
+
const onInit = ({ data: { source, event } }: OnInitParams) => {
|
|
327
|
+
if (source === 'DraftPlugin' && event === 'loaded') {
|
|
328
|
+
const {
|
|
329
|
+
on,
|
|
330
|
+
container,
|
|
331
|
+
locale,
|
|
332
|
+
autosave,
|
|
333
|
+
token,
|
|
334
|
+
apiUrl,
|
|
335
|
+
pluginId,
|
|
336
|
+
__config,
|
|
337
|
+
} = this.config;
|
|
338
|
+
|
|
339
|
+
verifyConfig(this.config);
|
|
340
|
+
|
|
341
|
+
if (this.iframe && this.iframe.contentWindow) {
|
|
342
|
+
this.postEvent('init', {
|
|
343
|
+
container,
|
|
344
|
+
locale,
|
|
345
|
+
uid,
|
|
346
|
+
autosave,
|
|
347
|
+
token,
|
|
348
|
+
pluginId,
|
|
349
|
+
apiUrl,
|
|
350
|
+
__config,
|
|
351
|
+
enabledListeners: Object.keys(on),
|
|
352
|
+
template,
|
|
353
|
+
templateName,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
window.addEventListener('message', this.onMessage);
|
|
358
|
+
window.removeEventListener('message', onInit);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
this.iframe = createIframe();
|
|
363
|
+
window.addEventListener('message', onInit);
|
|
364
|
+
containerEl.appendChild(this.iframe);
|
|
365
|
+
|
|
366
|
+
if (this.captureHotkeysEnabled() && !this.hotkeysAttached) {
|
|
367
|
+
window.addEventListener('keydown', this.onHostHotkey);
|
|
368
|
+
this.hotkeysAttached = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
destroy = () => {
|
|
374
|
+
this.iframe?.remove();
|
|
375
|
+
this.iframe = null;
|
|
376
|
+
window.removeEventListener('message', this.onMessage);
|
|
377
|
+
|
|
378
|
+
if (this.hotkeysAttached) {
|
|
379
|
+
window.removeEventListener('keydown', this.onHostHotkey);
|
|
380
|
+
this.hotkeysAttached = false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
changeTemplate(
|
|
385
|
+
template: string | TemplateJSON,
|
|
386
|
+
options: {
|
|
387
|
+
templateName?: string;
|
|
388
|
+
uid?: string;
|
|
389
|
+
} = {},
|
|
390
|
+
) {
|
|
391
|
+
this.postEvent('changeTemplate', { ...options, template });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
toggleGrid(state?: boolean) {
|
|
395
|
+
this.postEvent('toggleGrid', { state });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
togglePreview(state?: boolean) {
|
|
399
|
+
this.postEvent('togglePreview', { state });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
toggleMenu(menu: EditorMenu | null = null) {
|
|
403
|
+
this.postEvent('toggleMenu', { menu });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
toggleViewMode(viewMode: ViewMode) {
|
|
407
|
+
this.postEvent('toggleViewMode', { viewMode });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
undo() {
|
|
411
|
+
this.postEvent('undo');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
redo() {
|
|
415
|
+
this.postEvent('redo');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
save() {
|
|
419
|
+
return this.promisedIframeMethod<SaveParams>('saveAction');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
exit() {
|
|
423
|
+
return this.promisedIframeMethod<ExitParams>('exitAction');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
exportContent() {
|
|
427
|
+
return this.promisedIframeMethod<SaveParams>('exportContent');
|
|
428
|
+
}
|
|
429
|
+
}
|
package/src/routes.ts
ADDED