@silexlabs/silex 3.5.0 → 3.5.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.
Files changed (95) hide show
  1. package/.silex.js +0 -3
  2. package/dist/client/client/config.d.ts +8 -0
  3. package/dist/client/client/grapesjs/PublicationManager.d.ts +43 -0
  4. package/dist/client/client/grapesjs/cms/DataSource.d.ts +43 -0
  5. package/dist/client/client/grapesjs/cms/blocks.d.ts +2 -0
  6. package/dist/client/client/grapesjs/cms/buttons.d.ts +2 -0
  7. package/dist/client/client/grapesjs/cms/collection-pages.d.ts +4 -0
  8. package/dist/client/client/grapesjs/cms/config.d.ts +9 -0
  9. package/dist/client/client/grapesjs/cms/filters.d.ts +3 -0
  10. package/dist/client/client/grapesjs/cms/index.d.ts +44 -0
  11. package/dist/client/client/grapesjs/cms/liquid.d.ts +59 -0
  12. package/dist/client/client/grapesjs/cms/page-settings.d.ts +6 -0
  13. package/dist/client/client/grapesjs/cms/publication.d.ts +60 -0
  14. package/dist/client/client/grapesjs/cms/site-settings.d.ts +3 -0
  15. package/dist/client/client/grapesjs/cms/states.d.ts +8 -0
  16. package/dist/client/client/grapesjs/cms/traits.d.ts +8 -0
  17. package/dist/client/client/grapesjs/settings.d.ts +12 -3
  18. package/dist/client/client/grapesjs/view-buttons.d.ts +2 -1
  19. package/dist/client/client/utils.d.ts +1 -0
  20. package/dist/client/constants.d.ts +2 -1
  21. package/dist/client/css/admin.css +117 -19
  22. package/dist/client/index.html +2 -2
  23. package/dist/client/js/main.js +1974 -267
  24. package/dist/client/types.d.ts +3 -1
  25. package/dist/plugins/client/plugins/client/template.js +1 -1
  26. package/dist/plugins/client/plugins/client/template.js.map +1 -1
  27. package/dist/plugins/client/types.d.ts +3 -1
  28. package/dist/plugins/client/types.js +11 -8
  29. package/dist/plugins/client/types.js.map +1 -1
  30. package/dist/plugins/server/constants.d.ts +2 -1
  31. package/dist/plugins/server/constants.js +3 -2
  32. package/dist/plugins/server/constants.js.map +1 -1
  33. package/dist/plugins/server/plugins/server/FtpConnector.js +15 -20
  34. package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
  35. package/dist/plugins/server/plugins/server/GitlabConnector.js +34 -7
  36. package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
  37. package/dist/plugins/server/server/connectors/FsStorage.js +16 -20
  38. package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
  39. package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +4 -0
  40. package/dist/plugins/server/server/utils/websiteDataSerialization.js +21 -12
  41. package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -1
  42. package/dist/plugins/server/types.d.ts +3 -1
  43. package/dist/plugins/server/types.js +12 -9
  44. package/dist/plugins/server/types.js.map +1 -1
  45. package/dist/server/constants.js +3 -2
  46. package/dist/server/server/connectors/FsStorage.js +14 -18
  47. package/dist/server/server/utils/websiteDataSerialization.js +16 -11
  48. package/dist/server/types.js +6 -2
  49. package/package.json +16 -11
  50. package/src/css/_variables.scss +2 -1
  51. package/src/css/grapesjs-plugins.scss +31 -0
  52. package/src/css/grapesjs.scss +20 -0
  53. package/src/css/page-panel.scss +25 -6
  54. package/src/css/silex.scss +64 -5
  55. package/src/ts/client/config.ts +23 -0
  56. package/src/ts/client/grapesjs/PublicationManager.ts +159 -1
  57. package/src/ts/client/grapesjs/breadcrumbs.ts +3 -2
  58. package/src/ts/client/grapesjs/cms/DataSource.ts +333 -0
  59. package/src/ts/client/grapesjs/cms/blocks.ts +124 -0
  60. package/src/ts/client/grapesjs/cms/buttons.ts +163 -0
  61. package/src/ts/client/grapesjs/cms/collection-pages.ts +214 -0
  62. package/src/ts/client/grapesjs/cms/config.ts +47 -0
  63. package/src/ts/client/grapesjs/cms/filters.ts +53 -0
  64. package/src/ts/client/grapesjs/cms/index.ts +130 -0
  65. package/src/ts/client/grapesjs/cms/liquid.mock.ts +181 -0
  66. package/src/ts/client/grapesjs/cms/liquid.test.ts +239 -0
  67. package/src/ts/client/grapesjs/cms/liquid.ts +325 -0
  68. package/src/ts/client/grapesjs/cms/page-settings.ts +285 -0
  69. package/src/ts/client/grapesjs/cms/publication.test.ts +300 -0
  70. package/src/ts/client/grapesjs/cms/publication.ts +708 -0
  71. package/src/ts/client/grapesjs/cms/site-settings.ts +79 -0
  72. package/src/ts/client/grapesjs/cms/states.ts +125 -0
  73. package/src/ts/client/grapesjs/cms/traits.ts +95 -0
  74. package/src/ts/client/grapesjs/css-props.ts +15 -16
  75. package/src/ts/client/grapesjs/flex.ts +59 -39
  76. package/src/ts/client/grapesjs/img.ts +1 -1
  77. package/src/ts/client/grapesjs/index.ts +77 -4
  78. package/src/ts/client/grapesjs/internal-links.ts +4 -2
  79. package/src/ts/client/grapesjs/page-panel.ts +126 -11
  80. package/src/ts/client/grapesjs/sectors.ts +18 -10
  81. package/src/ts/client/grapesjs/semantic.ts +4 -0
  82. package/src/ts/client/grapesjs/settings.ts +252 -101
  83. package/src/ts/client/grapesjs/storage.ts +12 -3
  84. package/src/ts/client/grapesjs/view-buttons.ts +39 -3
  85. package/src/ts/client/utils.ts +15 -2
  86. package/src/ts/constants.ts +2 -1
  87. package/src/ts/plugins/client/template.ts +1 -1
  88. package/src/ts/plugins/server/FtpConnector.test.ts +1 -0
  89. package/src/ts/plugins/server/FtpConnector.ts +20 -23
  90. package/src/ts/plugins/server/GitlabConnector.ts +40 -10
  91. package/src/ts/server/connectors/FsStorage.test.ts +3 -2
  92. package/src/ts/server/connectors/FsStorage.ts +21 -23
  93. package/src/ts/server/utils/websiteDataSerialization.test.ts +45 -13
  94. package/src/ts/server/utils/websiteDataSerialization.ts +23 -14
  95. package/src/ts/types.ts +6 -2
