@silexlabs/silex 3.0.0-alpha.151 → 3.0.0-alpha.152

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.
@@ -23,6 +23,8 @@ import openImport from './openImport'
23
23
  * Handle plugins, options and initialization of the editor
24
24
  */
25
25
 
26
+ const notificationContainer = document.createElement('div')
27
+
26
28
  // ////////////////////
27
29
  // Plugins
28
30
  // ////////////////////
@@ -45,10 +47,11 @@ import rateLimitPlugin from '@silexlabs/grapesjs-storage-rate-limit'
45
47
  import borderPugin from 'grapesjs-style-border'
46
48
  import backgroundPlugin from 'grapesjs-style-bg'
47
49
  import resizePanelPlugin from './resize-panel'
50
+ import notificationsPlugin, { NotificationEditor } from '@silexlabs/grapesjs-notifications'
48
51
 
49
52
  import { pagePanelPlugin, cmdTogglePages, cmdAddPage } from './page-panel'
50
53
  import { newPageDialog, cmdOpenNewPageDialog } from './new-page-dialog'
51
- import { projectBarPlugin } from './project-bar'
54
+ import { PROJECT_BAR_PANEL_ID, projectBarPlugin } from './project-bar'
52
55
  import { settingsDialog, cmdOpenSettings } from './settings'
53
56
  import { blocksPlugin } from './blocks'
54
57
  import { semanticPlugin } from './semantic'
@@ -90,7 +93,8 @@ const plugins = [
90
93
  {name: './footer', value: footerPlugin},
91
94
  {name: '@silexlabs/grapesjs-storage-rate-limit', value: rateLimitPlugin},
92
95
  {name: 'grapesjs-style-border', value: borderPugin},
93
- {name: './resize-panel', value: resizePanelPlugin}
96
+ {name: './resize-panel', value: resizePanelPlugin},
97
+ {name: '@silexlabs/grapesjs-notifications', value: notificationsPlugin},
94
98
  ]
95
99
  // Check that all plugins are loaded correctly
96
100
  plugins
@@ -201,9 +205,9 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
201
205
  buttons: [
202
206
  {
203
207
  id: 'symbol-create-button',
204
- className: 'pages__add-page fa-solid fa-plus',
205
- label: 'Create symbol from selection',
208
+ className: 'gjs-pn-btn',
206
209
  command: cmdPromptAddSymbol,
210
+ text: '\u271A',
207
211
  },
208
212
  ],
209
213
  }, {
@@ -212,9 +216,9 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
212
216
  attributes: { title: 'Pages', containerClassName: 'page-panel-container', },
213
217
  command: cmdTogglePages,
214
218
  buttons: [{
215
- className: 'pages__add-page fa fa-file',
219
+ className: 'gjs-pn-btn',
216
220
  command: cmdAddPage,
217
- text: '+',
221
+ text: '\u271A',
218
222
  }],
219
223
  }, {
220
224
  id: 'layer-manager-btn',
@@ -237,6 +241,16 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
237
241
  id: 'spacer',
238
242
  attributes: {},
239
243
  className: 'project-bar-spacer',
244
+ }, {
245
+ id: 'notifications-btn',
246
+ className: 'notifications-btn fa-regular fa-bell',
247
+ attributes: { title: 'Notifications', containerClassName: 'notifications-container', },
248
+ command: 'open-notifications',
249
+ buttons: [{
250
+ className: 'gjs-pn-btn',
251
+ command: 'notifications:clear',
252
+ text: '\u2716',
253
+ }],
240
254
  }, {
241
255
  id: 'dash2',
242
256
  className: 'fa-solid fa-house',
@@ -275,10 +289,6 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
275
289
  [filterStyles]: {
276
290
  appendBefore: '.gjs-sm-sectors',
277
291
  },
278
- [internalLinksPlugin.toString()]: {
279
- // FIXME: warn the user about links in error
280
- onError: (errors) => console.warn('Links errors:', errors),
281
- },
282
292
  [codePlugin.toString()]: {
283
293
  blockLabel: 'HTML',
284
294
  blockCustomCode: {
@@ -308,6 +318,10 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
308
318
  [imgPlugin.toString()]: {
309
319
  replacedElements: config.replacedElements,
310
320
  },
321
+ [notificationsPlugin.toString()]: {
322
+ container: notificationContainer,
323
+ reverse: true,
324
+ },
311
325
  },
312
326
  }
313
327
  }
@@ -344,6 +358,26 @@ export async function initEditor(config: EditorConfig) {
344
358
  modalImportTitle: 'Import from website',
345
359
  }))
346
360
 
