@codeparticle/strapi-plugin-grapejs 1.3.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.
Files changed (44) hide show
  1. package/README.md +48 -0
  2. package/admin/src/components/InputGrape/assets-view-override.js +87 -0
  3. package/admin/src/components/InputGrape/components.js +123 -0
  4. package/admin/src/components/InputGrape/edit-code-btn.js +70 -0
  5. package/admin/src/components/InputGrape/index.js +361 -0
  6. package/admin/src/components/InputGrape/storage.js +18 -0
  7. package/admin/src/components/InputGrapeNewsletter/index.js +9 -0
  8. package/admin/src/containers/App/index.js +25 -0
  9. package/admin/src/containers/HomePage/index.js +18 -0
  10. package/admin/src/index.js +52 -0
  11. package/admin/src/lifecycles.js +3 -0
  12. package/admin/src/pluginId.js +5 -0
  13. package/admin/src/translations/ar.json +1 -0
  14. package/admin/src/translations/cs.json +1 -0
  15. package/admin/src/translations/de.json +1 -0
  16. package/admin/src/translations/en.json +1 -0
  17. package/admin/src/translations/es.json +1 -0
  18. package/admin/src/translations/fr.json +1 -0
  19. package/admin/src/translations/index.js +43 -0
  20. package/admin/src/translations/it.json +1 -0
  21. package/admin/src/translations/ko.json +1 -0
  22. package/admin/src/translations/ms.json +1 -0
  23. package/admin/src/translations/nl.json +1 -0
  24. package/admin/src/translations/pl.json +1 -0
  25. package/admin/src/translations/pt-BR.json +1 -0
  26. package/admin/src/translations/pt.json +1 -0
  27. package/admin/src/translations/ru.json +1 -0
  28. package/admin/src/translations/sk.json +1 -0
  29. package/admin/src/translations/tr.json +1 -0
  30. package/admin/src/translations/vi.json +1 -0
  31. package/admin/src/translations/zh-Hans.json +1 -0
  32. package/admin/src/translations/zh.json +1 -0
  33. package/admin/src/utils/getTrad.js +5 -0
  34. package/package.json +72 -0
  35. package/pnpm-workspace.yaml +6 -0
  36. package/server/controllers/grape.js +7 -0
  37. package/server/controllers/index.js +7 -0
  38. package/server/index.js +13 -0
  39. package/server/register.js +29 -0
  40. package/server/routes/index.js +11 -0
  41. package/server/services/grape.js +1 -0
  42. package/server/services/index.js +7 -0
  43. package/strapi-admin.js +1 -0
  44. package/strapi-server.js +1 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # README #