@@ -0,0 +1,325 @@
1
+ import {
2
+ Expression,
3
+ Filter, Property, State, StateId, Token, getPersistantId, getStateVariableName, toExpression,
4
+ FIXED_TOKEN_ID,
5
+ UnariOperator,
6
+ getExpressionResultType,
7
+ BinaryOperator,
8
+ } from '@silexlabs/grapesjs-data-source'
9
+ import { Component } from 'grapesjs'
10
+ import { EleventyDataSourceId } from './DataSource'
11
+
12
+ export interface BinaryCondition {
13
+ operator: BinaryOperator,
14
+ expression: Expression,
15
+ expression2: Expression,
16
+ }
17
+
18
+ export interface UnaryCondition {
19
+ operator: UnariOperator,
20
+ expression: Expression,
21
+ }
22
+
23
+ export type Condition = BinaryCondition | UnaryCondition
24
+
25
+ /**
26
+ * Generate liquid instructions which echo the value of an expression
27
+ */
28
+ export function echoBlock(component: Component, expression: Expression): string {
29
+ if (expression.length === 0) throw new Error('Expression is empty')
30
+ if (expression.length === 1 && expression[0].type === 'property' && expression[0].fieldId === FIXED_TOKEN_ID) {
31
+ return expression[0].options?.value as string ?? ''
32
+ }
33
+ const statements = getLiquidBlock(component, expression)
34
+ return `{% liquid
35
+ ${statements
36
+ .map(({ liquid }) => liquid)
37
+ .join('\n\t')
38
+ }
39
+ echo ${statements[statements.length - 1].variableName}
40
+ %}`
41
+ }
42
+
43
+ /**
44
+ * Generate liquid instructions which echo the value of an expression, on 1 line
45
+ */
46
+ export function echoBlock1line(component: Component, expression: Expression): string {
47
+ if (expression.length === 0) throw new Error('Expression is empty')
48
+ if (expression.length === 1 && expression[0].type === 'property' && expression[0].fieldId === FIXED_TOKEN_ID) {
49
+ return expression[0].options?.value as string ?? ''
50
+ }
51
+ const statements = getLiquidBlock(component, expression)
52
+ return `{% ${statements
53
+ .map(({ liquid }) => liquid)
54
+ .join(' %}{% ')
55
+ } %}{{ ${statements[statements.length - 1].variableName} }}`
56
+ }
57
+
58
+ /**
59
+ * Generate liquid instructions which define a variable for later use
60
+ * This is used for components states
61
+ */
62
+ export function assignBlock(stateId: StateId, component: Component, expression: Expression): string {
63
+ if (expression.length === 0) throw new Error('Expression is empty')
64
+ const statements = getLiquidBlock(component, expression)
65
+ const persistantId = getPersistantId(component)
66
+ if (!persistantId) throw new Error('This component has no persistant ID')
67
+ return `{% liquid
68
+ ${statements
69
+ .map(({ liquid }) => liquid)
70
+ .join('\n\t')
71
+ }
72
+ assign ${getStateVariableName(persistantId, stateId)} = ${statements[statements.length - 1].variableName}
73
+ %}`
74
+ }
75
+
76
+ /**
77
+ * Generate liquid instructions which start and end a loop over the provided expression
78
+ * This is used for components states
79
+ */
80
+ export function loopBlock(component: Component, expression: Expression): [start: string, end: string] {
81
+ if (expression.length === 0) throw new Error('Expression is empty')
82
+ // Check data to loop over
83
+ const field = getExpressionResultType(expression, component)
84
+ if (!field) throw new Error(`Expression ${expression.map(token => token.label).join(' -> ')} is invalid`)
85
+ if (field.kind !== 'list') throw new Error(`Provided property needs to be a list in order to loop, not a ${field.kind}`)
86
+ const statements = getLiquidBlock(component, expression)
87
+ const loopDataVariableName = statements[statements.length - 1].variableName
88
+ const persistantId = getPersistantId(component)
89
+ if (!persistantId) {
90
+ console.error('Component', component, 'has no persistant ID. Persistant ID is required to get component states.')
91
+ throw new Error('This component has no persistant ID')
92
+ }
93
+ return [`{% liquid
94
+ ${statements
95
+ .map(({ liquid }) => liquid)
96
+ .join('\n\t')
97
+ }
98
+ %}
99
+ {% for ${getStateVariableName(persistantId, '__data')} in ${loopDataVariableName} %}
100
+ `, '{% endfor %}']
101
+ }
102
+
103
+ /**
104
+ * Generate liquid instructions which define a variable for later use
105
+ * This is used for components states
106
+ */
107
+ export function ifBlock(component: Component, condition: Condition): [start: string, end: string] {
108
+ // Check the first expression
109
+ if (condition.expression.length === 0) throw new Error('If block expression is empty')
110
+
111
+ // Check the operator
112
+ const unary = Object.values(UnariOperator).includes(condition.operator as UnariOperator) ? condition as UnaryCondition : null
113
+ const binary = Object.values(BinaryOperator).includes(condition.operator as BinaryOperator) ? condition as BinaryCondition : null
114
+ if (!unary && !binary) throw new Error(`If block operator is invalid: ${condition.operator}`)
115
+
116
+ // Check the second expression
117
+ if (binary && binary.expression2.length === 0) return ['', '']
118
+
119
+ // Get liquid for the first expression
120
+ const statements = getLiquidBlock(component, condition.expression)
121
+ const lastVariableName = statements[statements.length - 1].variableName
122
+
123
+ // Get liquid for the second
124
+ let lastVariableName2 = ''
125
+ if (binary) {
126
+ statements.push(...getLiquidBlock(component, binary.expression2))
127
+ lastVariableName2 = statements[statements.length - 1].variableName
128
+ }
129
+
130
+ // Get liquid for the whole if block
131
+ return [`{% liquid
132
+ ${statements
133
+ .map(({ liquid }) => liquid)
134
+ .join('\n\t')
135
+ }
136
+ %}
137
+ {% if ${unary ? getUnaryOp(lastVariableName, unary.operator) : getBinaryOp(lastVariableName, lastVariableName2, binary!.operator)} %}
138
+ `, '{% endif %}']
139
+ }
140
+
141
+ function getUnaryOp(variableName: string, operator: UnariOperator): string {
142
+ switch (operator) {
143
+ case UnariOperator.TRUTHY: return `${variableName} and ${variableName} != blank and ${variableName} != empty`
144
+ case UnariOperator.FALSY: return `not ${variableName}`
145
+ case UnariOperator.EMPTY_ARR: return `${variableName}.size == 0`
146
+ case UnariOperator.NOT_EMPTY_ARR: return `${variableName}.size > 0`
147
+ }
148
+ }
149
+
150
+ function getBinaryOp(variableName: string, variableName2: string, operator: BinaryOperator): string {
151
+ switch (operator) {
152
+ case BinaryOperator.EQUAL: return `${variableName} == ${variableName2}`
153
+ case BinaryOperator.NOT_EQUAL: return `${variableName} != ${variableName2}`
154
+ case BinaryOperator.GREATER_THAN: return `${variableName} > ${variableName2}`
155
+ case BinaryOperator.LESS_THAN: return `${variableName} < ${variableName2}`
156
+ case BinaryOperator.GREATER_THAN_OR_EQUAL: return `${variableName} >= ${variableName2}`
157
+ case BinaryOperator.LESS_THAN_OR_EQUAL: return `${variableName} <= ${variableName2}`
158
+ }
159
+ }
160
+
161
+ let numNextVar = 0
162
+ /**
163
+ * Pagination data has no filter and no states in it
164
+ */
165
+ export function getPaginationData(expression: Property[]): string {
166
+ const statement = getLiquidStatementProperties(expression)
167
+ const firstToken = expression[0]
168
+ if (firstToken) {
169
+ if (!firstToken.dataSourceId || firstToken.dataSourceId === EleventyDataSourceId) {
170
+ return statement
171
+ }
172
+ return `${firstToken.dataSourceId}.${statement}`
173
+ } else {
174
+ return ''
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Convert an expression to liquid code
180
+ */
181
+ export function getLiquidBlock(component: Component, expression: Expression): { variableName: string, liquid: string }[] {
182
+ if (expression.length === 0) return []
183
+ const result = [] as { variableName: string, liquid: string }[]
184
+ const firstToken = expression[0]
185
+ let lastVariableName = ''
186
+ if (firstToken.type === 'filter') throw new Error('Expression cannot start with a filter')
187
+ if (firstToken.type === 'property' && firstToken.dataSourceId && firstToken.dataSourceId !== 'eleventy') {
188
+ lastVariableName = firstToken.dataSourceId as string
189
+ }
190
+ const rest = [...expression]
191
+ while (rest.length) {
192
+ // Move all tokens until the first filter
193
+ const firstFilterIndex = rest.findIndex(token => token.type === 'filter')
194
+ const variableExpression = firstFilterIndex === -1 ? rest.splice(0) : rest.splice(0, firstFilterIndex)
195
+ // Add all the filters until a property again
196
+ const firstNonFilterIndex = rest.findIndex(token => token.type !== 'filter')
197
+ const filterExpression = firstNonFilterIndex === -1 ? rest.splice(0) : rest.splice(0, firstNonFilterIndex)
198
+ const variableName = getNextVariableName(component, numNextVar++)
199
+ const statement = getLiquidStatement(variableExpression.concat(filterExpression), variableName, lastVariableName)
200
+ lastVariableName = variableName
201
+ result.push({
202
+ variableName,
203
+ liquid: statement,
204
+ })
205
+ }
206
+ return result
207
+ }
208
+
209
+ export function getNextVariableName(component: Component, numNextVar: number): string {
210
+ return `var_${component.ccid}_${numNextVar}`
211
+ }
212
+
213
+ /**
214
+ * Get the liquid assign statement for the expression
215
+ * The expression must
216
+ * - start with a property or state
217
+ * - once it has a filter it canot have a property again
218
+ * - state can only be the first token
219
+ *
220
+ * Example of return value: `countries.continent.countries | first.name`
221
+ */
222
+ export function getLiquidStatement(expression: Expression, variableName: string, lastVariableName: string = ''): string {
223
+ if (expression.length === 0) throw new Error('Expression cannot be empty')
224
+ // Split expression in 2: properties and filters
225
+ const firstFilterIndex = expression.findIndex(token => token.type === 'filter')
226
+ if (firstFilterIndex === 0) throw new Error('Expression cannot start with a filter')
227
+ const properties = (firstFilterIndex < 0 ? expression : expression.slice(0, firstFilterIndex)) as (Property | State)[]
228
+ const filters = firstFilterIndex > 0 ? expression.slice(firstFilterIndex) as Filter[] : []
229
+ // Check that no properties or state come after filter
230
+ if (filters.find(token => token.type !== 'filter')) {
231
+ throw new Error('A filter cannot be followed by a property or state')
232
+ }
233
+ // Start with the assign statement
234
+ return `assign ${variableName} = ${lastVariableName ? `${lastVariableName}.` : ''
235
+ }${
236
+ // Add all the properties
237
+ getLiquidStatementProperties(properties)
238
+ }${
239
+ // Add all the filters
240
+ getLiquidStatementFilters(filters)
241
+ }`
242
+ }
243
+
244
+ export function getLiquidStatementProperties(properties: (Property | State)[]): string {
245
+ return properties.map((token, index) => {
246
+ switch (token.type) {
247
+ case 'state': {
248
+ if (index !== 0) throw new Error('State can only be the first token in an expression')
249
+ return getStateVariableName(token.componentId, token.storedStateId)
250
+ }
251
+ case 'property': {
252
+ if (token.fieldId === FIXED_TOKEN_ID) {
253
+ return `"${token.options?.value ?? ''}"`
254
+ }
255
+ return token.fieldId
256
+ }
257
+ default: {
258
+ throw new Error(`Only state or property can be used in an expression, got ${(token as Token).type}`)
259
+ }
260
+ }
261
+ })
262
+ .join('.')
263
+ }
264
+
265
+ export function getLiquidStatementFilters(filters: Filter[]): string {
266
+ if (!filters.length) return ''
267
+ return ' | ' + filters.map(token => {
268
+ const options = token.options ? Object.entries(token.options)
269
+ // Order the filter's options by the order they appear in the filter's optionsKeys
270
+ .map(([key, value]) => ({
271
+ key,
272
+ value: value,
273
+ order: token.optionsKeys?.indexOf(key),
274
+ }))
275
+ .sort((a, b) => {
276
+ if (a.order === undefined && b.order === undefined) return 0
277
+ if (a.order === undefined) return 1
278
+ if (b.order === undefined) return -1
279
+ return a.order - b.order
280
+ })
281
+ // Convert the options to liquid
282
+ .map(({ key, value }) => handleFilterOption(token, key, value as string)) : []
283
+ return `${token.filterName ?? token.id}${options.length ? `: ${options.join(', ')}` : ''}`
284
+ })
285
+ .join(' | ')
286
+ }
287
+
288
+ /**
289
+ * Quote a string for liquid
290
+ * Check that the string is not already quoted
291
+ * Escape existing quotes
292
+ */
293
+ function quote(value: string): string {
294
+ if (value.startsWith('"') && value.endsWith('"')) return value
295
+ return `"${value.replace(/"/g, '\\"')}"`
296
+ }
297
+
298
+ function handleFilterOption(filter: Filter, key: string, value: string): string {
299
+ try {
300
+ const expression = toExpression(value)
301
+ if (expression) {
302
+ const result = expression.map(token => {
303
+ switch (token.type) {
304
+ case 'property': {
305
+ if (token.fieldId === FIXED_TOKEN_ID) {
306
+ return `"${token.options?.value ?? ''}"`
307
+ }
308
+ return token.fieldId
309
+ }
310
+ case 'state': {
311
+ return getStateVariableName(token.componentId, token.storedStateId)
312
+ }
313
+ case 'filter': {
314
+ throw new Error('Filter cannot be used in a filter option')
315
+ }
316
+ }
317
+ })
318
+ .join('.')
319
+ return filter.quotedOptions?.includes(key) ? quote(result) : result
320
+ }
321
+ } catch {
322
+ // Ignore
323
+ }
324
+ return filter.quotedOptions?.includes(key) ? quote(value) : value
325
+ }
@@ -0,0 +1,285 @@
1
+ import { removeState, setState, toExpression } from '@silexlabs/grapesjs-data-source'
2
+ import { CMS_SETTINGS_SECTION_ID, EleventyPluginOptions, Silex11tyPluginWebsiteSettings } from './index'
3
+ import { html, TemplateResult } from 'lit-html'
4
+ import { Page, Editor, Component } from 'grapesjs'
5
+ import { WebsiteSettings } from '../../../types'
6
+ import { ClientEvent } from '../../events'
7
+ import { cmdAddSection } from '../settings'
8
+ import { createRef, ref } from 'lit-html/directives/ref.js'
9
+
10
+ /**
11
+ * Main function to add the settings to the page
12
+ */
13
+ export default function(editor: Editor, opts: EleventyPluginOptions): void {
14
+ if (!opts.enable11ty) return // Do not add the settings if 11ty is disabled
15
+
16
+ editor.on(ClientEvent.SETTINGS_SAVE_START, handleSettingsSaveStart(editor))
17
+ editor.on(ClientEvent.SETTINGS_CLOSE, handleSettingsClose(editor))
18
+
19
+ editor.runCommand(cmdAddSection, {
20
+ section: {
21
+ id: CMS_SETTINGS_SECTION_ID,
22
+ label: 'CMS',
23
+ render: (settings: WebsiteSettings, page: Backbone.Model) => {
24
+ currentPage = page as Page
25
+ return renderSettingsSection(settings, editor, currentPage)
26
+ },
27
+ },
28
+ siteOrPage: 'page',
29
+ position: 'last',
30
+ })
31
+ }
32
+
33
+ // Store original settings for revert functionality
34
+ let originalSettings: Silex11tyPluginWebsiteSettings | null = null
35
+ let currentPage: Page | undefined
36
+
37
+ function handleSettingsSaveStart(editor: Editor) {
38
+ return (page: Page) => {
39
+ updateBodyStates(editor, page)
40
+ currentPage = null
41
+ originalSettings = null
42
+ }
43
+ }
44
+
45
+ function handleSettingsClose(editor: Editor) {
46
+ return () => {
47
+ if (currentPage && originalSettings) {
48
+ const currentSettings = currentPage.get('settings') as Silex11tyPluginWebsiteSettings || {}
49
+ // Revert the whole settings object if it was changed
50
+ if (JSON.stringify(currentSettings) !== JSON.stringify(originalSettings)) {
51
+ currentPage.set('settings', originalSettings)
52
+ updateBodyStates(editor, currentPage)
53
+ }
54
+ }
55
+ currentPage = null
56
+ originalSettings = null
57
+ }
58
+ }
59
+
60
+ function updateTemporarySettings(editor: Editor, page: Page, pageDataEditor: HTMLInputElement | undefined): void {
61
+ if (!pageDataEditor) return
62
+ originalSettings = page.get('settings') as WebsiteSettings
63
+ const eleventyPageData = pageDataEditor.value || ''
64
+ page.set('settings', {
65
+ ...originalSettings,
66
+ eleventyPageData,
67
+ } as Silex11tyPluginWebsiteSettings)
68
+ updateBodyStates(editor, page)
69
+ }
70
+
71
+ /**
72
+ * Set the state on the body component
73
+ * This is only useful to build the GraphQL query
74
+ */
75
+ function stateOnBody(editor: Editor, value: string, name: string, body: Component): void {
76
+ if (value) {
77
+ const expression = toExpression(value)
78
+ if (expression) {
79
+ setState(body, name, {
80
+ label: name,
81
+ hidden: true,
82
+ expression,
83
+ })
84
+ } else {
85
+ removeState(body, name)
86
+ editor.runCommand('notifications:add', {
87
+ type: 'error',
88
+ message: `Invalid JSON for ${name}`,
89
+ group: 'Errors in your settings',
90
+ })
91
+ }
92
+ } else {
93
+ removeState(body, name)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Set the state on the body component
99
+ * This is only useful to build the GraphQL query
100
+ */
101
+ const bodyStateKeys: (keyof Silex11tyPluginWebsiteSettings)[] = [
102
+ 'eleventySeoTitle',
103
+ 'eleventySeoDescription',
104
+ 'eleventyFavicon',
105
+ 'eleventyOGImage',
106
+ 'eleventyOGTitle',
107
+ 'eleventyOGDescription',
108
+ 'eleventyPageData',
109
+ 'eleventyPermalink',
110
+ ]
111
+
112
+ function updateBodyStates(editor: Editor, page: Page): void {
113
+ if (page) {
114
+ const settings = page?.get('settings') as Silex11tyPluginWebsiteSettings | undefined
115
+ if (settings) {
116
+ const body = page.getMainComponent()
117
+ for (const key of bodyStateKeys) {
118
+ stateOnBody(editor, (settings as Record<string, string>)[key], key, body)
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Render the settings form
126
+ */
127
+ function renderSettingsSection(settings: Silex11tyPluginWebsiteSettings, editor: Editor, page: Page): TemplateResult {
128
+ const body = page.getMainComponent()
129
+ const pageDataEditor = createRef<HTMLInputElement>()
130
+ setTimeout(() => {
131
+ // Update all input fields with their corresponding values from settings
132
+ // This is needed because we change things without refreshing the DOM in JS
133
+ ;(document.querySelectorAll(`
134
+ #settings-${CMS_SETTINGS_SECTION_ID} input,
135
+ #settings-${CMS_SETTINGS_SECTION_ID} state-editor
136
+ `) as NodeListOf<HTMLInputElement>)
137
+ .forEach((input: HTMLInputElement) => {
138
+ const value = settings[input.name]
139
+ if (input.type === 'checkbox') {
140
+ input.checked = !!value
141
+ } else if (typeof value !== 'undefined') {
142
+ input.value = value
143
+ } else {
144
+ input.value = ''
145
+ }
146
+ })
147
+ })
148
+ return html`
149
+ <style>
150
+ .silex-warning {
151
+ margin-top: 10px
152
+ padding: 10px
153
+ background-color: var(--ds-primary)
154
+ border-color: var(--ds-button-color)
155
+ color: #721c24
156
+ border: 1px solid transparent
157
+ border-radius: .25rem
158
+ }
159
+ </style>
160
+ <div id="settings-${CMS_SETTINGS_SECTION_ID}" class="silex-hideable silex-hidden">
161
+ <div class="gjs-sm-sector-title">Silex CMS - Page settings</div>
162
+ <div class="silex-help">
163
+ The "Silex CMS" feature integrates <a target="_blank" href="https://www.11ty.dev/">11ty</a> static site generator and your favorite headless CMS with Silex.
164
+ <br>Read the <a target="_blank" href="https://docs.silex.me/en/user/cms">documentation</a> to learn more.
165
+ </div>
166
+ <div class="silex-form__group col2">
167
+ <label class="silex-form__element">
168
+ <h3>Create pages from data</h3>
169
+ <p class="silex-help">Pagination allows you to iterate over data and create multiple webiste pages from a single page in Silex. </p>
170
+ <state-editor
171
+ ${ref(pageDataEditor)}
172
+ id="eleventyPageData"
173
+ name="eleventyPageData"
174
+ value=${settings.eleventyPageData ?? ''}
175
+ .editor=${editor}
176
+ .selected=${body}
177
+ @change=${() => updateTemporarySettings(editor, page, pageDataEditor.value)}
178
+ no-states
179
+ no-filters
180
+ >
181
+ <label slot="label">Data</label>
182
+ </state-editor>
183
+ <label class="silex-form__element">Size
184
+ <input type="number" name="eleventyPageSize" .value=${settings.eleventyPageSize ?? 1}/>
185
+ </label>
186
+ <label class="silex-form__element">Reverse
187
+ <input type="checkbox" name="eleventyPageReverse" ?checked=${!!settings.eleventyPageReverse}/>
188
+ </label>
189
+ <state-editor
190
+ id="eleventyPermalink"
191
+ name="eleventyPermalink"
192
+ value=${settings.eleventyPermalink ?? ''}
193
+ .editor=${editor}
194
+ .selected=${body}
195
+ >
196
+ <label slot="label">Permalink</label>
197
+ </state-editor>
198
+ <label class="silex-form__element">Available languages
199
+ <p class="silex-help">Silex can duplicate this page for each language and generate a different URL for each language.</p>
200
+ <p class="silex-help">Provide a comma separated list of languages. For example: <code>en,fr</code>. An empty value will deactivate this feature.</p>
201
+ <input type="text" name="silexLanguagesList" .value=${settings.silexLanguagesList ?? ''}/>
202
+ </label>
203
+ </label>
204
+ <label class="silex-form__element">
205
+ <h3>Navigation Plugin</h3>
206
+ <p class="silex-help">This 11ty plugin enables infinite-depth hierarchical navigation in Eleventy projects. Supports breadcrumbs too! <a target="_blank" href="https://www.11ty.dev/docs/plugins/navigation/">Read more about the Navigation Plugin</a>.</p>
207
+ <label class="silex-form__element">Key
208
+ <input type="text" name="eleventyNavigationKey" .value=${settings.eleventyNavigationKey ?? ''}/>
209
+ </label>
210
+ <label class="silex-form__element">Title
211
+ <input type="text" name="eleventyNavigationTitle" .value=${settings.eleventyNavigationTitle ?? ''}/>
212
+ </label>
213
+ <label class="silex-form__element">Order
214
+ <input type="number" name="eleventyNavigationOrder" .value=${settings.eleventyNavigationOrder ?? ''}/>
215
+ </label>
216
+ <label class="silex-form__element">Parent
217
+ <input type="text" name="eleventyNavigationParent" .value=${settings.eleventyNavigationParent ?? ''}/>
218
+ </label>
219
+ <label class="silex-form__element">URL
220
+ <input type="text" name="eleventyNavigationUrl" .value=${settings.eleventyNavigationUrl ?? ''}/>
221
+ </label>
222
+ </label>
223
+ <label class="silex-form__element">
224
+ <h3>SEO</h3>
225
+ <state-editor
226
+ id="eleventySeoTitle"
227
+ name="eleventySeoTitle"
228
+ .value=${settings.eleventySeoTitle ?? ''}
229
+ .editor=${editor}
230
+ .selected=${body}
231
+ >
232
+ <label slot="label">Title</label>
233
+ </state-editor>
234
+ <state-editor
235
+ id="eleventySeoDescription"
236
+ name="eleventySeoDescription"
237
+ .value=${settings.eleventySeoDescription ?? ''}
238
+ .editor=${editor}
239
+ .selected=${body}
240
+ >
241
+ <label slot="label">Description</label>
242
+ </state-editor>
243
+ <state-editor
244
+ id="eleventyFavicon"
245
+ name="eleventyFavicon"
246
+ .value=${settings.eleventyFavicon ?? ''}
247
+ .editor=${editor}
248
+ .selected=${body}
249
+ >
250
+ <label slot="label">Favicon</label>
251
+ </state-editor>
252
+ </label>
253
+ <label class="silex-form__element">
254
+ <h3>Social</h3>
255
+ <state-editor
256
+ id="eleventyOGImage"
257
+ name="eleventyOGImage"
258
+ .value=${settings.eleventyOGImage ?? ''}
259
+ .editor=${editor}
260
+ .selected=${body}
261
+ >
262
+ <label slot="label">OG Image</label>
263
+ </state-editor>
264
+ <state-editor
265
+ id="eleventyOGTitle"
266
+ name="eleventyOGTitle"
267
+ .value=${settings.eleventyOGTitle ?? ''}
268
+ .editor=${editor}
269
+ .selected=${body}
270
+ >
271
+ <label slot="label">OG Title</label>
272
+ </state-editor>
273
+ <state-editor
274
+ id="eleventyOGDescription"
275
+ name="eleventyOGDescription"
276
+ .value=${settings.eleventyOGDescription ?? ''}
277
+ .editor=${editor}
278
+ .selected=${body}
279
+ >
280
+ <label slot="label">OG Description</label>
281
+ </state-editor>
282
+ </div>
283
+ </div>
284
+ `
285
+ }