@silexlabs/silex 3.0.0-alpha.21

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 (106) hide show
  1. package/.dockerignore +5 -0
  2. package/.editorconfig +14 -0
  3. package/.eslintrc.json +43 -0
  4. package/.github/FUNDING.yml +13 -0
  5. package/.github/workflows/docker-hub-publish.yml +36 -0
  6. package/.github/workflows/npm-publish.yml +19 -0
  7. package/.github/workflows/test.yml +26 -0
  8. package/.nvmrc +1 -0
  9. package/.silex.js +19 -0
  10. package/CODE_OF_CONDUCT.md +128 -0
  11. package/CONTRIBUTING.md +108 -0
  12. package/Dockerfile +15 -0
  13. package/LICENSE +203 -0
  14. package/app.json +106 -0
  15. package/directus.silex.js +7 -0
  16. package/dist/client/css/admin.css +4350 -0
  17. package/dist/client/index.html +9 -0
  18. package/dist/client/js/main.js +282 -0
  19. package/dist/client/js/main.js.LICENSE.txt +52 -0
  20. package/dist/server/config.js +50 -0
  21. package/dist/server/events.js +5 -0
  22. package/dist/server/index.js +29 -0
  23. package/dist/server/start.js +18 -0
  24. package/jest.config.js +10 -0
  25. package/logos/logo_by_ceubri/silex-icon-2018@200px.png +0 -0
  26. package/logos/logo_by_ceubri/silex-icon-2018@4x.png +0 -0
  27. package/logos/logo_by_ceubri/silex-logo-2018-white-transp.pdf +0 -0
  28. package/logos/logo_by_ceubri/silex-logo-2018-white.jpg +0 -0
  29. package/logos/logo_by_ceubri/silex-logo-2018-white.pdf +0 -0
  30. package/logos/logo_by_ceubri/silex-logo-2018-white.png +0 -0
  31. package/logos/logo_by_ceubri/silex-logo-2018-white@0.5x.png +0 -0
  32. package/logos/logo_by_ceubri/silex-logo-2018-white@0.75x.png +0 -0
  33. package/logos/logo_by_ceubri/silex-logo-2018-white@4x.jpg +0 -0
  34. package/logos/logo_by_ceubri/silex-logo-2018-white@4x.png +0 -0
  35. package/logos/logo_by_ceubri/silex-logo-2018.ai +2520 -6
  36. package/logos/logo_by_ceubri/silex-logo-2018.jpg +0 -0
  37. package/logos/logo_by_ceubri/silex-logo-2018.pdf +0 -0
  38. package/logos/logo_by_ceubri/silex-logo-2018.png +0 -0
  39. package/logos/logo_by_ceubri/silex-logo-2018@0.5x.png +0 -0
  40. package/logos/logo_by_ceubri/silex-logo-2018@0.75x.png +0 -0
  41. package/logos/logo_by_ceubri/silex-logo-2018@4x.jpg +0 -0
  42. package/logos/logo_by_ceubri/silex-logo-2018@4x.png +0 -0
  43. package/logos/silexlabs.png +0 -0
  44. package/package.json +170 -0
  45. package/public/assets/container.png +0 -0
  46. package/public/assets/favicon.png +0 -0
  47. package/public/assets/logo-silex-small.png +0 -0
  48. package/public/example-config.js +31 -0
  49. package/public/robots.txt +2 -0
  50. package/readme.md +51 -0
  51. package/src/css/_mixins.scss +8 -0
  52. package/src/css/_variables.scss +33 -0
  53. package/src/css/dialog.scss +34 -0
  54. package/src/css/grapesjs.scss +44 -0
  55. package/src/css/index.scss +22 -0
  56. package/src/css/loader.scss +10 -0
  57. package/src/css/page-panel.scss +65 -0
  58. package/src/css/project-bar.scss +102 -0
  59. package/src/css/silex.scss +183 -0
  60. package/src/html/debug.pug +8 -0
  61. package/src/html/index.pug +39 -0
  62. package/src/html/templates/includes.pug +2 -0
  63. package/src/html/templates/workspace.pug +2 -0
  64. package/src/plugins/AuthPlugin.js +21 -0
  65. package/src/plugins/Cache.js +11 -0
  66. package/src/plugins/DefaultBackend.js +113 -0
  67. package/src/plugins/ExpressPlugin.js +30 -0
  68. package/src/plugins/HooksPlugin.js +58 -0
  69. package/src/plugins/PublishPlugin.js +69 -0
  70. package/src/plugins/SslPlugin.js +66 -0
  71. package/src/plugins/StaticPlugin.js +48 -0
  72. package/src/plugins/WebsitePlugin.js +149 -0
  73. package/src/ts/client/config.ts +21 -0
  74. package/src/ts/client/expose.ts +4 -0
  75. package/src/ts/client/grapesjs/blocks.ts +88 -0
  76. package/src/ts/client/grapesjs/eleventy.ts +21 -0
  77. package/src/ts/client/grapesjs/index.ts +255 -0
  78. package/src/ts/client/grapesjs/internal-links.test.ts +87 -0
  79. package/src/ts/client/grapesjs/internal-links.ts +210 -0
  80. package/src/ts/client/grapesjs/new-page-dialog.ts +73 -0
  81. package/src/ts/client/grapesjs/openImport.ts +59 -0
  82. package/src/ts/client/grapesjs/page-panel.ts +101 -0
  83. package/src/ts/client/grapesjs/project-bar.ts +92 -0
  84. package/src/ts/client/grapesjs/publish.ts +386 -0
  85. package/src/ts/client/grapesjs/rich-text.ts +16 -0
  86. package/src/ts/client/grapesjs/semantic.ts +96 -0
  87. package/src/ts/client/grapesjs/settings.ts +190 -0
  88. package/src/ts/client/grapesjs/symbolDialogs.ts +21 -0
  89. package/src/ts/client/grapesjs/template.ts +166 -0
  90. package/src/ts/client/index.ts +38 -0
  91. package/src/ts/client/utils.ts +9 -0
  92. package/src/ts/externs.ts +1 -0
  93. package/src/ts/page.ts +14 -0
  94. package/src/ts/server/cli.ts +23 -0
  95. package/src/ts/server/config.ts +62 -0
  96. package/src/ts/server/events.ts +2 -0
  97. package/src/ts/server/index.ts +35 -0
  98. package/src/ts/server/start.ts +18 -0
  99. package/src/ts/server/utils/BackwardCompat.ts +534 -0
  100. package/src/ts/types.ts +138 -0
  101. package/tsconfig-server-debug.json +20 -0
  102. package/tsconfig-server-release.json +19 -0
  103. package/tsconfig.jest.json +9 -0
  104. package/tsconfig.json +27 -0
  105. package/webpack.config.js +29 -0
  106. package/webpack.debug.config.js +8 -0