2
+
3
+ To run this plugin it must be installed into a strapi application.
4
+
5
+ ## Usage
6
+
7
+ Please refer to https://codeparticle.atlassian.net/wiki/spaces/CP/pages/2528313373/Plugin+GrapeJS.
8
+
9
+ ## Dev
10
+
11
+ You need to create a new strapi project or just use an existing example one:
12
+ ```
13
+ pnpm run dev
14
+ ```
15
+
16
+ ## Publishing
17
+
18
+ Publishing should already be setup. Just follow these steps to publish the project:
19
+
20
+ - After code merged to `main/master`
21
+ - Checkout the `main/master` branch
22
+ - Run `pnpm version [patch|minor|major]`
23
+ - Push to remote with `git push --tags` to trigger the tag pipeline
24
+
25
+ ## Modifying GrapeJS editor when it is initialized
26
+
27
+ Create config.js file in `admin/src/config.js`, then add the default code exported from that file in strapi-admin and set the `window.onGrapeInit` function, which takes as an argument the `editor`, which is the grapeJS editor object.
28
+
29
+ Example:
30
+ ```js
31
+ // File where we can initialize front end code for GrapeJS plugin
32
+
33
+ // Refer to https://grapesjs.com/docs for editor docs
34
+ window.onGrapeInit = (editor) => {
35
+ const { BlockManager } = editor;
36
+
37
+ // 'my-first-block' is the ID of the block
38
+ BlockManager.add('my-first-block', {
39
+ label: 'Simple block',
40
+ content: '<div class="my-block">This is a simple block</div>',
41
+ });
42
+ };
43
+
44
+ // Default code of the config.js file in strapi-admin
45
+ export const LOGIN_LOGO = null;
46
+ export const SHOW_TUTORIALS = true;
47
+ export const SETTINGS_BASE_URL = '/settings';
48
+ ```
@@ -0,0 +1,87 @@
1
+ import debounce from 'lodash/debounce';
2
+
3
+ const INPUT_DEBOUNCE_TIME = 300;
4
+
5
+ export const assetsViewOverride = (editor, onSearch) => {
6
+ // Render so AssetsView() can be defined after the timeout
7
+ editor.AssetManager.render();
8
+
9
+ setTimeout(() => {
10
+ const view = editor.AssetManager.AssetsView();
11
+
12
+ // Overring the template to add a search input to the html
13
+ // eslint-disable-next-line func-names
14
+ view.template = function template({ pfx, ppfx, em }) {
15
+ let form = '';
16
+
17
+ // eslint-disable-next-line react/no-this-in-sfc
18
+ if (this.config.showUrlInput) {
19
+ form = `
20
+ <form class="${pfx}add-asset">
21
+ <div class="${ppfx}field ${pfx}add-field">
22
+ <input placeholder="${em && em.t('assetManager.inputPlh')}"/>
23
+ </div>
24
+ <button class="${ppfx}btn-prim">${em && em.t('assetManager.addButton')}</button>
25
+ <div style="clear:both"></div>
26
+ </form>
27
+ `;
28
+ }
29
+
30
+ return `
31
+ <div class="${pfx}assets-cont">
32
+ <div class="${pfx}assets-header">
33
+ ${form}
34
+ <div class="gjs-am-assets-search-container">
35
+ Search
36
+ <div class="gjs-field">
37
+ <input class="${pfx}assets-search" />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <div class="${pfx}searching" style="display: none;">Searching...</div>
42
+ <div class="${pfx}assets" data-el="assets"></div>
43
+ <div style="clear:both"></div>
44
+ </div>
45
+ `;
46
+ };
47
+
48
+ // Override render to add the oninput event listener on the search input
49
+ // eslint-disable-next-line func-names
50
+ view.render = function render() {
51
+ const fuRendered = this.options.fu.render().el;
52
+ this.$el.empty();
53
+ this.$el.append(fuRendered).append(this.template(this));
54
+ this.el.className = `${this.ppfx}asset-manager`;
55
+ this.renderAssets();
56
+ this.rendered = 1;
57
+
58
+ const searchInput = this.$el.find('.gjs-am-assets-search')[0];
59
+
60
+ if (searchInput) {
61
+ const onInput = debounce(({ target: { value } }) => {
62
+ const searchingElement = this.$el.find('.gjs-am-searching')[0];
63
+ const assetsElement = this.$el.find('.gjs-am-assets')[0];
64
+
65
+ this.searching = true;
66
+ this.searchValue = value;
67
+ searchingElement.style.display = 'block';
68
+ assetsElement.style.display = 'none';
69
+
70
+ const onFinishSearch = () => {
71
+ this.searching = false;
72
+ searchingElement.style.display = 'none';
73
+ assetsElement.style.display = 'flex';
74
+ };
75
+
76
+ onSearch(value).then(onFinishSearch).catch(onFinishSearch);
77
+ }, INPUT_DEBOUNCE_TIME);
78
+
79
+ searchInput.oninput = onInput;
80
+ }
81
+
82
+ return this;
83
+ };
84
+
85
+ view.render();
86
+ });
87
+ };
@@ -0,0 +1,123 @@
1
+ import styled from 'styled-components';
2
+
3
+ const Wrapper = styled.div`
4
+ margin-bottom: 23px;
5
+
6
+ & > label {
7
+ color: ${({ theme }) => theme.colors.neutral1000};
8
+ display: block;
9
+ font-size: 0.75rem;
10
+ font-weight: 600;
11
+ line-height: 1.33;
12
+ margin-bottom: 4px;
13
+ }
14
+
15
+ .gjs-cv-canvas {
16
+ width: 75%;
17
+ }
18
+
19
+ .gjs {
20
+ border: 1px solid #E3E9F3;
21
+ border-radius: 2px;
22
+ height: 300px;
23
+
24
+ &.fullscreen {
25
+ bottom: 0;
26
+ height: auto !important;
27
+ left: 0;
28
+ position: fixed;
29
+ right: 0;
30
+ top: 0;
31
+ width: auto !important;
32
+ z-index: 999999;
33
+ }
34
+
35
+ .gjs-am-add-asset .gjs-btn-prim {
36
+ font-size: 0.9rem;
37
+ }
38
+
39
+ .gjs-pn-options {
40
+ right: 25%;
41
+ }
42
+
43
+ .gjs-pn-views {
44
+ width: 25%;
45
+ }
46
+
47
+ .gjs-pn-views-container {
48
+ height: calc(100% - 42px);
49
+ margin-top: 42px;
50
+ padding-top: 0;
51
+ width: 25%;
52
+ }
53
+
54
+ .gjs-pn-btn {
55
+ font-size: 18px !important;
56
+ }
57
+
58
+ .fa {
59
+ font-family: FontAwesome !important;
60
+ }
61
+
62
+ .gjs-pn-btn {
63
+ align-items: center;
64
+ display: flex;
65
+ justify-content: center;
66
+ }
67
+
68
+ .gjs-layer-move {
69
+ padding: 5px 7px 5px 5px;
70
+ }
71
+
72
+ .gjs-layer-caret {
73
+ padding: 0;
74
+ }
75
+
76
+ .gjs-layer-vis {
77
+ padding-top: 9px;
78
+ }
79
+
80
+ .gjs-block {
81
+ display: flex;
82
+ font-size: 1em;
83
+ flex-direction: column;
84
+ padding: 14px !important;
85
+
86
+ &::before {
87
+ flex: 1;
88
+ font-size: 3em !important;
89
+ }
90
+
91
+ &.fa::before {
92
+ align-items: center;
93
+ display: flex;
94
+ font: normal normal normal 2.5em FontAwesome !important;
95
+ justify-content: center;
96
+ margin-bottom: 10px;
97
+ }
98
+ }
99
+
100
+ .sp-container {
101
+ position: fixed !important;
102
+ }
103
+
104
+ .gjs-am-assets {
105
+ height: 230px !important;
106
+ }
107
+
108
+ .gjs-am-assets-search-container {
109
+ align-items: center;
110
+ display: flex;
111
+ margin-top: 10px;
112
+
113
+ .gjs-field {
114
+ flex: 1;
115
+ margin-left: 10px;
116
+ }
117
+ }
118
+ }
119
+ `;
120
+
121
+ export {
122
+ Wrapper,
123
+ };
@@ -0,0 +1,70 @@
1
+ export const addEditCodeBtn = (editor) => {
2
+ const pfx = editor.getConfig().stylePrefix;
3
+ const modal = editor.Modal;
4
+ const cmdm = editor.Commands;
5
+ const codeViewer = editor.CodeManager.getViewer('CodeMirror').clone();
6
+ const pnm = editor.Panels;
7
+ const container = document.createElement('div');
8
+ const btnEdit = document.createElement('button');
9
+
10
+ codeViewer.set({
11
+ codeName: 'htmlmixed',
12
+ readOnly: 0,
13
+ theme: 'hopscotch',
14
+ autoBeautify: true,
15
+ autoCloseTags: true,
16
+ autoCloseBrackets: true,
17
+ lineWrapping: true,
18
+ styleActiveLine: true,
19
+ smartIndent: true,
20
+ indentWithTabs: true,
21
+ });
22
+
23
+ btnEdit.innerHTML = 'Edit';
24
+ btnEdit.className = `${pfx}btn-prim ${pfx}btn-import`;
25
+ btnEdit.onclick = (e) => {
26
+ e.preventDefault();
27
+ const code = codeViewer.editor.getValue();
28
+ editor.DomComponents.getWrapper().set('content', '');
29
+ editor.setComponents(code.trim());
30
+ modal.close();
31
+ };
32
+
33
+ cmdm.add('html-edit', {
34
+ run: (e, sender) => {
35
+ if (sender) {
36
+ sender.set('active', 0);
37
+ }
38
+
39
+ let viewer = codeViewer.editor;
40
+ modal.setTitle('Edit code');
41
+
42
+ if (!viewer) {
43
+ const txtarea = document.createElement('textarea');
44
+ container.appendChild(txtarea);
45
+ container.appendChild(btnEdit);
46
+ codeViewer.init(txtarea);
47
+ viewer = codeViewer.editor;
48
+ }
49
+
50
+ const InnerHtml = editor.getHtml();
51
+ const Css = editor.getCss();
52
+ modal.setContent('');
53
+ modal.setContent(container);
54
+ codeViewer.setContent(`${InnerHtml}\n<style>\n${Css}\n</style>`);
55
+ modal.open();
56
+ viewer.refresh();
57
+ },
58
+ });
59
+
60
+ pnm.addButton('options', [
61
+ {
62
+ id: 'edit',
63
+ className: 'fa fa-edit',
64
+ command: 'html-edit',
65
+ attributes: {
66
+ title: 'Edit',
67
+ },
68
+ },
69
+ ]);
70
+ };
@@ -0,0 +1,361 @@
1
+ import grapesjs from 'grapesjs';
2
+ import 'grapesjs/dist/css/grapes.min.css';
3
+ import defaultPlugin from 'grapesjs-preset-webpage';
4
+ import blocksPlugin from 'grapesjs-blocks-basic';
5
+ import formsPlugin from 'grapesjs-plugin-forms';
6
+ import React, {
7
+ useCallback,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import { useIntl } from 'react-intl';
13
+ import { request, useCMEditViewDataManager } from '@strapi/helper-plugin';
14
+ import { useStableUniqueId } from 'react-stable-uniqueid';
15
+ import eotFont from 'grapesjs/dist/fonts/main-fonts.eot';
16
+ import woffFont from 'grapesjs/dist/fonts/main-fonts.woff';
17
+ import svgFont from 'grapesjs/dist/fonts/main-fonts.svg';
18
+ import ttfFont from 'grapesjs/dist/fonts/main-fonts.ttf';
19
+ import { Wrapper } from './components';
20
+ import { addEditCodeBtn } from './edit-code-btn';
21
+ import { assetsViewOverride } from './assets-view-override';
22
+
23
+ // Load grapejs font face (It usually loads from the grapejs.min.css, but with latest strapi it isnt working anymore)
24
+ const style = document.createElement('style');
25
+ style.innerHTML = `
26
+ @font-face {
27
+ font-family: 'font3336';
28
+ src: url('${eotFont}.eot?v=20');
29
+ src:
30
+ url('${woffFont}?v=20') format('woff'),
31
+ url('${ttfFont}.ttf?v=20') format('truetype'),
32
+ url('${svgFont}.svg?v=20') format('svg'),
33
+ url('${eotFont}.eot?v=20') format('embedded-opentype');
34
+ font-weight: normal;
35
+ font-style: normal;
36
+ }
37
+ `;
38
+ document.head.appendChild(style);
39
+
40
+ const getFullUrl = path => (/^http/.test(path) ? path : `${strapi.backendURL}${path}`);
41
+
42
+ let imageFetchListeners = [];
43
+ let globalImagesFetching = false;
44
+
45
+ const textCleanCanvas = 'Are you sure to clean the canvas?';
46
+
47
+ const getSaveData = (value, valueData) => {
48
+ // Parse initial value
49
+ const parsedValue = value || '';
50
+ // Remove <div>, </div>, and get all content before <style>
51
+ let html = parsedValue.replace('<div>', '');
52
+ const lastIndexOfStyle = parsedValue.lastIndexOf('<style>');
53
+ html = html.slice(0, lastIndexOfStyle);
54
+ // Get all content after <style> and remove </style>
55
+ const css = parsedValue.slice(lastIndexOfStyle).replace('</div>', '').replace('<style>', '').replace('</style>', '');
56
+
57
+ const isValueDataString = typeof valueData === 'string';
58
+ let savedData = isValueDataString ? {} : (valueData || {});
59
+
60
+ if (isValueDataString) {
61
+ try {
62
+ savedData = JSON.parse(valueData || '{}');
63
+ } catch (err) {
64
+ // ignore error
65
+ }
66
+ }
67
+
68
+ return {
69
+ components: savedData?.components || html.trim(),
70
+ style: savedData?.style || css.trim(),
71
+ };
72
+ };
73
+
74
+ const InputGrapeJs = ({
75
+ name,
76
+ label,
77
+ intlLabel,
78
+ onChange,
79
+ plugin = defaultPlugin,
80
+ }) => {
81
+ const {
82
+ modifiedData: {
83
+ [`${name}_data`]: valueData,
84
+ value,
85
+ },
86
+ } = useCMEditViewDataManager();
87
+
88
+ const { formatMessage } = useIntl();
89
+
90
+ if (window.FontAwesome) {
91
+ // Fixes for FontAwesome icons
92
+ window.FontAwesome.config.autoReplaceSvg = 'nest';
93
+ window.FontAwesome.noAuto();
94
+ }
95
+
96
+ const baseUrl = `${strapi.backendURL}/upload`;
97
+ const getUrl = `${baseUrl}/files`;
98
+
99
+ const mounted = useRef(true);
100
+ const [editor, setEditor] = useState();
101
+ const containerId = useStableUniqueId('gjs-');
102
+ const lastSearchValue = useRef('');
103
+ const editorRef = useRef(editor);
104
+ editorRef.current = editor;
105
+ const valueRef = useRef(value);
106
+ valueRef.current = value;
107
+
108
+ useEffect(() => {
109
+ if (editorRef.current) {
110
+ const editorData = JSON.stringify({
111
+ components: editorRef.current.getComponents(),
112
+ style: editorRef.current.getStyle(),
113
+ });
114
+
115
+ if (editorData !== valueData) {
116
+ const savedData = getSaveData(value, valueData);
117
+
118
+ editorRef.current.setComponents(savedData.components);
119
+ editorRef.current.setStyle(savedData.style);
120
+ }
121
+ }
122
+ }, [value, valueData]);
123
+
124
+ useEffect(() => {
125
+ const savedData = getSaveData(value, valueData);
126
+
127
+ // Initialize grapeJS
128
+ const newEditor = grapesjs.init({
129
+ canvas: {
130
+ styles: window.getGrapeJSStyles ? window.getGrapeJSStyles() : [],
131
+ },
132
+
133
+ height: '300px',
134
+ container: `#${containerId}`,
135
+
136
+ components: savedData.components,
137
+ style: savedData.style,
138
+ styleManager: {},
139
+
140
+ plugins: [plugin, blocksPlugin, formsPlugin, ...(window.getGrapeJSPlugins ? window.getGrapeJSPlugins() : [])],
141
+ pluginsOpts: {
142
+ [plugin]: {},
143
+ [blocksPlugin]: {},
144
+ [formsPlugin]: {},
145
+ },
146
+
147
+ // Disable storage manager so edits dont persist for all input types
148
+ storageManager: { type: null },
149
+
150
+ assetManager: {
151
+ // custom: true,
152
+ // Custom upload method so we save images on Media Library from strapi
153
+ upload: baseUrl,
154
+ uploadName: 'files',
155
+ uploadFile: (e) => {
156
+ const files = Array.from(e.dataTransfer ? e.dataTransfer.files : e.target.files);
157
+
158
+ return Promise.all(files.map(async (file) => {
159
+ const formData = new FormData();
160
+
161
+ formData.append('files', file);
162
+
163
+ const response = await request(
164
+ '/upload',
165
+ {
166
+ body: formData,
167
+ method: 'POST',
168
+ headers: {},
169
+ },
170
+ false,
171
+ false,
172
+ );
173
+
174
+ // Add image to editor after upload
175
+ response.forEach(({ mime, name: imgName, url }) => {
176
+ if (`${mime}${imgName}${url}`.includes(lastSearchValue.current.toLowerCase())) {
177
+ newEditor.AssetManager.add(getFullUrl(url));
178
+ }
179
+ });
180
+ }));
181
+ },
182
+ },
183
+ });
184
+
185
+ const assetManager = newEditor.AssetManager;
186
+ const commands = newEditor.Commands;
187
+ const pnm = newEditor.Panels;
188
+ const optionsPanel = pnm.getPanel('options');
189
+ const cmdClear = 'canvas-clear';
190
+
191
+ // Remove default clear button because newsletter plugin doesn't add one, only webpage plugin does
192
+ optionsPanel.buttons.remove(cmdClear);
193
+
194
+ optionsPanel.buttons.add({
195
+ id: cmdClear,
196
+ className: 'fa fa-trash',
197
+ command: e => e.runCommand(cmdClear),
198
+ });
199
+
200
+ // eslint-disable-next-line no-restricted-globals, no-alert
201
+ commands.add(cmdClear, e => confirm(textCleanCanvas) && e.runCommand('core:canvas-clear'));
202
+
203
+ // Override fullscreen command (original one makes whole browser full screen)
204
+ commands.add('fullscreen', {
205
+ run: () => {
206
+ newEditor.getContainer().classList.add('fullscreen');
207
+ newEditor.refresh();
208
+ },
209
+ stop: () => {
210
+ newEditor.getContainer().classList.remove('fullscreen');
211
+ newEditor.refresh();
212
+ },
213
+ });
214
+
215
+ const addFetchedImages = (images) => {
216
+ if (mounted.current) {
217
+ const existingModels = assetManager.getAll().models.slice();
218
+
219
+ existingModels.forEach(({ id }) => assetManager.remove(id));
220
+
221
+ // Map images and make sure there are no duplicates
222
+ images.reverse().forEach(({
223
+ created_at: createdAt,
224
+ height,
225
+ id,
226
+ mime,
227
+ name: imageName,
228
+ url,
229
+ width,
230
+ }) => {
231
+ assetManager.add({
232
+ createdAt,
233
+ height,
234
+ id,
235
+ mime,
236
+ name: imageName,
237
+ src: getFullUrl(url),
238
+ width,
239
+ });
240
+ });
241
+ }
242
+ };
243
+
244
+ const onSearch = (q = '') => new Promise((resolve, reject) => {
245
+ lastSearchValue.current = q;
246
+
247
+ request(getUrl, {
248
+ params: {
249
+ pageSize: 50,
250
+ _sort: 'updated_at:DESC',
251
+ 'filters[$and][0][name][$contains]': q,
252
+ },
253
+ }).then(({ results: images }) => {
254
+ globalImagesFetching = false;
255
+ addFetchedImages(images);
256
+
257
+ imageFetchListeners.forEach(listener => listener(images, q));
258
+ imageFetchListeners = [];
259
+
260
+ resolve(images);
261
+ }).catch((err) => {
262
+ console.log('Error fetching images', err);
263
+ globalImagesFetching = false;
264
+
265
+ reject(err);
266
+ });
267
+ });
268
+
269
+ // Fetch images from Media Library
270
+ if (!globalImagesFetching) {
271
+ globalImagesFetching = true;
272
+ onSearch('');
273
+ } else {
274
+ // If images already fetching, just listen for callback
275
+ imageFetchListeners.push(addFetchedImages);
276
+ }
277
+
278
+ addEditCodeBtn(newEditor);
279
+ assetsViewOverride(newEditor, onSearch);
280
+ setEditor(newEditor);
281
+
282
+ if (window.onGrapeInit) {
283
+ window.onGrapeInit(newEditor);
284
+ }
285
+
286
+ return () => {
287
+ mounted.current = false;
288
+ };
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ }, []);
291
+
292
+ // Update strapi value when template is updated
293
+ useEffect(() => {
294
+ if (!editor) {
295
+ return () => {};
296
+ }
297
+
298
+ const onUpdate = () => {
299
+ const html = editor.getHtml();
300
+ const css = editor.getCss({ avoidProtected: true });
301
+ let newValue = '';
302
+
303
+ if (html.length || css.length) {
304
+ newValue = `\
305
+ <div>
306
+ ${html.replace(/ draggable="true"/g, '')/* Remove draggable attribute from text elements */}
307
+ <style>
308
+ ${css}
309
+ </style>
310
+ </div>\
311
+ `;
312
+ }
313
+
314
+ if (valueRef.current === newValue) {
315
+ return;
316
+ }
317
+
318
+ onChange({
319
+ target: {
320
+ name,
321
+ value: newValue,
322
+ },
323
+ });
324
+
325
+ const editorData = JSON.stringify({
326
+ components: editor.getComponents(),
327
+ style: editor.getStyle(),
328
+ });
329
+
330
+ onChange({
331
+ target: {
332
+ name: `${name}_data`,
333
+ value: editorData,
334
+ },
335
+ });
336
+ };
337
+
338
+ editor.on('update', onUpdate);
339
+
340
+ return () => editor.off('update', onUpdate);
341
+ }, [editor, name, onChange]);
342
+
343
+ // Don't submit the form when editing inputs inside the GrapeJS editor
344
+ const preventFormSubmission = useCallback((e) => {
345
+ if (e.which === 13) {
346
+ e.preventDefault();
347
+ }
348
+ }, []);
349
+
350
+ return (
351
+ <Wrapper onKeyDown={preventFormSubmission}>
352
+ {/* eslint-disable-next-line jsx-a11y/label-has-for */}
353
+ <label htmlFor={name}>
354
+ {intlLabel ? formatMessage(intlLabel) : label}
355
+ </label>
356
+ <div className="gjs" id={containerId} />
357
+ </Wrapper>
358
+ );
359
+ };
360
+
361
+ export default InputGrapeJs;
@@ -0,0 +1,18 @@
1
+ // Cache config for Media
2
+ const MEDIA_CACHE_KEY = 'gjs-media';
3
+
4
+ export const storeMedia = (images) => {
5
+ localStorage.setItem(MEDIA_CACHE_KEY, JSON.stringify(images));
6
+ };
7
+
8
+ export const loadMedia = () => {
9
+ let images = [];
10
+
11
+ try {
12
+ images = JSON.parse(localStorage.getItem(MEDIA_CACHE_KEY) || '[]');
13
+ } catch (err) {
14
+ // Nothing
15
+ }
16
+
17
+ return images;
18
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import defaultPlugin from 'grapesjs-preset-newsletter';
3
+ import InputGrape from '../InputGrape';
4
+
5
+ const InputGrapeNewsletter = (props) => (
6
+ <InputGrape {...props} plugin={defaultPlugin} />
7
+ );
8
+
9
+ export default InputGrapeNewsletter;
@@ -0,0 +1,25 @@
1
+ /**
2
+ *
3
+ * This component is the skeleton around the actual pages, and should only
4
+ * contain code that should be seen on all pages. (e.g. navigation bar)
5
+ *
6
+ */
7
+
8
+ import React from 'react';
9
+ import { Switch, Route } from 'react-router-dom';
10
+ import { NotFound } from '@strapi/helper-plugin';
11
+ // Utils
12
+ import pluginId from '../../pluginId';
13
+ // Containers
14
+ import HomePage from '../HomePage';
15
+
16
+ const App = () => (
17
+ <div>
18
+ <Switch>
19
+ <Route path={`/plugins/${pluginId}`} component={HomePage} exact />
20
+ <Route component={NotFound} />
21
+ </Switch>
22
+ </div>
23
+ );
24
+
25
+ export default App;
@@ -0,0 +1,18 @@
1
+ /*
2
+ *
3
+ * HomePage
4
+ *
5
+ */
6
+
7
+ import React, { memo } from 'react';
8
+ // import PropTypes from 'prop-types';
9
+ import pluginId from '../../pluginId';
10
+
11
+ const HomePage = () => (
12
+ <div>
13
+ <h1>{pluginId}&apos;s HomePage</h1>
14
+ <p>Happy coding</p>
15
+ </div>
16
+ );
17
+
18
+ export default memo(HomePage);
@@ -0,0 +1,52 @@
1
+ import { prefixPluginTranslations } from '@strapi/helper-plugin';
2
+ import pluginPkg from '../../package.json';
3
+ import pluginId from './pluginId';
4
+ import trads from './translations';
5
+ import InputGrape from './components/InputGrape';
6
+ import InputGrapeNewsletter from './components/InputGrapeNewsletter';
7
+
8
+ const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
9
+ const name = pluginPkg.strapi.name;
10
+ const icon = pluginPkg.strapi.icon;
11
+
12
+ export default {
13
+ register(app) {
14
+ const plugin = {
15
+ description: pluginDescription,
16
+ icon,
17
+ name,
18
+ id: pluginId,
19
+ trads,
20
+ };
21
+
22
+ InputGrape.baseType = 'richtext';
23
+ InputGrapeNewsletter.baseType = 'richtext';
24
+ app.addComponents({ name: 'PluginGrapeInput', Component: InputGrape });
25
+ app.addComponents({ name: 'PluginGrapeNewsletterInput', Component: InputGrapeNewsletter });
26
+
27
+ app.registerPlugin(plugin);
28
+ },
29
+ async registerTrads({ locales }) {
30
+ const importedTrads = await Promise.all(
31
+ locales.map((locale) => {
32
+ return import(
33
+ /* webpackChunkName: "grapejs-trads" */ `./translations/${locale}.json`
34
+ )
35
+ .then(({ default: data }) => {
36
+ return {
37
+ data: prefixPluginTranslations(data, pluginId),
38
+ locale,
39
+ };
40
+ })
41
+ .catch(() => {
42
+ return {
43
+ data: {},
44
+ locale,
45
+ };
46
+ });
47
+ })
48
+ );
49
+
50
+ return Promise.resolve(importedTrads);
51
+ },
52
+ };
@@ -0,0 +1,3 @@
1
+ function lifecycles() {}
2
+
3
+ export default lifecycles;
@@ -0,0 +1,5 @@
1
+ const pluginPkg = require('../../package.json');
2
+
3
+ const pluginId = pluginPkg.name.replace(/^strapi-plugin-/i, '');
4
+
5
+ module.exports = pluginId;
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,43 @@
1
+ import ar from './ar.json';
2
+ import cs from './cs.json';
3
+ import de from './de.json';
4
+ import en from './en.json';
5
+ import es from './es.json';
6
+ import fr from './fr.json';
7
+ import it from './it.json';
8
+ import ko from './ko.json';
9
+ import ms from './ms.json';
10
+ import nl from './nl.json';
11
+ import pl from './pl.json';
12
+ import ptBR from './pt-BR.json';
13
+ import pt from './pt.json';
14
+ import ru from './ru.json';
15
+ import tr from './tr.json';
16
+ import vi from './vi.json';
17
+ import zhHans from './zh-Hans.json';
18
+ import zh from './zh.json';
19
+ import sk from './sk.json';
20
+
21
+ const trads = {
22
+ ar,
23
+ cs,
24
+ de,
25
+ en,
26
+ es,
27
+ fr,
28
+ it,
29
+ ko,
30
+ ms,
31
+ nl,
32
+ pl,
33
+ 'pt-BR': ptBR,
34
+ pt,
35
+ ru,
36
+ tr,
37
+ vi,
38
+ 'zh-Hans': zhHans,
39
+ zh,
40
+ sk,
41
+ };
42
+
43
+ export default trads;
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,5 @@
1
+ import pluginId from '../pluginId';
2
+
3
+ const getTrad = id => `${pluginId}.${id}`;
4
+
5
+ export default getTrad;
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@codeparticle/strapi-plugin-grapejs",
3
+ "version": "1.3.0",
4
+ "description": "Integrated GrapeJS as a content editor",
5
+ "strapi": {
6
+ "name": "grapejs",
7
+ "icon": "plug",
8
+ "description": "Integrated GrapeJS as a content editor",
9
+ "kind": "plugin"
10
+ },
11
+ "author": {
12
+ "name": "CodeParticle",
13
+ "email": "",
14
+ "url": ""
15
+ },
16
+ "maintainers": [
17
+ {
18
+ "name": "CodeParticle",
19
+ "email": "",
20
+ "url": ""
21
+ }
22
+ ],
23
+ "engines": {
24
+ "node": "22.x",
25
+ "pnpm": "10.x"
26
+ },
27
+ "license": "MIT",
28
+ "files": [
29
+ "README.md",
30
+ "admin",
31
+ "package.json",
32
+ "pnpm-workspace.yaml",
33
+ "server",
34
+ "strapi-admin.js",
35
+ "strapi-server.js"
36
+ ],
37
+ "dependencies": {
38
+ "@strapi/helper-plugin": "4.9.2",
39
+ "@strapi/strapi": "4.9.2",
40
+ "grapesjs": "^0.19.4",
41
+ "grapesjs-blocks-basic": "^1.0.2",
42
+ "grapesjs-plugin-forms": "^2.0.6",
43
+ "grapesjs-preset-newsletter": "^1.0.2",
44
+ "grapesjs-preset-webpage": "^1.0.3",
45
+ "lodash": "4.17.19",
46
+ "react": "^17.0.2",
47
+ "react-router-dom": "^5.0.0",
48
+ "react-stable-uniqueid": "^1.2.2",
49
+ "styled-components": "^5.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@commitlint/config-conventional": "^7.6.0",
53
+ "babel-eslint": "^10.1.0",
54
+ "commitlint": "^7.6.1",
55
+ "eslint": "7.32.0",
56
+ "eslint-config-airbnb": "18.2.1",
57
+ "eslint-config-airbnb-base": "14.2.1",
58
+ "eslint-config-prettier": "6.15.0",
59
+ "eslint-plugin-import": "2.26.0",
60
+ "eslint-plugin-jsx-a11y": "6.5.1",
61
+ "eslint-plugin-node": "11.1.0",
62
+ "eslint-plugin-react": "7.29.4",
63
+ "eslint-plugin-react-hooks": "4.5.0"
64
+ },
65
+ "scripts": {
66
+ "dev": "pnpm run link-plugin && cd example-app && pnpm install && pnpm run develop --watch-admin",
67
+ "link-plugin": "mkdir -p ./example-app/src/plugins && if [ ! -d \"$(pwd)/example-app/src/plugins/grapejs\" ]; then ln -s $(pwd) $(pwd)/example-app/src/plugins/grapejs; fi",
68
+ "lint": "eslint --ignore-path .gitignore --ignore-path .eslintignore .",
69
+ "lint-example-app": "cd example-app && pnpm run lint",
70
+ "postversion": "git push && git push --tags"
71
+ }
72
+ }
@@ -0,0 +1,6 @@
1
+ onlyBuiltDependencies:
2
+ - '@strapi/strapi'
3
+ - core-js
4
+ - core-js-pure
5
+ - esbuild
6
+ - sharp
@@ -0,0 +1,7 @@
1
+ module.exports = () => ({
2
+ index: function index(ctx) {
3
+ ctx.send({
4
+ message: 'ok',
5
+ });
6
+ },
7
+ });
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const grape = require('./grape');
4
+
5
+ module.exports = {
6
+ grape
7
+ };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const services = require('./services');
4
+ const routes = require('./routes');
5
+ const controllers = require('./controllers');
6
+ const register = require('./register');
7
+
8
+ module.exports = {
9
+ controllers,
10
+ routes,
11
+ services,
12
+ register
13
+ };
@@ -0,0 +1,29 @@
1
+ const _ = require('lodash');
2
+
3
+ module.exports = ({ strapi }) => {
4
+ addDataAttribute(strapi);
5
+ };
6
+
7
+ const addDataAttribute = (strapi) => {
8
+ Object.values(strapi.contentTypes).forEach(({ attributes, uid }) => {
9
+ Object.entries(attributes).forEach(([key, entry]) => {
10
+ if (entry.customInput === 'PluginGrapeInput' || entry.customInput === 'PluginGrapeNewsletterInput') {
11
+ const attrKey = `${key}_data`;
12
+
13
+ if (attributes[attrKey]) {
14
+ console.warn(`Grapejs plugin could not initialize data attribute on model ${uid} because there is already an attribute with key ${attrKey}`);
15
+ } else {
16
+ _.set(attributes, attrKey, {
17
+ type: 'json',
18
+ writable: true,
19
+ configurable: false,
20
+ visible: false,
21
+ ignoreInFindMany: true,
22
+ ignoreInUserApi: true,
23
+ ignoreInLocalizationsPopulate: true,
24
+ });
25
+ }
26
+ }
27
+ });
28
+ });
29
+ };
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ 'content-api': {
3
+ type: 'content-api',
4
+ routes: [{
5
+ method: 'GET',
6
+ path: '/',
7
+ handler: 'grape.index',
8
+ config: { policies: [] }
9
+ }],
10
+ },
11
+ };
@@ -0,0 +1 @@
1
+ module.exports = () => ({});
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const grape = require('./grape');
4
+
5
+ module.exports = {
6
+ grape
7
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./admin/src').default;
@@ -0,0 +1 @@
1
+ module.exports = require('./server');