@silexlabs/silex 3.4.0 → 3.5.1-1

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 (91) hide show
  1. package/.silex.js +0 -3
  2. package/dist/client/client/config.d.ts +8 -0
  3. package/dist/client/client/grapesjs/cms/DataSource.d.ts +43 -0
  4. package/dist/client/client/grapesjs/cms/blocks.d.ts +2 -0
  5. package/dist/client/client/grapesjs/cms/buttons.d.ts +2 -0
  6. package/dist/client/client/grapesjs/cms/collection-pages.d.ts +4 -0
  7. package/dist/client/client/grapesjs/cms/config.d.ts +9 -0
  8. package/dist/client/client/grapesjs/cms/filters.d.ts +3 -0
  9. package/dist/client/client/grapesjs/cms/index.d.ts +44 -0
  10. package/dist/client/client/grapesjs/cms/liquid.d.ts +59 -0
  11. package/dist/client/client/grapesjs/cms/page-settings.d.ts +6 -0
  12. package/dist/client/client/grapesjs/cms/publication.d.ts +60 -0
  13. package/dist/client/client/grapesjs/cms/site-settings.d.ts +3 -0
  14. package/dist/client/client/grapesjs/cms/states.d.ts +8 -0
  15. package/dist/client/client/grapesjs/cms/traits.d.ts +8 -0
  16. package/dist/client/client/grapesjs/settings.d.ts +12 -3
  17. package/dist/client/client/grapesjs/view-buttons.d.ts +2 -1
  18. package/dist/client/client/utils.d.ts +1 -0
  19. package/dist/client/constants.d.ts +2 -1
  20. package/dist/client/css/admin.css +88 -14
  21. package/dist/client/index.html +2 -2
  22. package/dist/client/js/main.js +1968 -264
  23. package/dist/client/types.d.ts +3 -1
  24. package/dist/plugins/client/plugins/client/template.js +1 -1
  25. package/dist/plugins/client/plugins/client/template.js.map +1 -1
  26. package/dist/plugins/client/types.d.ts +3 -1
  27. package/dist/plugins/client/types.js +11 -8
  28. package/dist/plugins/client/types.js.map +1 -1
  29. package/dist/plugins/server/constants.d.ts +2 -1
  30. package/dist/plugins/server/constants.js +3 -2
  31. package/dist/plugins/server/constants.js.map +1 -1
  32. package/dist/plugins/server/plugins/server/FtpConnector.js +15 -20
  33. package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
  34. package/dist/plugins/server/plugins/server/GitlabConnector.js +34 -7
  35. package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
  36. package/dist/plugins/server/server/connectors/FsStorage.js +16 -20
  37. package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
  38. package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +4 -0
  39. package/dist/plugins/server/server/utils/websiteDataSerialization.js +21 -12
  40. package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -1
  41. package/dist/plugins/server/types.d.ts +3 -1
  42. package/dist/plugins/server/types.js +12 -9
  43. package/dist/plugins/server/types.js.map +1 -1
  44. package/dist/server/constants.js +3 -2
  45. package/dist/server/server/connectors/FsStorage.js +14 -18
  46. package/dist/server/server/utils/websiteDataSerialization.js +16 -11
  47. package/dist/server/types.js +6 -2
  48. package/package.json +15 -10
  49. package/src/css/_variables.scss +2 -1
  50. package/src/css/grapesjs-plugins.scss +31 -0
  51. package/src/css/grapesjs.scss +15 -0
  52. package/src/css/page-panel.scss +1 -1
  53. package/src/css/silex.scss +64 -5
  54. package/src/ts/client/config.ts +23 -0
  55. package/src/ts/client/grapesjs/breadcrumbs.ts +3 -2
  56. package/src/ts/client/grapesjs/cms/DataSource.ts +333 -0
  57. package/src/ts/client/grapesjs/cms/blocks.ts +124 -0
  58. package/src/ts/client/grapesjs/cms/buttons.ts +137 -0
  59. package/src/ts/client/grapesjs/cms/collection-pages.ts +205 -0
  60. package/src/ts/client/grapesjs/cms/config.ts +47 -0
  61. package/src/ts/client/grapesjs/cms/filters.ts +53 -0
  62. package/src/ts/client/grapesjs/cms/index.ts +130 -0
  63. package/src/ts/client/grapesjs/cms/liquid.mock.ts +181 -0
  64. package/src/ts/client/grapesjs/cms/liquid.test.ts +239 -0
  65. package/src/ts/client/grapesjs/cms/liquid.ts +325 -0
  66. package/src/ts/client/grapesjs/cms/page-settings.ts +285 -0
  67. package/src/ts/client/grapesjs/cms/publication.test.ts +300 -0
  68. package/src/ts/client/grapesjs/cms/publication.ts +708 -0
  69. package/src/ts/client/grapesjs/cms/site-settings.ts +79 -0
  70. package/src/ts/client/grapesjs/cms/states.ts +125 -0
  71. package/src/ts/client/grapesjs/cms/traits.ts +95 -0
  72. package/src/ts/client/grapesjs/flex.ts +59 -39
  73. package/src/ts/client/grapesjs/img.ts +1 -1
  74. package/src/ts/client/grapesjs/index.ts +77 -4
  75. package/src/ts/client/grapesjs/internal-links.ts +4 -2
  76. package/src/ts/client/grapesjs/page-panel.ts +8 -7
  77. package/src/ts/client/grapesjs/sectors.ts +18 -10
  78. package/src/ts/client/grapesjs/settings.ts +252 -101
  79. package/src/ts/client/grapesjs/storage.ts +6 -1
  80. package/src/ts/client/grapesjs/view-buttons.ts +39 -3
  81. package/src/ts/client/utils.ts +15 -2
  82. package/src/ts/constants.ts +2 -1
  83. package/src/ts/plugins/client/template.ts +1 -1
  84. package/src/ts/plugins/server/FtpConnector.test.ts +1 -0
  85. package/src/ts/plugins/server/FtpConnector.ts +20 -23
  86. package/src/ts/plugins/server/GitlabConnector.ts +40 -10
  87. package/src/ts/server/connectors/FsStorage.test.ts +3 -2
  88. package/src/ts/server/connectors/FsStorage.ts +21 -23
  89. package/src/ts/server/utils/websiteDataSerialization.test.ts +45 -13
  90. package/src/ts/server/utils/websiteDataSerialization.ts +23 -14
  91. package/src/ts/types.ts +6 -2