@@ -0,0 +1,149 @@
1
+ const { Router } = require('express')
2
+ const { resolve } = require('path')
3
+ const formidable = require('formidable')
4
+
5
+ const { noCache } = require('./Cache')
6
+
7
+ const EVENT_READ_START = 'EVENT_READ_START'
8
+ const EVENT_READ_END = 'EVENT_READ_END'
9
+ const EVENT_WRITE_START = 'EVENT_WRITE_START'
10
+ const EVENT_WRITE_END = 'EVENT_WRITE_END'
11
+ const EVENT_ASSET_READ_START = 'EVENT_ASSET_READ_START'
12
+ const EVENT_ASSET_READ_END = 'EVENT_ASSET_READ_END'
13
+ const EVENT_ASSET_WRITE_START = 'EVENT_ASSET_WRITE_START'
14
+ const EVENT_ASSET_WRITE_END = 'EVENT_ASSET_WRITE_END'
15
+
16
+ module.exports = async function(config, opts = {}) {
17
+ // Options with defaults
18
+ const options = {
19
+ // Defaults
20
+ backend: 'src/plugins/DefaultBackend.js',
21
+ // Options
22
+ ...opts
23
+ }
24
+
25
+ // Link to the desired backend
26
+ const backend = resolve(__dirname, '../..', options.backend)
27
+ const { assetsDir, assetUrl, init, writeData, readData } = require(backend)
28
+
29
+ config.on('silex:startup:start', ({app}) => {
30
+ // const backwardCompat = new BackwardCompat(rootUrl)
31
+ const router = Router()
32
+
33
+ // website specials
34
+ router.get('/website', readWebsite)
35
+ router.post('/website', writeWebsite)
36
+ router.get(/\/assets\/(.*)/, readAsset)
37
+ router.post('/assets', writeAsset)
38
+
39
+ app.use(noCache, router)
40
+ })
41
+
42
+ async function readWebsite(req, res) {
43
+ const id = req.query.id
44
+ config.emit(EVENT_READ_START, { req, id })
45
+ try {
46
+ const data = await readData(id)
47
+ config.emit(EVENT_READ_END, { req, res, data, id })
48
+ res
49
+ .type('application/json')
50
+ .send(data)
51
+ } catch (err) {
52
+ if(err.code === 'ENOENT') {
53
+ console.error('Read file error, website does not exist', err)
54
+ res.status(404).json({ message: 'Read file error, website does not exist: ' + err.message, code: err.code})
55
+ } else {
56
+ console.error('Read file error', err)
57
+ res.status(400).json({ message: 'Read file error: ' + err.message, code: err.code})
58
+ }
59
+ }
60
+ }
61
+
62
+ function fromBody(body) {
63
+ try {
64
+ return [{
65
+ assets: body.assets,
66
+ pages: body.pages,
67
+ files: body.files,
68
+ styles: body.styles,
69
+ settings: body.settings,
70
+ publication: body.publication,
71
+ name: body.name,
72
+ fonts: body.fonts,
73
+ symbols: body.symbols,
74
+ }, null]
75
+ } catch(err) {
76
+ console.error('Could not parse body data', body, err)
77
+ return [null, err]
78
+ }
79
+ }
80
+
81
+ async function writeWebsite(req, res) {
82
+ const id = req.query.id
83
+ await init(id)
84
+
85
+ const [data, err] = fromBody(req.body)
86
+ if(err) {
87
+ res.status(400).json({ message: 'Error writing data file, could not parse the provided body: ' + err.message, code: err.code})
88
+ return
89
+ }
90
+ config.emit(EVENT_WRITE_START, { req, id, data })
91
+ try {
92
+ await writeData(id, data)
93
+ } catch (err) {
94
+ console.error('Error writing data file', err)
95
+ res.status(500).json({ message: 'Error writing data file: ' + err.message, code: err.code})
96
+ return
97
+ }
98
+
99
+ config.emit(EVENT_WRITE_END, { res, req, id, data })
100
+
101
+ res.json({
102
+ message: 'OK',
103
+ })
104
+ }
105
+
106
+ async function readAsset(req, res) {
107
+ const id = req.query.id
108
+ const fileName = req.params[0]
109
+ const uploadDir = await assetsDir(id)
110
+ config.emit(EVENT_ASSET_READ_START, { req, id, fileName, uploadDir })
111
+ res.sendFile(`${uploadDir}/${fileName}`)
112
+ config.emit(EVENT_ASSET_READ_END, { req, id, fileName, uploadDir })
113
+ }
114
+
115
+ async function writeAsset(req, res) {
116
+ const id = req.query.id
117
+ // FIXME: assetsDir and assetsUrl will both download the site settings file => get both in 1 call
118
+ const uploadDir = await assetsDir(id)
119
+
120
+ const form = formidable({
121
+ uploadDir,
122
+ filename: (name, ext, part, _form) => `${name}${ext}`,
123
+ multiples: true,
124
+ keepExtensions: true,
125
+ })
126
+
127
+ form.parse(req, async (err, fields, files) => {
128
+ if (err) {
129
+ console.error('Error parsing upload data', err)
130
+ res
131
+ .status(400)
132
+ .json({ message: 'Error parsing upload data: ' + err.message, code: err.code})
133
+ return
134
+ }
135
+ Promise.all(
136
+ [].concat(files['files[]']) // may be an array or 1 element
137
+ .map(file => {
138
+ const { originalFilename, filepath } = file
139
+ return assetUrl(id, originalFilename)
140
+ })
141
+ )
142
+ .then(data => {
143
+ config.emit(EVENT_ASSET_WRITE_START, { req, id, uploadDir, form, data })
144
+ res.json({ data })
145
+ config.emit(EVENT_ASSET_WRITE_END, { res, req, id, uploadDir, form, data })
146
+ })
147
+ })
148
+ }
149
+ }
@@ -0,0 +1,21 @@
1
+ import { Config } from '@silexlabs/silex-plugins'
2
+ import { getEditorConfig } from './grapesjs'
3
+
4
+ /**
5
+ * @fileoverview Silex client side config
6
+ */
7
+
8
+ const id = new URL(location.href).searchParams.get('id') || 'default'
9
+ const rootUrl = '.'
10
+
11
+ export class SilexConfig extends Config {
12
+ /**
13
+ * debug mode
14
+ */
15
+ debug = false
16
+
17
+ /**
18
+ * Grapesjs config
19
+ */
20
+ editor = getEditorConfig(id, rootUrl)
21
+ }
@@ -0,0 +1,4 @@
1
+ export * from './index'
2
+ export * from './grapesjs/index'
3
+ export * from './config'
4
+
@@ -0,0 +1,88 @@
1
+ import * as grapesjs from 'grapesjs/dist/grapes.min.js'
2
+
3
+ const pluginName = 'blocks'
4
+ const clsContainer = 'silex-container'
5
+ const clsSection = 'silex-section'
6
+ const clsSectionBackground = 'silex-section-background'
7
+
8
+ export const blocksPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => {
9
+ // Make row and column classes private
10
+ const privateCls = [clsSection, clsSectionBackground, clsContainer]
11
+ .map(cls => '.' + cls)
12
+ editor.on(
13
+ 'selector:add',
14
+ selector => privateCls.indexOf(selector.getFullName()) >= 0 && selector.set('private', 1)
15
+ )
16
+ ;[
17
+ // {
18
+ // id: 'section',
19
+ // def: {
20
+ // label: 'Section',
21
+ // category: 'Containers',
22
+ // attributes: { class: 'fa fa-list-alt' },
23
+ // draggable: 'body',
24
+ // content: {
25
+ // tagName: 'section',
26
+ // styles: `
27
+ // .${clsSectionBackground} {
28
+ // display: flex;
29
+ // justify-content: center;
30
+ // }
31
+ // `,
32
+ // components: [
33
+ // {
34
+ // tagName: 'div',
35
+ // name: 'Section container',
36
+ // attributes: { class: ['website-width'] },
37
+ // draggable: false,
38
+ // removable: false,
39
+ // resizable: true,
40
+ // styles: `
41
+ // .${clsSection} {
42
+ // min-height: 100px;
43
+ // width: 1200px;
44
+ // }
45
+ // `,
46
+ // },
47
+ // ],
48
+ // },
49
+ // },
50
+ // },
51
+ {
52
+ id: 'container',
53
+ def: {
54
+ label: 'Container',
55
+ category: 'Basics',
56
+ attributes: { class: 'container-png' },
57
+ content: {
58
+ type: 'container',
59
+ style: {
60
+ 'min-height': '100px',
61
+ },
62
+ },
63
+ },
64
+ },
65
+ // {
66
+ // id: 'link-block',
67
+ // def: {
68
+ // category: 'Containers',
69
+ // label: 'Link Block',
70
+ // attributes: { class: 'fa fa-link' },
71
+ // content: {
72
+ // type: 'link',
73
+ // editable: false,
74
+ // droppable: true,
75
+ // resizable: true,
76
+ // style:{
77
+ // display: 'inline-block',
78
+ // padding: '5px',
79
+ // 'min-height': '100px',
80
+ // 'width': '100px'
81
+ // }
82
+ // },
83
+ // },
84
+ // },
85
+ ]
86
+ .forEach(block => editor.BlockManager.add(block.id, block.def))
87
+ })
88
+
@@ -0,0 +1,21 @@
1
+ import grapesjs from 'grapesjs/dist/grapes.min.js'
2
+
3
+ import { getPageSlug } from '../../page'
4
+ import { Page } from '../../types'
5
+
6
+ const pluginName = 'eleventy'
7
+
8
+ export const eleventyPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => {
9
+ editor.on(opts.eventStart || 'publish:start', data => {
10
+ data.pages.forEach((page: Page, idx) => {
11
+ const file = data.files[idx]
12
+ file.css = `---
13
+ permalink: /css/${getPageSlug(page.name)}.css
14
+ ---
15
+ ${file.css}
16
+ `
17
+ file.cssPath += '.liquid'
18
+ })
19
+ })
20
+ })
21
+
@@ -0,0 +1,255 @@
1
+ import grapesjs, { EditorConfig } from 'grapesjs'
2
+ import openImport from './openImport'
3
+
4
+ /**
5
+ * @fileoverview This is where grapes config gets created
6
+ * Handle plugins, options and initialization of the editor
7
+ */
8
+
9
+ // ////////////////////
10
+ // Plugins
11
+ // ////////////////////
12
+ import blocksBasicPlugin from 'grapesjs-blocks-basic'
13
+ import styleFilterPlugin from 'grapesjs-style-filter'
14
+ import formPlugin from 'grapesjs-plugin-forms'
15
+ import codePlugin from 'grapesjs-custom-code'
16
+ import uiSuggestClasses from '@silexlabs/grapesjs-ui-suggest-classes'
17
+ import symbolsPlugin from '@silexlabs/grapesjs-symbols/src'
18
+ import loadingPlugin from '@silexlabs/grapesjs-loading'
19
+ import fontsDialogPlugin, { cmdOpenFonts } from '@silexlabs/grapesjs-fonts'
20
+ import symbolDialogsPlugin, { cmdPromptAddSymbol } from './symbolDialogs'
21
+
22
+ import { pagePanelPlugin, cmdTogglePages, cmdAddPage } from './page-panel'
23
+ import { newPageDialog, cmdOpenNewPageDialog } from './new-page-dialog'
24
+ import { projectBarPlugin } from './project-bar'
25
+ import { settingsDialog, cmdOpenSettings } from './settings'
26
+ import { blocksPlugin } from './blocks'
27
+ import { semanticPlugin } from './semantic'
28
+ import { richTextPlugin } from './rich-text'
29
+ import { internalLinksPlugin } from './internal-links'
30
+ import { publishPlugin } from './publish'
31
+ import { templatePlugin } from './template'
32
+ import { eleventyPlugin } from './eleventy'
33
+
34
+ const plugins = [
35
+ {name: './grapesjs/project-bar', value: projectBarPlugin}, // has to be before panels and dialogs
36
+ {name: './grapesjs/settings', value: settingsDialog},
37
+ {name: '@silexlabs/grapesjs-fonts', value: fontsDialogPlugin},
38
+ {name: './grapesjs/new-page-dialog', value: newPageDialog},
39
+ {name: './grapesjs/page-panel', value: pagePanelPlugin},
40
+ {name: 'grapesjs-blocks-basic', value: blocksBasicPlugin},
41
+ {name: './grapesjs/blocks', value: blocksPlugin},
42
+ {name: './grapesjs/semantic', value: semanticPlugin},
43
+ {name: './grapesjs/rich-text', value: richTextPlugin},
44
+ {name: 'grapesjs-style-filter', value: styleFilterPlugin},
45
+ {name: 'grapesjs-plugin-forms', value: formPlugin},
46
+ {name: 'grapesjs-custom-code', value: codePlugin},
47
+ {name: './grapesjs/internal-links', value: internalLinksPlugin},
48
+ {name: '@silexlabs/grapesjs-ui-suggest-classes', value: uiSuggestClasses},
49
+ {name: './grapesjs/symbolDialogs', value: symbolDialogsPlugin},
50
+ {name: '@silexlabs/grapesjs-symbols', value: symbolsPlugin},
51
+ {name: './grapesjs/publish', value: publishPlugin},
52
+ {name: './grapesjs/template', value: templatePlugin},
53
+ {name: './grapesjs/eleventy', value: eleventyPlugin},
54
+ {name: '@silexlabs/grapesjs-loading', value: loadingPlugin},
55
+ ]
56
+ // Check that all plugins are loaded correctly
57
+ plugins
58
+ .filter(p => !p.value)
59
+ .forEach(p => {
60
+ throw new Error(`Plugin ${p.name} could not be loaded correctly`)
61
+ })
62
+
63
+ export function getPlugins() { return plugins }
64
+
65
+ // ////////////////////
66
+ // Config
67
+ // ////////////////////
68
+ const catBasic = 'Containers'
69
+ const catComponents = 'Components'
70
+
71
+ export function getEditorConfig(id: string, rootUrl: string) {
72
+ return {
73
+ container: '#gjs',
74
+ height: '100%',
75
+ showOffsets: true,
76
+ showDevices: true,
77
+
78
+ //pageManager: {},
79
+
80
+ layerManager: {
81
+ appendTo: '.layer-manager-container',
82
+ },
83
+
84
+ blockManager: {
85
+ appendTo: '.block-manager-container',
86
+ },
87
+
88
+ storageManager: {
89
+ autoload: true,
90
+ options: {
91
+ remote: {
92
+ urlStore: `${rootUrl}/website/?id=${id}`,
93
+ urlLoad: `${rootUrl}/website/?id=${id}`,
94
+ },
95
+ },
96
+ },
97
+
98
+ plugins: plugins.map(p => p.value),
99
+
100
+ pluginsOpts: {
101
+ [blocksBasicPlugin as any]: {
102
+ blocks: ['text', 'image', 'video', 'map'],
103
+ category: catBasic,
104
+ //flexGrid: true,
105
+ },
106
+ [projectBarPlugin as any]: {
107
+ panels: [
108
+ {
109
+ id: 'dash',
110
+ className: 'logo',
111
+ attributes: { title: 'Go to your dashboard' },
112
+ link: '/',
113
+ command: 'open-dash',
114
+ }, {
115
+ id: 'block-manager-btn',
116
+ className: 'block-manager-btn fa fa-fw fa-plus',
117
+ attributes: { title: 'Blocks', containerClassName: 'block-manager-container', },
118
+ command: 'open-blocks',
119
+ }, {
120
+ id: 'symbols-btn',
121
+ className: 'symbols-btn fa fa-fw fa-diamond',
122
+ attributes: { title: 'Symbols', containerClassName: 'symbols-list-container', },
123
+ command: 'open-symbols',
124
+ buttons: [
125
+ {
126
+ id: 'symbol-create-button',
127
+ className: 'pages__add-page fa fa-plus',
128
+ label: 'Create symbol from selection',
129
+ command: cmdPromptAddSymbol,
130
+ },
131
+ ],
132
+ }, {
133
+ id: 'page-panel-btn',
134
+ className: 'page-panel-btn fa fa-fw fa-file',
135
+ attributes: { title: 'Pages', containerClassName: 'page-panel-container', },
136
+ command: cmdTogglePages,
137
+ buttons: [{
138
+ className: 'pages__add-page fa fa-file',
139
+ command: cmdAddPage,
140
+ text: '+',
141
+ }],
142
+ }, {
143
+ id: 'layer-manager-btn',
144
+ className: 'layer-manager-btn fa fa-fw fa-list',
145
+ attributes: { title: 'Layers', containerClassName: 'layer-manager-container', },
146
+ command: 'open-layers',
147
+ }, {
148
+ id: 'font-dialog-btn',
149
+ className: 'font-manager-btn fa fa-fw fa-font',
150
+ attributes: { title: 'Fonts' },
151
+ command: cmdOpenFonts,
152
+ }, {
153
+ id: 'settings-dialog-btn',
154
+ className: 'page-panel-btn fa fa-fw fa-cog',
155
+ attributes: { title: 'Settings' },
156
+ command: cmdOpenSettings,
157
+ }, {
158
+ id: 'spacer',
159
+ attributes: {},
160
+ className: 'project-bar-spacer',
161
+ }, {
162
+ id: 'logout-button',
163
+ className: 'page-panel-btn fa fa-fw fa-sign-out',
164
+ attributes: { title: 'Sign out' },
165
+ command: 'relogin',
166
+ },
167
+ ],
168
+ },
169
+ [publishPlugin as any]: {
170
+ appendTo: 'options',
171
+ rootUrl,
172
+ id,
173
+ },
174
+ [pagePanelPlugin as any]: {
175
+ cmdOpenNewPageDialog,
176
+ cmdOpenSettings,
177
+ appendTo: '.page-panel-container',
178
+ },
179
+ [uiSuggestClasses as any]: {
180
+ enableCount: false,
181
+ enablePerformance: false,
182
+ },
183
+ [internalLinksPlugin as any]: {
184
+ // FIXME: warn the user about links in error
185
+ onError: (errors) => console.log('Links errors:', errors),
186
+ },
187
+ [codePlugin as any]: {
188
+ blockLabel: 'HTML',
189
+ blockCustomCode: {
190
+ category: catComponents,
191
+ }
192
+ },
193
+ [symbolsPlugin as any]: {
194
+ appendTo: '.symbols-list-container',
195
+ },
196
+ [fontsDialogPlugin as any]: {
197
+ api_key: 'AIzaSyAdJTYSLPlKz4w5Iqyy-JAF2o8uQKd1FKc',
198
+ },
199
+ },
200
+ }
201
+ }
202
+
203
+ // ////////////////////
204
+ // Initialize editor
205
+ // ////////////////////
206
+ // Expose grapes for plugins
207
+ // window['grapesjs'] = grapesjs
208
+
209
+ // Keep a ref to the editor singleton
210
+ let editor
211
+
212
+ export function initEditor(config: EditorConfig) {
213
+ if(editor) throw new Error('Grapesjs editor already created')
214
+
215
+ editor = grapesjs.init(config)
216
+
217
+ // customize the editor
218
+ ;['text']
219
+ .forEach(id => editor.Blocks.get(id)?.set('category', 'Basics'))
220
+ ;['image', 'video']
221
+ .forEach(id => editor.Blocks.get(id)?.set('category', 'Media'))
222
+ ;['map']
223
+ .forEach(id => editor.Blocks.get(id)?.set('category', 'Components'))
224
+ editor.Blocks.render()
225
+
226
+ editor.Commands.add('gjs-open-import-webpage', openImport(editor, {
227
+ modalImportLabel: '',
228
+ modalImportContent: 'Paste a web page HTML code here.',
229
+ modalImportButton: 'Import',
230
+ modalImportTitle: 'Import from website',
231
+ }))
232
+
233
+ editor.on('load', () => {
234
+ // // move the options panel to the sidebar
235
+ // const optionsEl = editor.Panels.getPanel('options').view.el
236
+ // editor.Panels.getPanel('project-bar-panel').view.el
237
+ // .appendChild(options)
238
+ // options.style.width = 0
239
+ // options.style.position = 'static'
240
+ // remove blocks and layers buttons from the properties
241
+ setTimeout(() => {
242
+ editor.Panels.getPanel('views').buttons.remove('open-blocks')
243
+ editor.Panels.getPanel('views').buttons.remove('open-layers')
244
+ editor.Panels.getPanel('views').view.el.firstChild.style.justifyContent = 'initial' // align left
245
+ editor.Panels.getPanel('options').buttons.remove('export-template')
246
+ })
247
+
248
+ // use the style filter plugin
249
+ editor.StyleManager.addProperty('extra',{ extend: 'filter' })
250
+ })
251
+ }
252
+
253
+ export function getEditor() {
254
+ return editor
255
+ }
@@ -0,0 +1,87 @@
1
+ import { internalLinksPlugin } from './internal-links'
2
+ import grapesjs from 'grapesjs'
3
+
4
+ let editor
5
+ let nextId = 1
6
+ function getNewId() {
7
+ return (nextId++).toString()
8
+ }
9
+ const comp1 = {
10
+ tagName: 'a',
11
+ content: 'link to main',
12
+ attributes: { href: './index.html' },
13
+ }
14
+ const comp2 = {
15
+ tagName: 'a',
16
+ content: 'link to page 1',
17
+ attributes: { href: './page-1.html' },
18
+ }
19
+ const comp3 = {
20
+ tagName: 'a',
21
+ content: 'link to page 2',
22
+ attributes: { href: './page-2.html' },
23
+ }
24
+
25
+ const pageMain = {
26
+ id: getNewId(),
27
+ type: 'main',
28
+ component: {
29
+ tagName: 'div',
30
+ components: [comp1, comp2],
31
+ },
32
+ }
33
+ const page1 = {
34
+ id: getNewId(),
35
+ name: 'Page 1',
36
+ component: {
37
+ tagName: 'div',
38
+ components: [comp3],
39
+ },
40
+ }
41
+ const page2 = {
42
+ id: getNewId(),
43
+ name: 'Page 2',
44
+ component: {
45
+ tagName: 'div',
46
+ components: [comp1, comp2, comp3],
47
+ },
48
+ }
49
+ beforeEach(() => {
50
+ editor = grapesjs.init({
51
+ headless: true,
52
+ storageManager: { autoload: false },
53
+ pageManager: {
54
+ pages: [pageMain, page1, page2],
55
+ },
56
+ plugins: [internalLinksPlugin],
57
+ })
58
+ expect(editor).not.toBeUndefined()
59
+ })
60
+ test('init', () => {
61
+ expect(editor.getComponents()).toHaveLength(pageMain.component.components.length)
62
+ })
63
+ test('rename page and rewrite links in current page', () => {
64
+ const page = editor.Pages.getSelected()
65
+ expect(page.id).toBe(pageMain.id)
66
+ expect(page.getName()).toBeUndefined()
67
+ const newName = 'Home page'
68
+ const newPath = './home-page.html'
69
+ // Do not work: page.setName(newName)
70
+ page.set('name', newName)
71
+ expect(page.getName()).toBe(newName)
72
+ const comp1Model = page.getMainComponent().components().find(c => c.attributes.content === comp1.content)
73
+ expect(comp1Model).toBeTruthy()
74
+ expect(comp1Model.attributes.attributes.href).toBe(newPath)
75
+ })
76
+ test('rename page and rewrite links in another page', () => {
77
+ const page = editor.Pages.get(page2.id)
78
+ expect(page.get('name')).toBe(page2.name)
79
+ const newName = 'Page 2 new-name'
80
+ const newPath = './page-2-new-name.html'
81
+ // Do not work: page.setName(newName)
82
+ page.set('name', newName)
83
+ expect(page.get('name')).toBe(newName)
84
+ const comp1Model = page.getMainComponent().components().find(c => c.attributes.content === comp3.content)
85
+ expect(comp1Model).toBeTruthy()
86
+ expect(comp1Model.attributes.attributes.href).toBe(newPath)
87
+ })