361
+ // Detect loading errors
362
+ // Display a useful notification
363
+ const typeConfig = {
364
+ view: {
365
+ onRender({editor, el, model}) {
366
+ const src = model.getAttributes().src
367
+ el.addEventListener('error', () => {
368
+ editor.runCommand('notifications:add', {
369
+ type: 'error',
370
+ group: 'Image loading error',
371
+ message: `Error loading image: ${src}`,
372
+ componentId: model.getId(),
373
+ })
374
+ })
375
+ },
376
+ },
377
+ }
378
+ editor.DomComponents.addType('image', typeConfig)
379
+ editor.DomComponents.addType('iframe', typeConfig)
380
+
347
381
  // Adjustments to do when the editor is ready
348
382
  editor.on('load', () => {
349
383
  const views = editor.Panels.getPanel('views')
@@ -360,9 +394,25 @@ export async function initEditor(config: EditorConfig) {
360
394
  // Render the block manager, otherwise it is empty
361
395
  editor.BlockManager.render(null)
362
396
 
363
- // use the style filter plugin
397
+ // Use the style filter plugin
364
398
  editor.StyleManager.addProperty('extra', { extend: 'filter' })
365
399
 
400
+ // Add a class to the Style Manager's sticky top section
401
+ editor.SelectorManager.selectorTags.el.parentElement.classList.add('top-style-section')
402
+
403
+ // Add the notifications container
404
+ document.body.querySelector('.notifications-container').appendChild(notificationContainer)
405
+ // Mark the button as dirty when there are notifications
406
+ // TODO: move this in the notifications plugin options
407
+ editor.on(
408
+ 'notifications:changed',
409
+ () => {
410
+ const notificationButton = editor.Panels.getPanel(PROJECT_BAR_PANEL_ID).view?.el.querySelector('.notifications-btn')
411
+ ;(editor as NotificationEditor)
412
+ .NotificationManager.length ? notificationButton.classList.add('project-bar__dirty') : notificationButton.classList.remove('project-bar__dirty')
413
+ }
414
+ )
415
+
366
416
  // GrapesJs editor is ready
367
417
  resolve(editor)
368
418
  })
@@ -60,7 +60,17 @@ export const internalLinksPlugin = (editor, opts) => {
60
60
  issues.push(component)
61
61
  }
62
62
  })
63
- opts?.onError(issues)
63
+ // Add a notification for the issues
64
+ if(issues.length) {
65
+ issues.forEach(component => {
66
+ editor.runCommand('notifications:add', {
67
+ type: 'error',
68
+ message: `Page ${page.previous('name')} was removed, please fix the following links:`,
69
+ componentId: component.getId(),
70
+ group: 'Broken internal links',
71
+ })
72
+ })
73
+ }
64
74
  break
65
75
  }
66
76
  })
@@ -18,8 +18,8 @@
18
18
  import { Editor, Panel } from 'grapesjs'
19
19
  import {html, render} from 'lit-html'
20
20
 
21
- const panelId = 'project-bar-panel'
22
- const containerPanelId = 'project-bar-container'
21
+ export const PROJECT_BAR_PANEL_ID = 'project-bar-panel'
22
+ export const containerPanelId = 'project-bar-container'
23
23
 
24
24
  export interface PanelObject {
25
25
  command: string | ((editor: Editor) => void)
@@ -34,6 +34,7 @@ export interface PanelObject {
34
34
  text: string
35
35
  className: string
36
36
  }[]
37
+ onClick?: (editor: Editor) => void
37
38
  }
38
39
 
39
40
  export const projectBarPlugin = (editor, opts) => {
@@ -44,7 +45,7 @@ export const projectBarPlugin = (editor, opts) => {
44
45
  })
45
46
  // create the project bar panel in grapesjs
46
47
  editor.Panels.addPanel({
47
- id: panelId,
48
+ id: PROJECT_BAR_PANEL_ID,
48
49
  buttons: opts.panels,
49
50
  visible : true,
50
51
  })
@@ -113,6 +114,10 @@ export function addButton(editor: Editor, panel: PanelObject) {
113
114
  document.body.classList.add('silex-squeeze-left')
114
115
  editor.refresh()
115
116
  }
117
+ // Call the onClick function if it exists
118
+ if(panel.onClick) panel.onClick(editor)
119
+ // Remove the dirty flag
120
+ el.classList.remove('project-bar__dirty')
116
121
  },
117
122
  stop() {
118
123
  if(panel.attributes.containerClassName) {
@@ -121,6 +126,10 @@ export function addButton(editor: Editor, panel: PanelObject) {
121
126
  document.body.classList.remove('silex-squeeze-left')
122
127
  editor.refresh()
123
128
  }
129
+ // Call the onClick function if it exists
130
+ if(panel.onClick) panel.onClick(editor)
131
+ // Remove the dirty flag
132
+ el.classList.remove('project-bar__dirty')
124
133
  },
125
134
  })