@@ -43,17 +43,25 @@ export function registerSector(editor: Editor, config: SectorConfig, at?: number
43
43
  }
44
44
  })
45
45
 
46
- async function applyVisibility(comp: Component = editor.getSelected()) {
46
+ // Update visibility depending on selected component
47
+ let showSector = false
48
+ editor.on('component:selected', async (comp: Component) => {
49
+ if (!comp) return // No selection yet
47
50
  const shouldShow = await config.shouldShow(comp)
48
- if (sector.get('visible') !== shouldShow) {
49
- sector.off('change:visible', applyVisibility)
50
- sector.set('visible', shouldShow)
51
- sector.on('change:visible', applyVisibility)
52
- // sector.getProperties()
53
- // .forEach(p => p.set('visible', shouldShow))
51
+ showSector = shouldShow
52
+ sector.set('visible', shouldShow)
53
+ sector.getProperties().forEach(p => p.set('visible', shouldShow))
54
+ })
55
+
56
+ // Watch changes made by grapesjs and revert them if needed
57
+ function doubleCheck() {
58
+ if (sector.get('visible') !== showSector) {
59
+ sector.off('change:visible', doubleCheck)
60
+ sector.set('visible', showSector)
61
+ sector.getProperties().forEach(p => p.set('visible', showSector))
62
+ sector.on('change:visible', doubleCheck)
54
63
  }
55
64
  }
56
-
57
- sector.on('change:visible', applyVisibility)
58
- editor.on('component:selected', comp => applyVisibility(comp))
65
+ sector.on('change:visible', doubleCheck)
59
66
  }
67
+
@@ -22,69 +22,221 @@ import {defaultSections, idCodeWrapper, isSite, SettingsSection} from './setting
22
22
  * @fileoverview This file contains the settings dialog. The config API lets plugins add sections to the settings dialog.
23
23
  */
24
24
 
25
- import { WebsiteSettings } from '../../types'
25
+ import { WebsiteData, WebsiteSettings } from '../../types'
26
26
  import { ClientEvent } from '../events'
27
27
  import { SILEX_VERSION } from '../../constants'
28
+ import { Button, Editor } from 'grapesjs'
28
29
 
29
- const sectionsSite = [...defaultSections]
30
- const sectionsPage = [...defaultSections]
30
+ const sectionsSite: SettingsSection[] = [...defaultSections]
31
+ const sectionsPage: SettingsSection[] = [...defaultSections]
31
32
 
32
- const el = document.createElement('div')
33
- let modal
33
+ const el: HTMLDivElement = document.createElement('div')
34
+ let modal: any | undefined // the modal var should be of type ModalModule, not Modal, but it is not exported from grapesjs
35
+ let settingsState: {
36
+ isOpen: boolean
37
+ title: string
38
+ page: unknown
39
+ sectionId?: string
40
+ sender?: Button
41
+ } | null = null
42
+ let customSettingsDialog: HTMLDivElement | null = null
34
43
 
35
44
  export const cmdOpenSettings = 'open-settings'
45
+ export const cmdAddSection = 'settings:add-section'
46
+ export const cmdRemoveSection = 'settings:remove-section'
47
+
48
+ export interface AddSectionOption {
49
+ section: SettingsSection,
50
+ siteOrPage: 'site' | 'page',
51
+ position: 'first' | 'last' | number
52
+ }
53
+
54
+ let headEditor: ReturnType<Editor['CodeManager']['createViewer']> | null = null
55
+
56
+
57
+ function createCustomSettingsDialog(editor: Editor, opts: Record<string, unknown>, page: unknown, sectionId?: string): HTMLDivElement {
58
+ const dialog = document.createElement('div')
59
+ dialog.className = 'settings-dialog gjs-two-color'
60
+ dialog.innerHTML = `
61
+ <div class="settings-content" role="dialog" aria-modal="true" aria-labelledby="settings-title">
62
+ <div class="settings-header">
63
+ <h3 id="settings-title">${page ? 'Page settings' : 'Site Settings'}</h3>
64
+ <button type="button" class="settings-close" title="Close" aria-label="Close settings">×</button>
65
+ </div>
66
+ <div class="settings-body"></div>
67
+ </div>
68
+ `
69
+
70
+ // Handle close button
71
+ const closeButton = dialog.querySelector('.settings-close') as HTMLButtonElement
72
+ closeButton.addEventListener('click', () => {
73
+ closeCustomSettingsDialog(editor)
74
+ })
75
+
76
+ // Handle keyboard shortcuts
77
+ dialog.addEventListener('keydown', (e: KeyboardEvent) => {
78
+ // Close on Escape
79
+ if (e.key === 'Escape') {
80
+ e.preventDefault()
81
+ closeCustomSettingsDialog(editor)
82
+ return
83
+ }
84
+
85
+ // Save on Ctrl/Cmd + Enter
86
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
87
+ e.preventDefault()
88
+ const form = dialog.querySelector('form') as HTMLFormElement
89
+ if (form) {
90
+ form.requestSubmit()
91
+ }
92
+ return
93
+ }
94
+
95
+ // Handle Alt+C (Cancel) and Alt+A (Apply)
96
+ if (e.altKey) {
97
+ if (e.key.toLowerCase() === 'c') {
98
+ e.preventDefault()
99
+ closeCustomSettingsDialog(editor)
100
+ return
101
+ }
102
+ if (e.key.toLowerCase() === 'a') {
103
+ e.preventDefault()
104
+ const form = dialog.querySelector('form') as HTMLFormElement
105
+ if (form) {
106
+ form.requestSubmit()
107
+ }
108
+ return
109
+ }
110
+ }
111
+ })
112
+
113
+ // Add content
114
+ displaySettings(editor, opts, page, sectionId)
115
+ const body = dialog.querySelector('.settings-body') as HTMLDivElement
116
+ body.appendChild(el)
117
+
118
+ // Initialize focus when dialog opens
119
+ setTimeout(() => {
120
+ const firstTab = dialog.querySelector('.silex-list--menu li[role="button"]') as HTMLElement
121
+ if (firstTab) {
122
+ firstTab.focus()
123
+ }
124
+ }, 100)
125
+
126
+ // Handle form submission
127
+ const form = el.querySelector('form') as HTMLFormElement
128
+ form.onsubmit = (event: Event) => {
129
+ event.preventDefault()
130
+ editor.trigger(ClientEvent.SETTINGS_SAVE_START, page)
131
+ saveSettings(editor, opts, page)
132
+ editor.trigger(ClientEvent.SETTINGS_SAVE_END, page)
133
+ closeCustomSettingsDialog(editor, false)
134
+ }
135
+
136
+ return dialog
137
+ }
138
+
139
+ function closeCustomSettingsDialog(editor: Editor, fromCommand = false) {
140
+ if (customSettingsDialog) {
141
+ customSettingsDialog.remove()
142
+ customSettingsDialog = null
143
+ }
144
+ settingsState?.sender?.set && settingsState.sender.set('active', 0)
145
+
146
+ // Only call stopCommand if not already being called from the command's stop method
147
+ if (!fromCommand) {
148
+ editor.stopCommand(cmdOpenSettings)
149
+ }
150
+
151
+ settingsState = null
152
+ editor.trigger(ClientEvent.SETTINGS_CLOSE)
153
+ }
154
+
155
+ function reopenSettingsModal(editor: Editor, opts: Record<string, unknown>) {
156
+ if (!settingsState) {
157
+ return
158
+ }
159
+
160
+ modal = editor.Modal.open({
161
+ title: settingsState.title,
162
+ content: '',
163
+ attributes: { class: 'settings-dialog' },
164
+ })
165
+ .onceClose(() => {
166
+ settingsState?.sender?.set && settingsState.sender.set('active', 0)
167
+ editor.stopCommand(cmdOpenSettings)
168
+ settingsState = null
169
+ })
170
+
171
+ displaySettings(editor, opts, settingsState.page, settingsState.sectionId)
172
+ modal.setContent(el)
173
+
174
+ const form = el.querySelector('form') as HTMLFormElement
175
+ form.onsubmit = (event: Event) => {
176
+ event.preventDefault()
177
+ editor.trigger(ClientEvent.SETTINGS_SAVE_START, settingsState?.page)
178
+ saveSettings(editor, opts, settingsState?.page)
179
+ editor.trigger(ClientEvent.SETTINGS_SAVE_END, settingsState?.page)
180
+ editor.stopCommand(cmdOpenSettings)
181
+ }
182
+ }
183
+
184
+ export const settingsDialog = (
185
+ editor: Editor,
186
+ opts: Record<string, unknown>
187
+ ): void => {
188
+ // No need to override Modal system - we use custom dialog
36
189
 
37
- let headEditor = null
38
- export const settingsDialog = (editor, opts) => {
39
190
  editor.Commands.add(cmdOpenSettings, {
40
- run: (_, sender, {page}) => {
41
- modal = editor.Modal.open({
42
- title: page ? 'Page settings' : 'Site Settings',
43
- content: '',
44
- attributes: { class: 'settings-dialog' },
45
- })
46
- .onceClose(() => {
47
- sender?.set && sender.set('active', 0) // Deactivate the button to make it ready to be clicked again, only in case of a grapesjs button (not in the pages panel)
48
- editor.stopCommand(cmdOpenSettings) // apparently this is needed to be able to run the command several times
49
- })
50
- displaySettings(editor, opts, page)
51
- modal.setContent(el)
52
- const form = el.querySelector('form')
53
- form.onsubmit = event => {
54
- event.preventDefault()
55
- editor.trigger(ClientEvent.SETTINGS_SAVE_START, page)
56
- saveSettings(editor, opts, page)
57
- editor.trigger(ClientEvent.SETTINGS_SAVE_END, page)
58
- editor.stopCommand(cmdOpenSettings)
191
+ run: (
192
+ _: Editor,
193
+ sender: Button,
194
+ {page, sectionId}: {page?: unknown, sectionId?: string}
195
+ ) => {
196
+ const title = page ? 'Page settings' : 'Site Settings'
197
+
198
+ // Store settings state
199
+ settingsState = {
200
+ isOpen: true,
201
+ title,
202
+ page,
203
+ sectionId,
204
+ sender
59
205
  }
60
- form.querySelector('input')?.focus()
61
- // Notify other plugins
206
+
207
+ // Create and show custom dialog instead of using GrapesJS modal
208
+ customSettingsDialog = createCustomSettingsDialog(editor, opts, page, sectionId)
209
+ document.body.appendChild(customSettingsDialog)
210
+
211
+ // Let the first focusable element get focus naturally
212
+
62
213
  editor.trigger(ClientEvent.SETTINGS_OPEN, page)
63
- // Return the dialog
64
- return modal
214
+ return customSettingsDialog
65
215
  },
66
- stop: () => {
67
- modal.close()
68
- editor.trigger(ClientEvent.SETTINGS_CLOSE)
216
+ stop: (): void => {
217
+ closeCustomSettingsDialog(editor, true)
69
218
  },
70
219
  })
71
- // add settings to the website
72
- editor.on('storage:start:store', (data) => {
220
+ editor.Commands.add(cmdAddSection, (_editor: Editor, _sender: Button, options: AddSectionOption): void => {
221
+ addSection(options.section, options.siteOrPage, options.position)
222
+ })
223
+ editor.Commands.add(cmdRemoveSection, (_editor: Editor, _sender: Button, options: AddSectionOption): void => {
224
+ removeSection(options.section.id, options.siteOrPage)
225
+ })
226
+ editor.on('storage:start:store', (data: WebsiteData): void => {
73
227
  data.settings = editor.getModel().get('settings')
228
+ /* @ts-ignore FIXME: this should not be there? or is it used on the server side in the sotrage providers? */
74
229
  data.name = editor.getModel().get('name')
75
230
  })
76
- editor.on('storage:end:load', (data) => {
231
+ editor.on('storage:end:load', (data: WebsiteData): void => {
77
232
  const model = editor.getModel()
78
233
  model.set('settings', data.settings || {})
79
- model.set('name', data.name)
234
+ model.set('name', editor.getModel().get('name'))
80
235
  })
81
- // Normal way to apply the head content to the DOM
82
- // fix #1559 Custom <head> code must be reapplied
83
- editor.on('canvas:frame:load', () => {
236
+ editor.on('canvas:frame:load', (): void => {
84
237
  updateDom(editor)
85
238
  })
86
- // When the page changes, update the dom
87
- editor.on('page', (e) => {
239
+ editor.on('page', (_e: unknown): void => {
88
240
  updateDom(editor)
89
241
  })
90
242
  headEditor = editor.CodeManager.createViewer({
@@ -96,9 +248,7 @@ export const settingsDialog = (editor, opts) => {
96
248
  })
97
249
  }
98
250
 
99
- function showSection(item: SettingsSection) {
100
- // **
101
- // Handle the side bar
251
+ function showSection(item: SettingsSection): void {
102
252
  const aside = el.querySelector('aside.silex-bar') as HTMLElement
103
253
  const ul = aside.querySelector('ul.silex-list--menu') as HTMLUListElement
104
254
  const li = ul.querySelector('li#settings-sidebar-' + item.id) as HTMLLIElement
@@ -109,14 +259,12 @@ function showSection(item: SettingsSection) {
109
259
  console.error('Cannot find the default section in the side bar')
110
260
  return
111
261
  }
112
- showSection(defaultSections[0]) // Fallback to the first section
262
+ showSection(defaultSections[0])
113
263
  return
114
264
  }
115
- // Update active
116
265
  Array.from(ul.querySelectorAll('.active')).forEach(el => el.classList.remove('active'))
117
266
  li.classList.add('active')
118
- // **
119
- // Handle the main section
267
+
120
268
  const main = el.querySelector('main#settings__main') as HTMLElement
121
269
  const mainItem = main.querySelector(`#settings-${item.id}`)
122
270
  if(!mainItem) {
@@ -125,20 +273,19 @@ function showSection(item: SettingsSection) {
125
273
  console.error('Cannot find the default section in the settings dialog')
126
274
  return
127
275
  }
128
- showSection(defaultSections[0]) // Fallback to the first section
276
+ showSection(defaultSections[0])
129
277
  return
130
278
  }
131
- // Update hidden
132
279
  Array.from(main.querySelectorAll('.silex-hideable')).forEach(el => el.classList.add('silex-hidden'))
133
280
  mainItem.classList.remove('silex-hidden')
134
- // This messes up with the save / cancel mechanism
135
- // displaySettings(editor, config, model)
136
- // Refresh the code editor just in case it went from hidden to visible
137
- // This makes it ready to be used when the user clicks on the tab
138
- headEditor.refresh()
281
+ headEditor?.refresh()
139
282
  }
140
283
 
141
- export function addSection(section, siteOrPage: 'site' | 'page', position: 'first' | 'last' | number) {
284
+ export function addSection(
285
+ section: SettingsSection,
286
+ siteOrPage: 'site' | 'page',
287
+ position: 'first' | 'last' | number
288
+ ): void {
142
289
  const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
143
290
  if (position === 'first') {
144
291
  sections.unshift(section)
@@ -151,7 +298,10 @@ export function addSection(section, siteOrPage: 'site' | 'page', position: 'firs
151
298
  }
152
299
  }
153
300
 
154
- export function removeSection(id, siteOrPage: 'site' | 'page') {
301
+ export function removeSection(
302
+ id: string,
303
+ siteOrPage: 'site' | 'page'
304
+ ): void {
155
305
  const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
156
306
  const index = sections.findIndex(section => section.id === id)
157
307
  if(index === -1) throw new Error(`Cannot find section with id ${id}`)
@@ -159,15 +309,18 @@ export function removeSection(id, siteOrPage: 'site' | 'page') {
159
309
  }
160
310
 
161
311
 
162
- let currentSection
163
- function displaySettings(editor, config, model = editor.getModel()) {
164
- // Update the model with the current settings
165
- const settings = model.get('settings') || {} as WebsiteSettings
312
+ let currentSection: SettingsSection | undefined
313
+ function displaySettings(
314
+ editor: Editor,
315
+ config: Record<string, unknown>,
316
+ model: any = editor.getModel(),
317
+ sectionId?: string
318
+ ): void {
319
+ const settings: WebsiteSettings = model.get('settings') || {} as WebsiteSettings
166
320
  model.set('settings', settings)
167
- // Get the current sections for page or site
168
- const menuItemsCurrent = isSite(model) ? sectionsSite : sectionsPage
169
- // Init the current section selection
170
- currentSection = currentSection || menuItemsCurrent[0]
321
+ const menuItemsCurrent: SettingsSection[] = isSite(model) ? sectionsSite : sectionsPage
322
+ const targetSection = !!sectionId && menuItemsCurrent.find(section => section.id === sectionId)
323
+ currentSection = targetSection || currentSection || menuItemsCurrent[0]
171
324
  const sections: TemplateResult[] = menuItemsCurrent.map(item => {
172
325
  try {
173
326
  return item.render(settings, model)
@@ -176,7 +329,6 @@ function displaySettings(editor, config, model = editor.getModel()) {
176
329
  return html`<div class="silex-error">Error rendering settings section ${item.id}</div>`
177
330
  }
178
331
  })
179
- // Render the settings dialog
180
332
  render(html`
181
333
  <form class="silex-form">
182
334
  <div class="silex-help">
@@ -190,17 +342,24 @@ function displaySettings(editor, config, model = editor.getModel()) {
190
342
  </div>
191
343
  <section class="silex-sidebar-dialog">
192
344
  <aside class="silex-bar">
193
- <ul class="silex-list silex-list--menu">
194
- ${menuItemsCurrent.map(item => html`
345
+ <ul class="silex-list silex-list--menu" aria-label="Settings sections">
346
+ ${menuItemsCurrent.map((item, index) => html`
195
347
  <li
196
348
  id="settings-sidebar-${item.id}"
197
- class=${item.id === currentSection.id ? 'active' : ''}
198
- @click=${e => {
349
+ class=${item.id === currentSection!.id ? 'active' : ''}
350
+ role="button"
351
+ tabindex="0"
352
+ @click=${(e: Event) => {
199
353
  e.preventDefault()
200
- // Show the new section
201
354
  showSection(item)
202
- // Notify other plugins
203
355
  editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
356
+ }}
357
+ @keydown=${(e: KeyboardEvent) => {
358
+ if (e.key === 'Enter' || e.key === ' ') {
359
+ e.preventDefault()
360
+ showSection(item)
361
+ editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
362
+ }
204
363
  }}
205
364
  >
206
365
  ${item.label}
@@ -212,47 +371,45 @@ function displaySettings(editor, config, model = editor.getModel()) {
212
371
  ${ sections }
213
372
  </main>
214
373
  </section>
215
- <footer>
374
+ <footer role="contentinfo">
216
375
  <p class="silex-version">Silex ${SILEX_VERSION}</p>
217
- <input class="silex-button" type="button" @click=${e => editor.stopCommand(cmdOpenSettings)} value="Cancel">
218
- <input class="silex-button" type="submit" value="Apply">
376
+ <input class="silex-button" type="button" value="Cancel" @click=${() => editor.stopCommand(cmdOpenSettings)} accesskey="c">
377
+ <input class="silex-button" type="submit" value="Apply" accesskey="a">
219
378
  </footer>
220
379
  </form>
221
380
  `, el)
222
- // Display the current section
223
- showSection(currentSection)
224
- // Init the code editor
225
- el.querySelector(`#${idCodeWrapper}`)?.appendChild(headEditor.getElement())
226
- headEditor.setContent(settings.head || '')
381
+ showSection(currentSection!)
382
+ el.querySelector(`#${idCodeWrapper}`)?.appendChild(headEditor!.getElement())
383
+ headEditor!.setContent(settings.head || '')
227
384
  }
228
385
 
229
- function saveSettings(editor, config, model = editor.getModel()) {
230
- const form = el.querySelector('form')
386
+ function saveSettings(
387
+ editor: Editor,
388
+ config: Record<string, unknown>,
389
+ model: any = editor.getModel()
390
+ ): void {
391
+ const form = el.querySelector('form') as HTMLFormElement
231
392
  const formData = new FormData(form)
232
- const data = Array.from(formData as any)
233
- .reduce((aggregate, [key, value]) => {
234
- // Keep only the values that are set
393
+ const data = Array.from(formData)
394
+ .reduce((aggregate: {[key: string]: any}, [key, value]) => {
235
395
  if(value !== '') {
236
396
  aggregate[key] = value
237
397
  }
238
398
  return aggregate
239
- }, {}) as {[key: string]: any}
240
- // take the name out to the main model (by design in grapesjs pages)
399
+ }, {})
241
400
  const { name, ...settings } = data
242
401
  model.set({
243
402
  settings: {
244
403
  ...data,
245
- head: headEditor.getContent(),
404
+ head: headEditor!.getContent(),
246
405
  },
247
406
  name,
248
407
  })
249
- // save if auto save is on
250
408
  editor.getModel().set('changesCount', editor.getDirtyCount() + 1)
251
- // update the DOM
252
409
  updateDom(editor)
253
410
  }
254
- function getHeadContainer(doc, className) {
255
- const container = doc.head.querySelector(`.${className}`)
411
+ function getHeadContainer(doc: Document, className: string): HTMLElement {
412
+ const container = doc.head.querySelector(`.${className}`) as HTMLElement | null
256
413
  if(container) {
257
414
  return container
258
415
  }
@@ -261,22 +418,16 @@ function getHeadContainer(doc, className) {
261
418
  doc.head.appendChild(newContainer)
262
419
  return newContainer
263
420
  }
264
- function updateDom(editor) {
421
+ function updateDom(editor: Editor): void {
265
422
  const doc = editor.Canvas.getDocument()
266
423
  const settings = editor.getModel().get('settings')
267
- const pageSettings = editor.Pages.getSelected().get('settings')
424
+ const pageSettings = editor.Pages.getSelected().get('settings') as WebsiteSettings
268
425
  if(doc && settings) {
269
- // Delay the update to let the DOM be ready
270
426
  setTimeout(() => {
271
- // Site head
272
427
  getHeadContainer(doc, 'site-head')
273
428
  .innerHTML = settings.head || ''
274
- // Pages head
275
429
  getHeadContainer(doc, 'page-head')
276
430
  .innerHTML = pageSettings?.head || ''
277
431
  })
278
- } else {
279
- // No document??
280
432
  }
281
433
  }
282
-
@@ -87,14 +87,18 @@ export const storagePlugin = (editor: PublishableEditor) => {
87
87
  return data
88
88
  } else {
89
89
  editor.runCommand(cmdPauseAutoSave)
90
- const { pages, ...rest } = data
90
+ const { pages, pagesFolder, ...rest } = data
91
91
  // Load any additional project data, e.g. symbols, but not the ones we progressive load
92
92
  loader && render(getLoaderHtml('Loading styles, assets and symbols', 0, pages.length + 1), loader)
93
93
  // Add to the project, everything but pages
94
94
  editor.loadProjectData(rest)
95
+ editor.getModel().set('pagesFolder', pagesFolder)
95
96
  // Add the pages to the project
96
97
  await progressiveLoadPages(editor, pages)
97
98
  await nextFrame()
99
+ // Trigger symbol update to recount instances now that pages are loaded
100
+ editor.trigger('symbol')
101
+ await nextFrame()
98
102
  // Select the first page
99
103
  const firstPage = editor.Pages.getAll()[0]
100
104
  if (firstPage) editor.Pages.select(firstPage)
@@ -160,6 +164,7 @@ export const storagePlugin = (editor: PublishableEditor) => {
160
164
  if (user) {
161
165
  data.assets = removeTempDataFromAssetUrl(data.assets)
162
166
  data.styles = removeTempDataFromStyles(data.styles)
167
+ data.pagesFolder = editor.getModel().get('pagesFolder')
163
168
  await websiteSave({ websiteId: options.id, connectorId: user.storage.connectorId, data })
164
169
  isSaving = false
165
170
  } else {
@@ -16,12 +16,27 @@
16
16
  *
17
17
  */
18
18
 
19
+ import { Editor } from 'grapesjs'
20
+
19
21
  const pasteBtnClass = 'fa fa-fw fa-paste silex-button'
22
+ const undoBtnClass = 'fa fa-fw fa-rotate-left silex-button'
23
+ const redoBtnClass = 'fa fa-fw fa-rotate-right silex-button'
20
24
  const disabledClass = ' disabled'
21
25
 
22
- export default function(editor) {
23
- // Add copy/paste buttons in the top bar
24
- const [copyBtn, pasteBtn] = editor.Panels.addButton('options', [{
26
+ export default function(editor: Editor) {
27
+ // Add undo/redo and copy/paste buttons in the top bar
28
+ // Add undo/redo and copy/paste buttons in the top bar
29
+ editor.Panels.addButton('options', [{
30
+ id: 'undo',
31
+ className: undoBtnClass + disabledClass,
32
+ command: () => editor.UndoManager.undo(),
33
+ attributes: { title: 'Undo (Ctrl+Z)' },
34
+ }, {
35
+ id: 'redo',
36
+ className: redoBtnClass + disabledClass,
37
+ command: () => editor.UndoManager.redo(),
38
+ attributes: { title: 'Redo (Ctrl+Shift+Z)' },
39
+ }, {
25
40
  id: 'copy',
26
41
  className: 'fa fa-fw fa-copy',
27
42
  command: 'core:copy',
@@ -34,6 +49,27 @@ export default function(editor) {
34
49
  active: false,
35
50
  }])
36
51
 
52
+ // Get buttons after creation
53
+ const undoBtn = editor.Panels.getButton('options', 'undo')
54
+ const redoBtn = editor.Panels.getButton('options', 'redo')
55
+ const pasteBtn = editor.Panels.getButton('options', 'paste')
56
+
57
+ // Update undo/redo button states based on UndoManager
58
+ const updateUndoRedoButtons = () => {
59
+ const hasUndo = editor.UndoManager.hasUndo()
60
+ const hasRedo = editor.UndoManager.hasRedo()
61
+ undoBtn.set('className', undoBtnClass + (hasUndo ? '' : disabledClass))
62
+ redoBtn.set('className', redoBtnClass + (hasRedo ? '' : disabledClass))
63
+ }
64
+
65
+ // Listen for changes in the undo stack
66
+ editor.on('change:changesCount', updateUndoRedoButtons)
67
+ editor.on('undo', updateUndoRedoButtons)
68
+ editor.on('redo', updateUndoRedoButtons)
69
+
70
+ // Initial state
71
+ updateUndoRedoButtons()
72
+
37
73
  // Disable paste button if clipboard is empty
38
74
  editor.EditorModel.on('change:clipboard', (model, value) => {
39
75
  const hasClipboard = value && value.length > 0
@@ -21,8 +21,10 @@ import { Component, Editor } from 'grapesjs'
21
21
  export function onAll(editor: Editor, cbk: (c: Component) => void) {
22
22
  editor.Pages.getAll()
23
23
  .forEach(page => {
24
- page.getMainComponent()
25
- .onAll(c => cbk(c))
24
+ const mainComponent = page.getMainComponent()
25
+ if (mainComponent) {
26
+ mainComponent.onAll(c => cbk(c))
27
+ }
26
28
  })
27
29
  }
28
30
 
@@ -76,3 +78,14 @@ export function titleCase(str: string, sep: string = ' '): string {
76
78
  const split = str.split(sep)
77
79
  return split.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(sep)
78
80
  }
81
+
82
+ export function debounce<T extends (...args: any[]) => any>(func: T, wait = 100): (...args: Parameters<T>) => void {
83
+ let timeout: ReturnType<typeof setTimeout> | null = null
84
+
85
+ return function(this: any, ...args: Parameters<T>) {
86
+ if (timeout) clearTimeout(timeout)
87
+ timeout = setTimeout(() => {
88
+ func.apply(this, args)
89
+ }, wait)
90
+ }
91
+ }
@@ -21,7 +21,8 @@
21
21
 
22
22
  export const WEBSITE_DATA_FILE = 'website.json'
23
23
  export const WEBSITE_META_DATA_FILE = 'meta.json'
24
- export const WEBSITE_PAGES_FOLDER = 'src'
24
+ export const LEGACY_WEBSITE_PAGES_FOLDER = 'src' // This is used in legacy websites, check the value in EMPTY_WEBSITE
25
+ export const WEBSITE_PAGES_FOLDER = 'pages' // This is the default now, if not specified in the website data
25
26
 
26
27
  export const DEFAULT_WEBSITE_ID = 'default'
27
28
  export const DEFAULT_LANGUAGE = 'en'
@@ -15,7 +15,7 @@
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
- throw new Error('This file is obsolete, templates have been replaced by Silex CMS plugin: https://github.com/silexlabs/silex-cms/')
18
+ throw new Error('This file is obsolete, templates have been replaced by the built-in Silex CMS feature')
19
19
 
20
20
  // Silex serves /clients
21
21
  // @ts-ignore
@@ -35,6 +35,7 @@ const dummySession = {
35
35
  const dummyId: WebsiteId = 'dummy id'
36
36
  const dummyWebsite: WebsiteData = {
37
37
  pages: [],
38
+ pagesFolder: 'pages/',
38
39
  assets: [],
39
40
  styles: [],
40
41
  settings: {},