@silexlabs/silex 3.3.1-0 → 3.3.1-2

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.
@@ -42,6 +42,8 @@ import loginDialogPlugin, { LoginDialogOptions, cmdLogout } from './LoginDialog'
42
42
  import footerPlugin from './footer'
43
43
  import breadcrumbsPlugin from './breadcrumbs'
44
44
  import imgPlugin from './img'
45
+ import liPlugin from './li'
46
+ import flexPlugin from './flex'
45
47
  import cssPropsPlugin from './css-props'
46
48
  import rateLimitPlugin from '@silexlabs/grapesjs-storage-rate-limit'
47
49
  import borderPugin from 'grapesjs-style-border'
@@ -94,6 +96,8 @@ const plugins = [
94
96
  {name: '@silexlabs/grapesjs-loading', value: loadingPlugin},
95
97
  {name: './breadcrumbs', value: breadcrumbsPlugin},
96
98
  {name: './img', value: imgPlugin},
99
+ {name: './li', value: liPlugin},
100
+ {name: './flex', value: flexPlugin},
97
101
  {name: './css-props', value: cssPropsPlugin},
98
102
  {name: './footer', value: footerPlugin},
99
103
  {name: '@silexlabs/grapesjs-storage-rate-limit', value: rateLimitPlugin},
@@ -334,6 +338,8 @@ export function getEditorConfig(config: ClientConfig): EditorConfig {
334
338
  },
335
339
  [imgPlugin.toString()]: {
336
340
  replacedElements: config.replacedElements,
341
+ websiteId,
342
+ storageId,
337
343
  },
338
344
  [notificationsPlugin.toString()]: {
339
345
  container: notificationContainer,
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Silex website builder, free/libre no-code tool for makers.
3
+ * Copyright (c) 2023 lexoyo and Silex Labs foundation
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+ import { Editor } from 'grapesjs'
18
+ import { SectorConfig, registerSector } from './sectors'
19
+
20
+ /**
21
+ * @fileoverview This plugin adds CSS properties for the UL, OL and LI elements
22
+ *
23
+ */
24
+
25
+ export default (editor: Editor) => {
26
+ const sectorConfigs: SectorConfig[] = [
27
+ {
28
+ name: 'List',
29
+ props: [
30
+ {
31
+ id: 'list-style-type',
32
+ name: 'List Style Type',
33
+ property: 'list-style-type',
34
+ type: 'select',
35
+ options: [
36
+ { id: '', label: '' },
37
+ { id: 'disc', label: 'disc' },
38
+ { id: 'circle', label: 'circle' },
39
+ { id: 'square', label: 'square' },
40
+ { id: 'decimal', label: 'decimal' },
41
+ { id: 'decimal-leading-zero', label: 'decimal-leading-zero' },
42
+ { id: 'lower-roman', label: 'lower-roman' },
43
+ { id: 'upper-roman', label: 'upper-roman' },
44
+ { id: 'lower-alpha', label: 'lower-alpha' },
45
+ { id: 'upper-alpha', label: 'upper-alpha' },
46
+ { id: 'none', label: 'none' },
47
+ ],
48
+ defaults: '',
49
+ },
50
+ {
51
+ id: 'list-style-position',
52
+ name: 'List Style Position',
53
+ property: 'list-style-position',
54
+ type: 'select',
55
+ options: [
56
+ { id: '', label: '' },
57
+ { id: 'outside', label: 'outside' },
58
+ { id: 'inside', label: 'inside' },
59
+ ],
60
+ defaults: '',
61
+ },
62
+ {
63
+ id: 'list-style-image',
64
+ name: 'List Style Image',
65
+ property: 'list-style-image',
66
+ type: 'text',
67
+ defaults: '',
68
+ },
69
+ ],
70
+ shouldShow: comp => ['ul', 'ol', 'li'].includes(comp.get('tagName')?.toLowerCase() || ''),
71
+ },
72
+ ]
73
+
74
+ editor.on('load', () => {
75
+ sectorConfigs.forEach(sectorConfig => registerSector(editor, sectorConfig))
76
+ })
77
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Silex website builder, free/libre no-code tool for makers.
3
+ * Copyright (c) 2023 lexoyo and Silex Labs foundation
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+ import { Component, Editor } from 'grapesjs'
18
+
19
+ /**
20
+ * @fileoverview
21
+ *
22
+ */
23
+
24
+ export type SectorConfig = {
25
+ name: string
26
+ props: any[]
27
+ shouldShow: (comp: Component) => boolean
28
+ }
29
+
30
+ export function registerSector(editor: Editor, config: SectorConfig, at?: number) {
31
+ const sm = editor.StyleManager
32
+ let sector = sm.getSector(config.name)
33
+ if (!sector) {
34
+ sector = sm.addSector(config.name, { name: config.name, open: false, visible: false }, { at })
35
+ }
36
+
37
+ // Add props if not already present
38
+ const existingProps = sector.get('properties')//.map(p => p.get('property'))
39
+ config.props.forEach(p => {
40
+ if (!existingProps.includes(p.property)) {
41
+ sector.addProperty(p, {})
42
+ }
43
+ })
44
+
45
+ // Keep sector in sync with internal visibility state
46
+ sector.on('change:visible', () => {
47
+ const shouldBeVisible = sector.get('visible')
48
+ if (shouldBeVisible !== config.shouldShow(editor.getSelected() as Component)) {
49
+ sector.set('visible', !shouldBeVisible)
50
+ }
51
+ })
52
+
53
+ editor.on('component:selected', comp => {
54
+ const shouldShow = config.shouldShow(comp)
55
+ if (sector.get('visible') !== shouldShow) {
56
+ sector.set('visible', shouldShow)
57
+ }
58
+ })
59
+ }
@@ -21,94 +21,168 @@ import {map} from 'lit-html/directives/map.js'
21
21
  // constants
22
22
  const pluginName = 'semantic'
23
23
 
24
- const tags = [
25
- 'DIV',
26
- 'P',
27
- 'H1',
28
- 'H2',
29
- 'H3',
30
- 'H4',
31
- 'H5',
32
- 'H6',
33
- 'SPAN',
34
- 'MAIN',
35
- 'ASIDE',
36
- 'SECTION',
37
- 'ADDRESS',
38
- 'ARTICLE',
39
- 'BUTTON',
40
- 'NAV',
41
- 'HEADER',
42
- 'FOOTER',
43
- 'DETAILS',
44
- 'SUMMARY',
45
- 'PRE',
46
- 'BLOCKQUOTE',
24
+ // Group tags by category
25
+ const tagCategories = [
26
+ {
27
+ label: 'Containers',
28
+ tags: ['DIV', 'SPAN', 'P'],
29
+ },
30
+ {
31
+ label: 'Headings',
32
+ tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
33
+ },
34
+ {
35
+ label: 'Main document structure',
36
+ tags: ['MAIN', 'SECTION', 'ARTICLE', 'NAV', 'ASIDE', 'HEADER', 'FOOTER'],
37
+ },
38
+ {
39
+ label: 'Content grouping',
40
+ tags: ['ADDRESS', 'BLOCKQUOTE', 'PRE'],
41
+ },
42
+ {
43
+ label: 'Interactive/form',
44
+ tags: ['BUTTON', 'LABEL', 'DETAILS', 'SUMMARY'],
45
+ },
47
46
  ]
48
47
 
48
+ // Flatten all tags for lookup
49
+ const tags = tagCategories.flatMap(cat => cat.tags)
50
+
49
51
  // plugin code
50
52
  export const semanticPlugin = (editor, opts) => {
51
- // Add the new trait to all component types
52
- editor.DomComponents.getTypes().map(type => {
53
+ // Helper to merge extra traits into a type's default traits
54
+ function withExtraTraits(type, extraTraits) {
55
+ const origTraits = editor.DomComponents.getType(type.id).model.prototype.defaults.traits
56
+ return [
57
+ ...origTraits,
58
+ ...extraTraits,
59
+ ]
60
+ }
61
+
62
+ // Traits to add to all components
63
+ const extraTraits = [
64
+ {
65
+ label: 'Tag name',
66
+ type: 'tag-name',
67
+ name: 'tag-name',
68
+ },
69
+ {
70
+ label: 'For',
71
+ name: 'for',
72
+ type: 'for-trait',
73
+ placeholder: 'ID of input',
74
+ }
75
+ ]
76
+
77
+ // Add the extra traits to all component types
78
+ editor.DomComponents.getTypes().forEach(type => {
53
79
  editor.DomComponents.addType(type.id, {
54
80
  model: {
55
81
  defaults: {
56
- traits: [
57
- // Keep the type original traits
58
- ...editor.DomComponents.getType(type.id).model.prototype.defaults.traits,
59
- // Add the new trait
60
- {
61
- label: 'Tag name',
62
- type: 'tag-name',
63
- name: 'tag-name',
64
- },
65
- ]
82
+ traits: withExtraTraits(type, extraTraits),
66
83
  }
67
84
  }
68
85
  })
69
86
  })
70
87
 
71
- function doRender(el: HTMLElement, tagName: string) {
72
- const tagsWithCurrent = tags.includes(tagName.toUpperCase()) ? tags : tags.concat(tagName.toUpperCase())
73
- render(html`
74
- <label for="semantic__select" class="gjs-one-bg silex-label">Type</label>
75
- <select id="semantic__select" @change=${event => doRender(el, event.target.value)}>
76
- ${map<string>(tagsWithCurrent, tag => html`
77
- <option value="${tag}" ?selected=${tagName.toUpperCase() === tag}>${tag}</option>
78
- `)}
79
- </select>
80
- `, el)
88
+ // Shared render function for select and for-trait
89
+ function renderTrait(el: HTMLElement, opts: {
90
+ tagName?: string,
91
+ forAttr?: string,
92
+ type: 'tag' | 'for'
93
+ }) {
94
+ const tagName = (opts.tagName || '').toUpperCase()
95
+ if (opts.type === 'tag') {
96
+ // Render tag select
97
+ let categoriesWithCurrent = tagCategories.map(cat => ({
98
+ ...cat,
99
+ tags: cat.tags.slice(),
100
+ }))
101
+ if (tagName && !tags.includes(tagName)) {
102
+ categoriesWithCurrent = [
103
+ ...categoriesWithCurrent,
104
+ { label: 'Other', tags: [tagName] }
105
+ ]
106
+ }
107
+ render(html`
108
+ <select @change=${event => renderTrait(el, { tagName: event.target.value, type: 'tag' })}>
109
+ ${map(categoriesWithCurrent, cat => html`
110
+ <optgroup label="${cat.label}">
111
+ ${map(cat.tags, tag => html`
112
+ <option value="${tag}" ?selected=${tagName === tag}>${tag}</option>
113
+ `)}
114
+ </optgroup>
115
+ `)}
116
+ </select>
117
+ `, el)
118
+ } else if (opts.type === 'for') {
119
+ // Render "for" input, hide if not LABEL
120
+ const wrapper = el.closest('.gjs-trt-trait__wrp-for') as HTMLElement
121
+ if (tagName !== 'LABEL') {
122
+ if(wrapper) wrapper.style.display = 'none'
123
+ render(html``, el)
124
+ } else {
125
+ if(wrapper) wrapper.style.display = 'initial'
126
+ render(html`
127
+ <input type="text" placeholder="ID of input" value="${opts.forAttr || ''}"
128
+ @input=${event => renderTrait(el, { forAttr: (event.target as HTMLInputElement).value, tagName, type: 'for' })}
129
+ >
130
+ `, el)
131
+ }
132
+ }
81
133
  }
134
+
82
135
  function doRenderCurrent(el: HTMLElement) {
83
- doRender(el, editor.getSelected()?.get('tagName') || '')
136
+ renderTrait(el, { tagName: editor.getSelected()?.get('tagName') || '', type: 'tag' })
137
+ }
138
+ function doRenderCurrentFor(el: HTMLElement) {
139
+ const selected = editor.getSelected()
140
+ renderTrait(el, {
141
+ tagName: selected?.get('tagName') || '',
142
+ forAttr: selected?.getAttributes().for || '',
143
+ type: 'for'
144
+ })
84
145
  }
85
146
 
86
147
  // Add semantic traits
87
148
  // inspired by https://github.com/olivmonnier/grapesjs-plugin-header/blob/master/src/components.js
88
149
  editor.TraitManager.addType('tag-name', {
89
- createInput({ trait }) {
90
- // Create a new element container and add some content
150
+ createInput({ trait, component }) {
91
151
  const el = document.createElement('div')
92
- // update the UI when a page is added/renamed/removed
93
152
  editor.on('page', () => doRenderCurrent(el))
94
153
  doRenderCurrent(el)
95
- // this will be the element passed to onEvent and onUpdate
96
154
  return el
97
155
  },
98
- // Update the component based on UI changes
99
- // `elInput` is the result HTMLElement you get from `createInput`
100
156
  onEvent({ elInput, component, event }) {
101
- const value = elInput.querySelector('#semantic__select').value
102
- if(component.get('tagName').toUpperCase() === value.toUpperCase()){
103
- // Already done
104
- } else {
157
+ const value = (event.target as HTMLSelectElement).value
158
+ if(component.get('tagName').toUpperCase() !== value.toUpperCase()){
105
159
  component.set('tagName', value)
106
160
  }
107
161
  },
108
- // Update UI on the component change
109
162
  onUpdate({ elInput, component }) {
110
163
  const tagName = component.get('tagName')
111
- doRender(elInput, tagName)
164
+ renderTrait(elInput, { tagName, type: 'tag' })
165
+ },
166
+ })
167
+
168
+ editor.TraitManager.addType('for-trait', {
169
+ createInput({ trait, component }) {
170
+ const el = document.createElement('div')
171
+ editor.on('page', () => doRenderCurrentFor(el))
172
+ editor.on('component:update', () => doRenderCurrentFor(el))
173
+ doRenderCurrentFor(el)
174
+ return el
175
+ },
176
+ onEvent({ elInput, component, event }) {
177
+ const value = (event.target as HTMLInputElement).value
178
+ if(component.getAttributes().for !== value){
179
+ component.setAttributes({ for: value })
180
+ }
181
+ },
182
+ onUpdate({ elInput, component }) {
183
+ const forAttr = component.getAttributes().for || ''
184
+ const tagName = component.get('tagName') || ''
185
+ renderTrait(elInput, { forAttr, tagName, type: 'for' })
112
186
  },
113
187
  })
114
188
  }
@@ -15,21 +15,25 @@
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
+ import { Component, Editor } from 'grapesjs'
19
+
18
20
  export const cmdPromptAddSymbol = 'symbol-prompt-add'
19
21
 
20
- export default (editor, opts) => {
21
- function getNext(prefix) {
22
+ export default (editor: Editor) => {
23
+ function getNext(prefix: string) {
22
24
  let idx = 1
23
- while(editor.Symbols.find(s => s.get('label') === prefix + idx)) {
25
+ while(
26
+ editor.Components.getSymbols()
27
+ .find((symbol: Component) => symbol.get('custom-name') === prefix + idx)
28
+ ) {
24
29
  idx++
25
30
  }
26
31
  return prefix + idx
27
32
  }
28
33
  editor.Commands.add(cmdPromptAddSymbol, {
29
- run: (_, sender) => {
34
+ run: (_) => {
30
35
  const label = prompt('Label', getNext('Symbol '))
31
- const icon = 'fa-gem'
32
- editor.runCommand('symbols:add', { label, icon })
36
+ editor.runCommand('symbols:add', { label })
33
37
  }
34
38
  })
35
39
  }
@@ -21,13 +21,11 @@
21
21
  *
22
22
  */
23
23
 
24
- import { Component, ComponentView } from 'grapesjs'
25
24
  import { DEV_MESSAGE } from '../constants'
26
25
  import { ClientConfig } from './config'
27
26
  import { ClientEvent } from './events'
28
27
  import { initEditor, getEditor } from './grapesjs/index'
29
28
  import { cmdPauseAutoSave } from './grapesjs/storage'
30
- import { displayedToStored, storedToDisplayed } from './assetUrl'
31
29
 
32
30
  // Expose API to calling app as window.silex
33
31
  export * from './expose'
@@ -85,42 +83,6 @@ export async function start(options = {}): Promise<void> {
85
83
  // Add default plugins
86
84
  await config.addDefaultPlugins()
87
85
 
88
- const domc = editor.DomComponents
89
- const imgType = domc.getType('image')
90
- domc.addType('image', {
91
- ...imgType,
92
- view: {
93
- ...imgType.view,
94
- onRender() {
95
- const view = this as ComponentView
96
- imgType.view.onRender && imgType.view.onRender.call(this) // call original
97
- const el = view.el
98
- const elImg = view.el as HTMLImageElement
99
- const src = this.model.getAttributes().src
100
- if (src && !src.startsWith('http')) {
101
- elImg.src = storedToDisplayed(src, config.websiteId, config.storageId)
102
- }
103
- },
104
- },
105
- model: {
106
- ...imgType.model,
107
- init() {
108
- const model = this as Component
109
- imgType.model.init && imgType.model.init.call(this) // call original
110
- // Handle changes by the AssetManager
111
- model.on('change', () => {
112
- if(model.get('src')) {
113
- const newSrc = displayedToStored(model.get('src'))
114
- if(newSrc !== model.get('src')) {
115
- model.set('src', newSrc)
116
- model.view.render()
117
- }
118
- }
119
- })
120
- },
121
- },
122
- })
123
-
124
86
  // Load the site
125
87
  try {
126
88
  editor.runCommand(cmdPauseAutoSave)
@@ -656,7 +656,7 @@ export default class GitlabConnector implements StorageConnector {
656
656
  } while (page <= totalPages)
657
657
 
658
658
  return projects
659
- .filter(p => p.name.startsWith(this.options.repoPrefix))
659
+ .filter(p => p.name.startsWith(this.options.repoPrefix) && !p.name.includes('-deleted-'))
660
660
  .map(p => ({
661
661
  websiteId: p.id,
662
662
  name: p.name.replace(this.options.repoPrefix, ''),
@@ -826,75 +826,10 @@ export default class GitlabConnector implements StorageConnector {
826
826
  await this.callApi({
827
827
  session,
828
828
  path: `api/v4/projects/${websiteId}`,
829
- method: 'DELETE'
829
+ method: 'DELETE',
830
830
  })
831
- //// Load the meta repo data
832
- //const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
833
- // ref: this.options.branch,
834
- //})
835
- //const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
836
- //const data = metaRepo.websites[websiteId]
837
- //if(!data) throw new ApiError(`Website ${websiteId} not found`, 404)
838
- //// Update or create the website meta data
839
- //delete metaRepo.websites[websiteId]
840
- //// Save the meta repo data
841
- //const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
842
- // branch: this.options.branch,
843
- // commit_message: `Delete meta data of ${data.meta.name} (${websiteId}) from Silex`,
844
- // content: JSON.stringify(metaRepo),
845
- //})
846
831
  }
847
832
 
848
- // async duplicateWebsite(session: GitlabSession, websiteId: string): Promise<void> {
849
- // // Get the repo meta data
850
- // const meta = await this.getWebsiteMeta(session, websiteId)
851
- // // List all the repository files
852
- // const files = await this.ls({
853
- // session,
854
- // websiteId,
855
- // recursive: true,
856
- // })
857
-
858
- // // Create a new repo
859
- // const newId = await this.createWebsite(session, {
860
- // ...meta,
861
- // name: meta.name + ' Copy ' + new Date().toISOString().replace(/T.*/, '') + ' ' + Math.random().toString(36).substring(2, 4),
862
- // })
863
- // // Upload all files
864
- // const actions: GitlabAction[] = []
865
- // for (const file of files.keys()) {
866
- // const content = await this.readFile(session, websiteId, file)
867
- // // From buffer to string
868
- // const contentStr = content.toString('base64')
869
- // const path = encodeURIComponent(file)
870
- // switch (file) {
871
- // case WEBSITE_DATA_FILE:
872
- // actions.push({
873
- // action: 'update',
874
- // file_path: path,
875
- // content: contentStr,
876
- // })
877
- // break
878
- // default:
879
- // actions.push({
880
- // action: 'create',
881
- // file_path: path,
882
- // content: contentStr,
883
- // })
884
- // }
885
- // }
886
- // // Perform a batch commit
887
- // return this.callApi({
888
- // session,
889
- // path: `api/v4/projects/${newId}/repository/commits`,
890
- // method: 'POST',
891
- // requestBody: {
892
- // branch: this.options.branch,
893
- // commit_message: 'Update website data from Silex',
894
- // actions,
895
- // },
896
- // })
897
- // }
898
833
  // Fork the repo
899
834
  async duplicateWebsite(session: GitlabSession, websiteId: string): Promise<void> {
900
835
  const meta = await this.getWebsiteMeta(session, websiteId)