126
135
  }
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import {html, render} from 'lit-html'
19
- import {defaultSections, idCodeWrapper, isSite} from './settings-sections'
19
+ import {defaultSections, idCodeWrapper, isSite, SettingsSection} from './settings-sections'
20
20
 
21
21
  /**
22
22
  * @fileoverview This file contains the settings dialog. The config API lets plugins add sections to the settings dialog.
@@ -39,6 +39,7 @@ export const settingsDialog = (editor, opts) => {
39
39
  console.info('Silex version:', SILEX_VERSION)
40
40
  editor.Commands.add(cmdOpenSettings, {
41
41
  run: (_, sender, {page}) => {
42
+ console.log('open settings', page)
42
43
  modal = editor.Modal.open({
43
44
  title: page ? 'Page settings' : 'Site Settings',
44
45
  content: '',
@@ -101,6 +102,37 @@ export const settingsDialog = (editor, opts) => {
101
102
  })
102
103
  }
103
104
 
105
+ function showSection(item: SettingsSection) {
106
+ const aside = el.querySelector('aside.silex-bar') as HTMLElement
107
+ const ul = aside.querySelector('ul.silex-list--menu') as HTMLUListElement
108
+ const li = ul.querySelector('li#settings-' + item.id) as HTMLLIElement
109
+ currentSection = item
110
+ console.log('show section', item.id, {aside, ul, li})
111
+ if(!li) {
112
+ console.warn('Cannot find section', item.id, 'in the side bar, fallback to the first section')
113
+ showSection(defaultSections[0]) // Fallback to the first section
114
+ return
115
+ }
116
+ // Update active
117
+ Array.from(ul.querySelectorAll('.active')).forEach(el => el.classList.remove('active'))
118
+ li.classList.add('active')
119
+ const main = el.querySelector('main#settings__main') as HTMLElement
120
+ const mainItem = el.querySelector(`div#settings-${item.id}`)
121
+ if(!mainItem) {
122
+ console.warn('Cannot find section', item.id, 'in the settings dialog, fallback to the first section')
123
+ showSection(defaultSections[0]) // Fallback to the first section
124
+ return
125
+ }
126
+ // Update hidden
127
+ Array.from(main.querySelectorAll('.silex-hideable')).forEach(el => el.classList.add('silex-hidden'))
128
+ mainItem.classList.remove('silex-hidden')
129
+ // This messes up with the save / cancel mechanism
130
+ // displaySettings(editor, config, model)
131
+ // Refresh the code editor just in case it went from hidden to visible
132
+ // This makes it ready to be used when the user clicks on the tab
133
+ headEditor.refresh()
134
+ }
135
+
104
136
  export function addSection(section, siteOrPage: 'site' | 'page', position: 'first' | 'last' | number) {
105
137
  const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
106
138
  if (position === 'first') {
@@ -122,6 +154,7 @@ export function removeSection(id, siteOrPage: 'site' | 'page') {
122
154
  }
123
155
 
124
156
 
157
+ let currentSection
125
158
  function displaySettings(editor, config, model = editor.getModel()) {
126
159
  // Update the model with the current settings
127
160
  const settings = model.get('settings') || {} as WebsiteSettings
@@ -129,7 +162,8 @@ function displaySettings(editor, config, model = editor.getModel()) {
129
162
  // Get the current sections for page or site
130
163
  const menuItemsCurrent = isSite(model) ? sectionsSite : sectionsPage
131
164
  // Init the current section selection
132
- let sections = menuItemsCurrent[0]
165
+ currentSection = currentSection || menuItemsCurrent[0]
166
+ // Render the settings dialog
133
167
  render(html`
134
168
  <form class="silex-form">
135
169
  <div class="silex-help">
@@ -146,25 +180,12 @@ function displaySettings(editor, config, model = editor.getModel()) {
146
180
  <ul class="silex-list silex-list--menu">
147
181
  ${menuItemsCurrent.map(item => html`
148
182
  <li
149
- class=${item.id === sections.id ? 'active' : ''}
183
+ id="settings-${item.id}"
184
+ class=${item.id === currentSection.id ? 'active' : ''}
150
185
  @click=${e => {
151
186
  e.preventDefault()
152
- sections = item
153
- const li = e.target as HTMLElement
154
- const ul = li.closest('ul')
155
- const section = li.closest('section')
156
- const mainItem = section.querySelector(`#settings-${item.id}`)
157
- // Update active
158
- Array.from(ul.querySelectorAll('.active')).forEach(el => el.classList.remove('active'))
159
- li.classList.add('active')
160
- // Update hidden
161
- Array.from(section.querySelectorAll('.silex-hideable')).forEach(el => el.classList.add('silex-hidden'))
162
- mainItem.classList.remove('silex-hidden')
163
- // This messes up with the save / cancel mechanism
164
- // displaySettings(editor, config, model)
165
- // Refresh the code editor just in case it went from hidden to visible
166
- // This makes it ready to be used when the user clicks on the tab
167
- headEditor.refresh()
187
+ // Show the new section
188
+ showSection(item)
168
189
  // Notify other plugins
169
190
  editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
170
191
  }}
@@ -174,7 +195,7 @@ function displaySettings(editor, config, model = editor.getModel()) {
174
195
  `)}
175
196
  </ul>
176
197
  </aside>
177
- <main>
198
+ <main id="settings__main">
178
199
  ${menuItemsCurrent.map(item => item.render(settings, model))}
179
200
  </main>
180
201
  </section>
@@ -185,6 +206,9 @@ function displaySettings(editor, config, model = editor.getModel()) {
185
206
  </footer>
186
207
  </form>
187
208
  `, el)
209
+ // Display the current section
210
+ showSection(currentSection)
211
+ // Init the code editor
188
212
  el.querySelector(`#${idCodeWrapper}`)?.appendChild(headEditor.getElement())
189
213
  headEditor.setContent(settings.head || '')
190
214
  }
@@ -38,6 +38,7 @@ export interface GitlabOptions {
38
38
  //metaRepoFile: string
39
39
  repoPrefix: string
40
40
  scope: string
41
+ domain: string
41
42
  }
42
43
 
43
44
  interface GitlabToken {
@@ -189,6 +190,8 @@ export default class GitlabConnector implements StorageConnector {
189
190
  } as GitlabOptions
190
191
  if(!this.options.clientId) throw new Error('Missing Gitlab client ID')
191
192
  if(!this.options.clientSecret) throw new Error('Missing Gitlab client secret')
193
+ if(!this.options.domain) throw new Error('Missing Gitlab domain')
194
+
192
195
  }
193
196
 
194
197
  // **
@@ -224,7 +227,7 @@ export default class GitlabConnector implements StorageConnector {
224
227
  async readFile(session: GitlabSession, websiteId: string, fileName: string): Promise<Buffer> {
225
228
  const safePath = fileName.replace(/^\//, '')
226
229
  // Call the API
227
- const url = `https://gitlab.com/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${session.gitlab?.token?.access_token}`
230
+ const url = `${this.options.domain}/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${session.gitlab?.token?.access_token}`
228
231
  const response = await fetch(url, {
229
232
  method: 'GET',
230
233
  headers: {
@@ -273,7 +276,7 @@ export default class GitlabConnector implements StorageConnector {
273
276
  const token = session?.gitlab?.token
274
277
  const tokenParam = token ? `access_token=${token.access_token}&` : ''
275
278
  const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent((v as any).toString())}`).join('&')
276
- const url = `https://gitlab.com/${path}?${tokenParam}${paramsStr}`
279
+ const url = `${this.options.domain}/${path}?${tokenParam}${paramsStr}`
277
280
  const headers = {
278
281
  'Content-Type': 'application/json',
279
282
  }
@@ -304,7 +307,7 @@ export default class GitlabConnector implements StorageConnector {
304
307
  client_id: this.options.clientId,
305
308
  client_secret: this.options.clientSecret,
306
309
  }
307
- const response = await fetch('https://gitlab.com/oauth/token', {
310
+ const response = await fetch(this.options.domain + '/oauth/token', {
308
311
  method: 'POST',
309
312
  headers: {
310
313
  'Content-Type': 'application/json',
@@ -391,7 +394,7 @@ export default class GitlabConnector implements StorageConnector {
391
394
  codeVerifier,
392
395
  codeChallenge,
393
396
  }
394
- return `https://gitlab.com/oauth/authorize?client_id=${this.options.clientId}&redirect_uri=${redirect_uri}&response_type=code&state=${session.gitlab.state}&scope=${this.options.scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`
397
+ return `${this.options.domain}/oauth/authorize?client_id=${this.options.clientId}&redirect_uri=${redirect_uri}&response_type=code&state=${session.gitlab.state}&scope=${this.options.scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`
395
398
  }
396
399
 
397
400
  getOptions(formData: object): object {
@@ -420,7 +423,7 @@ export default class GitlabConnector implements StorageConnector {
420
423
  if(!session.gitlab?.codeVerifier) throw new ApiError('Missing code verifier', 401)
421
424
  if(!session.gitlab?.codeChallenge) throw new ApiError('Missing code challenge', 401)
422
425
 
423
- const response = await fetch('https://gitlab.com/oauth/token', {
426
+ const response = await fetch(this.options.domain + '/oauth/token', {
424
427
  method: 'POST',
425
428
  headers: {
426
429
  'Content-Type': 'application/json',