@silexlabs/silex 3.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +5 -0
- package/.editorconfig +14 -0
- package/.eslintrc.json +43 -0
- package/.github/FUNDING.yml +13 -0
- package/.github/workflows/docker-hub-publish.yml +36 -0
- package/.github/workflows/npm-publish.yml +19 -0
- package/.github/workflows/test.yml +26 -0
- package/.nvmrc +1 -0
- package/.silex.js +19 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +108 -0
- package/Dockerfile +15 -0
- package/LICENSE +203 -0
- package/app.json +106 -0
- package/directus.silex.js +7 -0
- package/dist/client/css/admin.css +4350 -0
- package/dist/client/index.html +9 -0
- package/dist/client/js/main.js +282 -0
- package/dist/client/js/main.js.LICENSE.txt +52 -0
- package/dist/server/config.js +50 -0
- package/dist/server/events.js +5 -0
- package/dist/server/index.js +29 -0
- package/dist/server/start.js +18 -0
- package/jest.config.js +10 -0
- package/logos/logo_by_ceubri/silex-icon-2018@200px.png +0 -0
- package/logos/logo_by_ceubri/silex-icon-2018@4x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white-transp.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@0.5x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@0.75x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@4x.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@4x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.ai +2520 -6
- package/logos/logo_by_ceubri/silex-logo-2018.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@0.5x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@0.75x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@4x.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@4x.png +0 -0
- package/logos/silexlabs.png +0 -0
- package/package.json +170 -0
- package/public/assets/container.png +0 -0
- package/public/assets/favicon.png +0 -0
- package/public/assets/logo-silex-small.png +0 -0
- package/public/example-config.js +31 -0
- package/public/robots.txt +2 -0
- package/readme.md +51 -0
- package/src/css/_mixins.scss +8 -0
- package/src/css/_variables.scss +33 -0
- package/src/css/dialog.scss +34 -0
- package/src/css/grapesjs.scss +44 -0
- package/src/css/index.scss +22 -0
- package/src/css/loader.scss +10 -0
- package/src/css/page-panel.scss +65 -0
- package/src/css/project-bar.scss +102 -0
- package/src/css/silex.scss +183 -0
- package/src/html/debug.pug +8 -0
- package/src/html/index.pug +39 -0
- package/src/html/templates/includes.pug +2 -0
- package/src/html/templates/workspace.pug +2 -0
- package/src/plugins/AuthPlugin.js +21 -0
- package/src/plugins/Cache.js +11 -0
- package/src/plugins/DefaultBackend.js +113 -0
- package/src/plugins/ExpressPlugin.js +30 -0
- package/src/plugins/HooksPlugin.js +58 -0
- package/src/plugins/PublishPlugin.js +69 -0
- package/src/plugins/SslPlugin.js +66 -0
- package/src/plugins/StaticPlugin.js +48 -0
- package/src/plugins/WebsitePlugin.js +149 -0
- package/src/ts/client/config.ts +21 -0
- package/src/ts/client/expose.ts +4 -0
- package/src/ts/client/grapesjs/blocks.ts +88 -0
- package/src/ts/client/grapesjs/eleventy.ts +21 -0
- package/src/ts/client/grapesjs/index.ts +255 -0
- package/src/ts/client/grapesjs/internal-links.test.ts +87 -0
- package/src/ts/client/grapesjs/internal-links.ts +210 -0
- package/src/ts/client/grapesjs/new-page-dialog.ts +73 -0
- package/src/ts/client/grapesjs/openImport.ts +59 -0
- package/src/ts/client/grapesjs/page-panel.ts +101 -0
- package/src/ts/client/grapesjs/project-bar.ts +92 -0
- package/src/ts/client/grapesjs/publish.ts +386 -0
- package/src/ts/client/grapesjs/rich-text.ts +16 -0
- package/src/ts/client/grapesjs/semantic.ts +96 -0
- package/src/ts/client/grapesjs/settings.ts +190 -0
- package/src/ts/client/grapesjs/symbolDialogs.ts +21 -0
- package/src/ts/client/grapesjs/template.ts +166 -0
- package/src/ts/client/index.ts +38 -0
- package/src/ts/client/utils.ts +9 -0
- package/src/ts/externs.ts +1 -0
- package/src/ts/page.ts +14 -0
- package/src/ts/server/cli.ts +23 -0
- package/src/ts/server/config.ts +62 -0
- package/src/ts/server/events.ts +2 -0
- package/src/ts/server/index.ts +35 -0
- package/src/ts/server/start.ts +18 -0
- package/src/ts/server/utils/BackwardCompat.ts +534 -0
- package/src/ts/types.ts +138 -0
- package/tsconfig-server-debug.json +20 -0
- package/tsconfig-server-release.json +19 -0
- package/tsconfig.jest.json +9 -0
- package/tsconfig.json +27 -0
- package/webpack.config.js +29 -0
- package/webpack.debug.config.js +8 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {html, render} from 'lit-html'
|
|
2
|
+
import {map} from 'lit-html/directives/map.js'
|
|
3
|
+
import grapesjs from 'grapesjs/dist/grapes.min.js'
|
|
4
|
+
import { getPageSlug } from '../../page'
|
|
5
|
+
import { onAll } from '../utils'
|
|
6
|
+
|
|
7
|
+
// constants
|
|
8
|
+
const pluginName = 'publish'
|
|
9
|
+
export const cmdPublish = 'publish-open-dialog'
|
|
10
|
+
let _token = null
|
|
11
|
+
let projectId
|
|
12
|
+
let rootUrl
|
|
13
|
+
|
|
14
|
+
// plugin code
|
|
15
|
+
export const publishPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => {
|
|
16
|
+
opts = {
|
|
17
|
+
appendTo: 'options',
|
|
18
|
+
...opts,
|
|
19
|
+
}
|
|
20
|
+
// Global config
|
|
21
|
+
rootUrl = opts.rootUrl
|
|
22
|
+
projectId = opts.projectId
|
|
23
|
+
// Keep track of the token
|
|
24
|
+
editor.on('login:success', async ({ getUser, getToken }) => {
|
|
25
|
+
_token = await getToken()
|
|
26
|
+
})
|
|
27
|
+
// add publication settings to the website
|
|
28
|
+
editor.on('storage:start:store', (data) => {
|
|
29
|
+
data.publication = editor.getModel().get('publication')
|
|
30
|
+
})
|
|
31
|
+
editor.on('storage:end:load', (data) => {
|
|
32
|
+
const model = editor.getModel()
|
|
33
|
+
model.set('publication', data.publication || {})
|
|
34
|
+
})
|
|
35
|
+
editor.Panels.addButton(opts.appendTo, {
|
|
36
|
+
id: 'publish-button',
|
|
37
|
+
className: 'silex-button--size publish-button',
|
|
38
|
+
command: cmdPublish,
|
|
39
|
+
attributes: { title: 'Publish' },
|
|
40
|
+
label: '<span class="fa fa-rocket"></span><span class="silex-button--small">Publish</span>',
|
|
41
|
+
})
|
|
42
|
+
editor.Commands.add(cmdPublish, {
|
|
43
|
+
run(editor) { openDialog(editor) },
|
|
44
|
+
stop(editor) { closeDialog(editor) },
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export function getDialogElements() {
|
|
49
|
+
const el = document.querySelector('#publish-dialog')
|
|
50
|
+
return el ? {
|
|
51
|
+
dialog: el,
|
|
52
|
+
primary: el.querySelector('#publish-button--primary'),
|
|
53
|
+
secondary: el.querySelector('#publish-button--secondary'),
|
|
54
|
+
} : null
|
|
55
|
+
}
|
|
56
|
+
function createDialogElements() {
|
|
57
|
+
const el = document.createElement('div')
|
|
58
|
+
el.id = 'publish-dialog'
|
|
59
|
+
el.className = 'silex-dialog-inline silex-dialog gjs-two-color'
|
|
60
|
+
document.body.append(el)
|
|
61
|
+
return getDialogElements()
|
|
62
|
+
}
|
|
63
|
+
function move(rect) {
|
|
64
|
+
Object.keys(rect).forEach(key => dialog.style[key] = rect[key] + 'px')
|
|
65
|
+
}
|
|
66
|
+
function update(editor) {
|
|
67
|
+
render(html`
|
|
68
|
+
${ open ? html`
|
|
69
|
+
<main>
|
|
70
|
+
${ status === STATUS_PENDING ? html`
|
|
71
|
+
<p>Publication in progress</p>
|
|
72
|
+
` : ''}
|
|
73
|
+
${ status === STATUS_SUCCESS ? html`
|
|
74
|
+
<p>Publication success</p>
|
|
75
|
+
${ url ? html`<a href="${url}" target="_blank">Click here to view the published website</a>` : ''}
|
|
76
|
+
` : ''}
|
|
77
|
+
${ status === STATUS_ERROR ? html`
|
|
78
|
+
<p>Publication error</p>
|
|
79
|
+
<div>${ errorMessage }</div>
|
|
80
|
+
` : ''}
|
|
81
|
+
${ state?.running ? html`
|
|
82
|
+
<progress
|
|
83
|
+
value=""
|
|
84
|
+
style="width: 100%;"
|
|
85
|
+
></progress>
|
|
86
|
+
` : ''}
|
|
87
|
+
${ state.logs?.length ? html`
|
|
88
|
+
<details>
|
|
89
|
+
<summary>Logs</summary>
|
|
90
|
+
<pre style="
|
|
91
|
+
max-width: 100%;
|
|
92
|
+
max-height: 50vh;
|
|
93
|
+
overflow: auto;
|
|
94
|
+
font-size: x-small;
|
|
95
|
+
"
|
|
96
|
+
>${ cleanup(state.logs) }
|
|
97
|
+
</pre>
|
|
98
|
+
</details>
|
|
99
|
+
` : '' }
|
|
100
|
+
${ state.errors?.length ? html`
|
|
101
|
+
<details>
|
|
102
|
+
<summary>Errors</summary>
|
|
103
|
+
<pre style="
|
|
104
|
+
max-width: 100%;
|
|
105
|
+
max-height: 50vh;
|
|
106
|
+
overflow: auto;
|
|
107
|
+
font-size: x-small;
|
|
108
|
+
"
|
|
109
|
+
>${ cleanup(state.errors) }
|
|
110
|
+
</pre>
|
|
111
|
+
</details>
|
|
112
|
+
` : '' }
|
|
113
|
+
</main>
|
|
114
|
+
<footer>
|
|
115
|
+
${ status === STATUS_PENDING ? '' : html`
|
|
116
|
+
<button
|
|
117
|
+
class="silex-button silex-button--primary"
|
|
118
|
+
id="publish-button--primary"
|
|
119
|
+
@click=${() => startPublication(editor)}
|
|
120
|
+
>Publish</button>
|
|
121
|
+
`}
|
|
122
|
+
<button
|
|
123
|
+
class="silex-button silex-button--secondary"
|
|
124
|
+
id="publish-button--secondary"
|
|
125
|
+
@click=${() => editor.Commands.stop(cmdPublish)}
|
|
126
|
+
>Close</button>
|
|
127
|
+
</footer>
|
|
128
|
+
` : ''}
|
|
129
|
+
`, dialog)
|
|
130
|
+
if(open) {
|
|
131
|
+
dialog.classList.remove('silex-dialog-hide')
|
|
132
|
+
} else {
|
|
133
|
+
dialog.classList.add('silex-dialog-hide')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export const STATUS_NONE='STATUS_NONE'
|
|
137
|
+
export const STATUS_PENDING='STATUS_PENDING'
|
|
138
|
+
export const STATUS_ERROR='STATUS_ERROR'
|
|
139
|
+
export const STATUS_SUCCESS='STATUS_SUCCESS'
|
|
140
|
+
export let status = STATUS_NONE
|
|
141
|
+
export let open = false
|
|
142
|
+
let errorMessage = ''
|
|
143
|
+
let dialog
|
|
144
|
+
let url // from the result of the first fetch when publishing
|
|
145
|
+
let state = {
|
|
146
|
+
queued: false,
|
|
147
|
+
error: false,
|
|
148
|
+
running: false,
|
|
149
|
+
logs: [],
|
|
150
|
+
errors: [],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cleanup(arr: string[][]): string {
|
|
154
|
+
return arr[arr.length-1]
|
|
155
|
+
?.map(str => str.replace(/\[.*\]/g, '').trim())
|
|
156
|
+
?.filter(str => !!str)
|
|
157
|
+
?.join('\n')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function displayError(editor, message) {
|
|
161
|
+
console.error(message)
|
|
162
|
+
errorMessage = message
|
|
163
|
+
status = STATUS_ERROR
|
|
164
|
+
update(editor)
|
|
165
|
+
}
|
|
166
|
+
export async function closeDialog(editor) {
|
|
167
|
+
open = false
|
|
168
|
+
update(editor)
|
|
169
|
+
}
|
|
170
|
+
export async function toggleDialog(editor) {
|
|
171
|
+
if(open) closeDialog(editor)
|
|
172
|
+
else openDialog(editor)
|
|
173
|
+
}
|
|
174
|
+
export async function openDialog(editor) {
|
|
175
|
+
open = true
|
|
176
|
+
|
|
177
|
+
// Position
|
|
178
|
+
const buttonEl = editor.Panels.getPanel('options').view.el
|
|
179
|
+
.querySelector('.publish-button')
|
|
180
|
+
const rect = buttonEl.getBoundingClientRect()
|
|
181
|
+
|
|
182
|
+
const width = 450
|
|
183
|
+
const padding = 10 * 2
|
|
184
|
+
const minHeight = 50
|
|
185
|
+
if(!dialog) dialog = createDialogElements().dialog
|
|
186
|
+
move({
|
|
187
|
+
left: rect.right - width - padding,
|
|
188
|
+
top: rect.bottom + 10,
|
|
189
|
+
width,
|
|
190
|
+
minHeight,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Publication
|
|
194
|
+
if(status === STATUS_NONE) {
|
|
195
|
+
startPublication(editor)
|
|
196
|
+
} else {
|
|
197
|
+
update(editor)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export async function startPublication(editor) {
|
|
201
|
+
if(status === STATUS_PENDING) throw new Error('Publication is already in progress')
|
|
202
|
+
status = STATUS_PENDING
|
|
203
|
+
update(editor)
|
|
204
|
+
editor.trigger('publish:before')
|
|
205
|
+
const projectData = editor.getProjectData()
|
|
206
|
+
const siteSettings = editor.getModel().get('settings')
|
|
207
|
+
const publicationSettings = editor.getModel().get('publication')
|
|
208
|
+
// Update assets URL to display outside the editor
|
|
209
|
+
const assetsFolderUrl = publicationSettings?.assets?.url
|
|
210
|
+
if(assetsFolderUrl) {
|
|
211
|
+
const publishedUrl = path => `${assetsFolderUrl}/${path.split('/').pop()}`
|
|
212
|
+
// New URLs for assets, according to site config
|
|
213
|
+
onAll(editor, c => {
|
|
214
|
+
// Attributes
|
|
215
|
+
if(c.get('type') === 'image') {
|
|
216
|
+
const path = c.get('src')
|
|
217
|
+
c.set('tmp-src', path)
|
|
218
|
+
c.set('src', publishedUrl(path))
|
|
219
|
+
}
|
|
220
|
+
//// Inline styles
|
|
221
|
+
//// This is handled by the editor.Css.getAll loop
|
|
222
|
+
//const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop()
|
|
223
|
+
//if(bgUrl) {
|
|
224
|
+
// c.set('tmp-bg-url', bgUrl)
|
|
225
|
+
// c.setStyle({
|
|
226
|
+
// ...c.getStyle(),
|
|
227
|
+
// 'background-image': `url('${publishedUrl(bgUrl)}')`,
|
|
228
|
+
// })
|
|
229
|
+
//}
|
|
230
|
+
})
|
|
231
|
+
editor.Css.getAll()
|
|
232
|
+
.forEach(c => {
|
|
233
|
+
const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop()
|
|
234
|
+
if(bgUrl) {
|
|
235
|
+
c.setStyle({
|
|
236
|
+
...c.getStyle(),
|
|
237
|
+
'background-image': `url('${publishedUrl(bgUrl)}')`,
|
|
238
|
+
})
|
|
239
|
+
c.set('tmp-bg-url-css', bgUrl)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
// Build the files structure
|
|
244
|
+
const files = await getFiles(editor, {siteSettings, publicationSettings})
|
|
245
|
+
|
|
246
|
+
// Create the data to send to the server
|
|
247
|
+
const data = {
|
|
248
|
+
...projectData,
|
|
249
|
+
settings: siteSettings,
|
|
250
|
+
publication: publicationSettings,
|
|
251
|
+
projectId,
|
|
252
|
+
files,
|
|
253
|
+
}
|
|
254
|
+
// Reset asset URLs
|
|
255
|
+
if(assetsFolderUrl) {
|
|
256
|
+
onAll(editor, c => {
|
|
257
|
+
if(c.get('type') === 'image' && c.has('tmp-src')) {
|
|
258
|
+
c.set('src', c.get('tmp-src'))
|
|
259
|
+
c.set('tmp-src')
|
|
260
|
+
}
|
|
261
|
+
//// This is handled by the editor.Css.getAll loop
|
|
262
|
+
//if(c.getStyle()['background-image'] && c.has('tmp-bg-url')) {
|
|
263
|
+
// c.setStyle({
|
|
264
|
+
// ...c.getStyle(),
|
|
265
|
+
// 'background-image': `url('${c.get('tmp-bg-url')}')`,
|
|
266
|
+
// })
|
|
267
|
+
// c.set('tmp-bg-url')
|
|
268
|
+
//}
|
|
269
|
+
})
|
|
270
|
+
editor.Css.getAll()
|
|
271
|
+
.forEach(c => {
|
|
272
|
+
if(c.has('tmp-bg-url-css')) {
|
|
273
|
+
c.setStyle({
|
|
274
|
+
...c.getStyle(),
|
|
275
|
+
'background-image': `url('${c.get('tmp-bg-url-css')}')`,
|
|
276
|
+
})
|
|
277
|
+
c.set('tmp-bg-url-css')
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
editor.trigger('publish:start', data)
|
|
282
|
+
let res
|
|
283
|
+
let json
|
|
284
|
+
try {
|
|
285
|
+
res = await fetch(`${ rootUrl }/publish`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
body: JSON.stringify({
|
|
288
|
+
data,
|
|
289
|
+
token: _token,
|
|
290
|
+
}),
|
|
291
|
+
headers: {
|
|
292
|
+
'Content-Type': 'application/json'
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
} catch(e) {
|
|
296
|
+
displayError(editor, `An error occured, your site is not published. ${e.message}`)
|
|
297
|
+
editor.trigger('publish:stop', {success: false, message: e.message})
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
json = await res.json()
|
|
302
|
+
} catch(e) {
|
|
303
|
+
displayError(editor, `Could not parse the server response, your site may be published. ${e.message}`)
|
|
304
|
+
editor.trigger('publish:stop', {success: false, message: e.message})
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
if(!res.ok) {
|
|
308
|
+
displayError(editor, `An network error occured, your site is not published. ${json.message}`)
|
|
309
|
+
editor.trigger('publish:stop', {success: false, message: json.message})
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
url = json.url
|
|
313
|
+
if(json.statusUrl) {
|
|
314
|
+
trackProgress(editor, json.statusUrl)
|
|
315
|
+
} else {
|
|
316
|
+
status = STATUS_SUCCESS
|
|
317
|
+
update(editor)
|
|
318
|
+
editor.trigger('publish:stop', {success: true})
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function getFiles(editor, {siteSettings, publicationSettings, }) {
|
|
323
|
+
return editor.Pages.getAll().map(page => {
|
|
324
|
+
const pageSettings = page.get('settings')
|
|
325
|
+
const pageName = publicationSettings?.autoHomePage !== false && page.get('type') === 'main' ? 'index' : (page.get('name') || page.get('type'))
|
|
326
|
+
function getSetting(name) {
|
|
327
|
+
return (pageSettings || {})[name] || (siteSettings || [])[name] || ''
|
|
328
|
+
}
|
|
329
|
+
const component = page.getMainComponent()
|
|
330
|
+
const slug = getPageSlug(pageName)
|
|
331
|
+
return {
|
|
332
|
+
html: `
|
|
333
|
+
<!DOCTYPE html>
|
|
334
|
+
<html lang="${ getSetting('lang') }">
|
|
335
|
+
<head>
|
|
336
|
+
<link rel="stylesheet" href="${publicationSettings?.css?.url || ''}/${slug}.css" />
|
|
337
|
+
${ siteSettings?.head || '' }
|
|
338
|
+
${ pageSettings?.head || '' }
|
|
339
|
+
<title>${ getSetting('title') }</title>
|
|
340
|
+
<link rel="icon" href="${ getSetting('favicon') }" />
|
|
341
|
+
${
|
|
342
|
+
['description', 'og:title', 'og:description', 'og:image']
|
|
343
|
+
.map(prop => `<meta property="${ prop }" content="${ getSetting(prop) }"/>`)
|
|
344
|
+
.join('\n')
|
|
345
|
+
}
|
|
346
|
+
</head>
|
|
347
|
+
${ editor.getHtml({ component }) }
|
|
348
|
+
</html>
|
|
349
|
+
`,
|
|
350
|
+
css: editor.getCss({ component }),
|
|
351
|
+
cssPath: `${ publicationSettings?.css?.path || '' }/${slug}${publicationSettings?.css?.ext || '.css'}`,
|
|
352
|
+
htmlPath: `${ publicationSettings?.html?.path || '' }/${slug}${publicationSettings?.html?.ext || '.html'}`,
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function trackProgress(editor, statusUrl) {
|
|
358
|
+
let res
|
|
359
|
+
let json
|
|
360
|
+
try {
|
|
361
|
+
res = await fetch(statusUrl)
|
|
362
|
+
} catch(e) {
|
|
363
|
+
displayError(editor, `An error occured, your site is not published. ${e.message}`)
|
|
364
|
+
editor.trigger('publish:stop', {success: false, message: e.message})
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
state = await res.json()
|
|
369
|
+
} catch(e) {
|
|
370
|
+
displayError(editor, `Could not parse the server response, your site may be published. ${e.message}`)
|
|
371
|
+
editor.trigger('publish:stop', {success: false, message: e.message})
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
if(!res.ok) {
|
|
375
|
+
displayError(editor, `An network error occured, your site is not published. ${res.statusText}`)
|
|
376
|
+
editor.trigger('publish:stop', {success: false, message: `An network error occured, your site is not published. ${res.statusText}`})
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
if(state.running) {
|
|
380
|
+
setTimeout(() => trackProgress(editor, statusUrl), 2000)
|
|
381
|
+
} else {
|
|
382
|
+
status = state.error ? STATUS_ERROR : STATUS_SUCCESS
|
|
383
|
+
editor.trigger('publish:stop', {success: state.error})
|
|
384
|
+
}
|
|
385
|
+
update(editor)
|
|
386
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as grapesjs from 'grapesjs/dist/grapes.min.js'
|
|
2
|
+
|
|
3
|
+
const pluginName = 'richText'
|
|
4
|
+
|
|
5
|
+
export const richTextPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => {
|
|
6
|
+
editor.RichTextEditor.add('orderedList', {
|
|
7
|
+
icon: '1.',
|
|
8
|
+
attributes: {title: 'Ordered List'},
|
|
9
|
+
result: rte => rte.exec('insertOrderedList'),
|
|
10
|
+
})
|
|
11
|
+
editor.RichTextEditor.add('unorderedList', {
|
|
12
|
+
icon: '<i class="fa fa-list-ul"></i>',
|
|
13
|
+
attributes: {title: 'Unordered List'},
|
|
14
|
+
result: rte => rte.exec('insertUnorderedList'),
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {html, render} from 'lit-html'
|
|
2
|
+
import {map} from 'lit-html/directives/map.js'
|
|
3
|
+
import grapesjs from 'grapesjs/dist/grapes.min.js'
|
|
4
|
+
|
|
5
|
+
// constants
|
|
6
|
+
const pluginName = 'semantic'
|
|
7
|
+
|
|
8
|
+
const tags = [
|
|
9
|
+
'DIV',
|
|
10
|
+
'P',
|
|
11
|
+
'H1',
|
|
12
|
+
'H2',
|
|
13
|
+
'H3',
|
|
14
|
+
'H4',
|
|
15
|
+
'H5',
|
|
16
|
+
'H6',
|
|
17
|
+
'SPAN',
|
|
18
|
+
'MAIN',
|
|
19
|
+
'ASIDE',
|
|
20
|
+
'SECTION',
|
|
21
|
+
'ADDRESS',
|
|
22
|
+
'ARTICLE',
|
|
23
|
+
'NAV',
|
|
24
|
+
'HEADER',
|
|
25
|
+
'FOOTER',
|
|
26
|
+
'DETAILS',
|
|
27
|
+
'SUMMARY',
|
|
28
|
+
'PRE',
|
|
29
|
+
'BLOCKQUOTE',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// plugin code
|
|
33
|
+
export const semanticPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => {
|
|
34
|
+
// Add the new trait to all component types
|
|
35
|
+
editor.DomComponents.getTypes().map(type => {
|
|
36
|
+
editor.DomComponents.addType(type.id, {
|
|
37
|
+
model: {
|
|
38
|
+
defaults: {
|
|
39
|
+
traits: [
|
|
40
|
+
// Keep the type original traits
|
|
41
|
+
...editor.DomComponents.getType(type.id).model.prototype.defaults.traits,
|
|
42
|
+
// Add the new trait
|
|
43
|
+
{
|
|
44
|
+
label: 'Tag name',
|
|
45
|
+
type: 'tag-name',
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function doRender(el: HTMLElement, tagName: string) {
|
|
54
|
+
const tagsWithCurrent = tags.includes(tagName.toUpperCase()) ? tags : tags.concat(tagName.toUpperCase())
|
|
55
|
+
render(html`
|
|
56
|
+
<label for="semantic__select" class="gjs-one-bg silex-label">Type</label>
|
|
57
|
+
<select id="semantic__select" @change=${event => doRender(el, event.target.value)}>
|
|
58
|
+
${map<string>(tagsWithCurrent, tag => html`
|
|
59
|
+
<option value="${tag}" ?selected=${tagName.toUpperCase() === tag}>${tag}</option>
|
|
60
|
+
`)}
|
|
61
|
+
</select>
|
|
62
|
+
`, el)
|
|
63
|
+
}
|
|
64
|
+
function doRenderCurrent(el: HTMLElement) {
|
|
65
|
+
doRender(el, editor.getSelected()?.get('tagName') || '')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add semantic traits
|
|
69
|
+
// inspired by https://github.com/olivmonnier/grapesjs-plugin-header/blob/master/src/components.js
|
|
70
|
+
editor.TraitManager.addType('tag-name', {
|
|
71
|
+
createInput({ trait }) {
|
|
72
|
+
// Create a new element container and add some content
|
|
73
|
+
const el = document.createElement('div')
|
|
74
|
+
// update the UI when a page is added/renamed/removed
|
|
75
|
+
editor.on('page', () => doRenderCurrent(el))
|
|
76
|
+
doRenderCurrent(el)
|
|
77
|
+
// this will be the element passed to onEvent and onUpdate
|
|
78
|
+
return el
|
|
79
|
+
},
|
|
80
|
+
// Update the component based on UI changes
|
|
81
|
+
// `elInput` is the result HTMLElement you get from `createInput`
|
|
82
|
+
onEvent({ elInput, component, event }) {
|
|
83
|
+
const value = elInput.querySelector('#semantic__select').value
|
|
84
|
+
if(component.get('tagName').toUpperCase() === value.toUpperCase()){
|
|
85
|
+
// Already done
|
|
86
|
+
} else {
|
|
87
|
+
component.set('tagName', value)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
// Update UI on the component change
|
|
91
|
+
onUpdate({ elInput, component }) {
|
|
92
|
+
const tagName = component.get('tagName')
|
|
93
|
+
doRender(elInput, tagName)
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {html, render} from 'lit-html'
|
|
2
|
+
import {live} from 'lit-html/directives/live.js'
|
|
3
|
+
import * as grapesjs from 'grapesjs/dist/grapes.min.js'
|
|
4
|
+
|
|
5
|
+
import { WebsiteSettings } from '../../types'
|
|
6
|
+
|
|
7
|
+
const pluginName = 'settings-dialog'
|
|
8
|
+
const el = document.createElement('div')
|
|
9
|
+
let modal
|
|
10
|
+
|
|
11
|
+
// Get version from webpack (see plugin in webpack.config.js)
|
|
12
|
+
declare const VERSION: string
|
|
13
|
+
|
|
14
|
+
export const cmdOpenSettings = 'open-settings'
|
|
15
|
+
|
|
16
|
+
let version = 'v3'
|
|
17
|
+
export const settingsDialog = grapesjs.plugins.add(pluginName, (editor, opts) => {
|
|
18
|
+
// Display Silex version from package.json
|
|
19
|
+
try {
|
|
20
|
+
version = VERSION
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Cannot get version from webpack (see plugin in webpack.config.js)
|
|
23
|
+
}
|
|
24
|
+
console.log('Silex version:', version)
|
|
25
|
+
editor.Commands.add(cmdOpenSettings, {
|
|
26
|
+
run: (_, sender, {page}) => {
|
|
27
|
+
modal = editor.Modal.open({
|
|
28
|
+
title: page ? 'Page settings' : 'Site Settings',
|
|
29
|
+
content: '',
|
|
30
|
+
attributes: { class: 'settings-dialog' },
|
|
31
|
+
})
|
|
32
|
+
.onceClose(() => {
|
|
33
|
+
editor.stopCommand(cmdOpenSettings) // apparently this is needed to be able to run the command several times
|
|
34
|
+
})
|
|
35
|
+
displaySettings(editor, opts, page)
|
|
36
|
+
modal.setContent(el)
|
|
37
|
+
const form = el.querySelector('form')
|
|
38
|
+
form.onsubmit = event => {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
saveSettings(editor, opts, page)
|
|
41
|
+
editor.stopCommand(cmdOpenSettings)
|
|
42
|
+
}
|
|
43
|
+
form.querySelector('input')?.focus()
|
|
44
|
+
return modal
|
|
45
|
+
},
|
|
46
|
+
stop: () => {
|
|
47
|
+
modal.close()
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
// add settings to the website
|
|
51
|
+
editor.on('storage:start:store', (data) => {
|
|
52
|
+
data.settings = editor.getModel().get('settings')
|
|
53
|
+
data.name = editor.getModel().get('name')
|
|
54
|
+
})
|
|
55
|
+
editor.on('storage:end:load', (data) => {
|
|
56
|
+
const model = editor.getModel()
|
|
57
|
+
model.set('settings', data.settings || {})
|
|
58
|
+
model.set('name', data.name)
|
|
59
|
+
updateDom(editor)
|
|
60
|
+
})
|
|
61
|
+
editor.on('page', (e) => {
|
|
62
|
+
editor.Canvas.getFrameEl().addEventListener('load', () => {
|
|
63
|
+
updateDom(editor)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Is the model a site or a page?
|
|
69
|
+
function isSite(model) { return !!model.getHtml }
|
|
70
|
+
|
|
71
|
+
function displaySettings(editor, config, model = editor.getModel()) {
|
|
72
|
+
const settings = model.get('settings') || {} as WebsiteSettings
|
|
73
|
+
model.set('settings', settings)
|
|
74
|
+
render(html`
|
|
75
|
+
<p>The page settings will override site settings, <a href="https://github.com/silexlabs/Silex/wiki/Settings" target="_blank">more info about settings here</a>.</p>
|
|
76
|
+
<form class="silex-form">
|
|
77
|
+
<h2>General settings</h2>
|
|
78
|
+
<div class="silex-form__group col2">
|
|
79
|
+
<label class="silex-form__element">
|
|
80
|
+
<h3>${isSite(model) ? 'Site name' : 'Page name'}</h3>
|
|
81
|
+
<p>${isSite(model) ? 'The project name in the editor, for you and your team.' : 'Label of the page in the editor, and file name of the HTML page.'}</p>
|
|
82
|
+
<input type="text" name="name" .value=${live(model.get('name') || '')}/>
|
|
83
|
+
</label>
|
|
84
|
+
<label class="silex-form__element">
|
|
85
|
+
<h3>Website language</h3>
|
|
86
|
+
<p>This is the default language code for this website. Example: en, fr, es...</p>
|
|
87
|
+
<input type="text" name="lang" .value=${live(settings.lang || '')}/>
|
|
88
|
+
</label>
|
|
89
|
+
<label class="silex-form__element">
|
|
90
|
+
<h3>Title</h3>
|
|
91
|
+
<p>Title of the browser window, and title in the search engines results. It is used by search engines to find out what your site is talking about. The title should be a maximum of 70 characters long, including spaces.</p>
|
|
92
|
+
<input type="text" name="title" .value=${live(settings.title || '')}/>
|
|
93
|
+
</label>
|
|
94
|
+
<label class="silex-form__element">
|
|
95
|
+
<h3>Description</h3>
|
|
96
|
+
<p>Description displayed by the search engines in search results. It is used by search engines to find out what your site is talking about. It is best to keep meta descriptions between 150 and 160 characters.</p>
|
|
97
|
+
<input type="text" name="description" .value=${live(settings.description || '')}/>
|
|
98
|
+
</label>
|
|
99
|
+
<label class="silex-form__element">
|
|
100
|
+
<h3>Favicon</h3>
|
|
101
|
+
<p>Small image displayed in the browser's address bar and in tabs. The recommended size is 16×16 or 32×32 pixels. This can be a URL or a relative path.</p>
|
|
102
|
+
<input type="text" name="favicon" .value=${live(settings.favicon || '')}/>
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
<h2>Social networks sharing</h2>
|
|
106
|
+
<div>
|
|
107
|
+
<p>Once your website is live, you can use these tools to test sharing: <a target="_blank" href="https://developers.facebook.com/tools/debug/">Facebook</a>,
|
|
108
|
+
<a target="_blank" href="https://cards-dev.twitter.com/validator">Twitter</a>,
|
|
109
|
+
<a target="_blank" href="https://www.linkedin.com/post-inspector/inspect/">Linkedin</a></p>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="silex-form__group col2">
|
|
112
|
+
<label class="silex-form__element">
|
|
113
|
+
<h3>Title</h3>
|
|
114
|
+
<p>The title of your website displayed when a user shares your website on a social network.
|
|
115
|
+
Do not include any branding in this title, just eye-catching phrase, e.g. "Learn everything about fishing".
|
|
116
|
+
Title should be between 60 and 90 characters long.</p>
|
|
117
|
+
<input type="text" name="og:title" .value=${live(settings['og:title'] || '')}/>
|
|
118
|
+
</label>
|
|
119
|
+
<label class="silex-form__element">
|
|
120
|
+
<h3>Description</h3>
|
|
121
|
+
<p>Description displayed when a user shares your website on a social network. Make it catchy, and invite readers to visit your website too, e.g. "Sam's website about fishing, check it out!" Title should be between 60 and 90 characters long.</p>
|
|
122
|
+
<input type="text" name="og:description" .value=${live(settings['og:description'] || '')}/>
|
|
123
|
+
</label>
|
|
124
|
+
<label class="silex-form__element">
|
|
125
|
+
<h3>Image</h3>
|
|
126
|
+
<p>Thumbnail image which is displayed when your website is shared on a social network. The optimal size is 1200×627 pixels. At this size, your thumbnail will be big and stand out from the crowd. But do not exceed the 5MB size limit. If you use an image that is smaller than 400 pixels x 209 pixels, it will render as a much smaller thumbnail.</p>
|
|
127
|
+
<p>Please enter the full URL here, e.g. "http://mysite.com/path/to/image.jpg"</p>
|
|
128
|
+
<input type="text" name="og:image" .value=${live(settings['og:image'] || '')}/>
|
|
129
|
+
</label>
|
|
130
|
+
</div>
|
|
131
|
+
<h2>Code</h2>
|
|
132
|
+
<div class="silex-form__group">
|
|
133
|
+
<label class="silex-form__element">
|
|
134
|
+
<h3>HTML head</h3>
|
|
135
|
+
<p>HTML code which will be inserted in the HEAD tag.</p>
|
|
136
|
+
<textarea name="head" .value=${live(settings.head || '')}></textarea>
|
|
137
|
+
</label>
|
|
138
|
+
</div>
|
|
139
|
+
<footer>
|
|
140
|
+
<p class="silex-version">Silex ${version}</p>
|
|
141
|
+
<input class="silex-button" type="button" @click=${e => editor.stopCommand(cmdOpenSettings)} value="Cancel">
|
|
142
|
+
<input class="silex-button" type="submit" value="Ok">
|
|
143
|
+
</footer>
|
|
144
|
+
</form>
|
|
145
|
+
`, el)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function saveSettings(editor, config, model = editor.getModel()) {
|
|
149
|
+
const form = el.querySelector('form')
|
|
150
|
+
const formData = new FormData(form)
|
|
151
|
+
const data = Array.from(formData as any)
|
|
152
|
+
.reduce((aggregate, [key, value]) => {
|
|
153
|
+
aggregate[key] = value
|
|
154
|
+
return aggregate
|
|
155
|
+
}, {}) as {[key: string]: any}
|
|
156
|
+
// take the name out to the main model (by design in grapesjs pages)
|
|
157
|
+
const { name, ...settings } = data
|
|
158
|
+
model.set({
|
|
159
|
+
settings,
|
|
160
|
+
name,
|
|
161
|
+
})
|
|
162
|
+
// save if auto save is on
|
|
163
|
+
editor.getModel().set('changesCount', editor.getDirtyCount() + 1)
|
|
164
|
+
// update the DOM
|
|
165
|
+
updateDom(editor)
|
|
166
|
+
}
|
|
167
|
+
function getHeadContainer(doc, className) {
|
|
168
|
+
const container = doc.head.querySelector(`.${className}`)
|
|
169
|
+
if(container) {
|
|
170
|
+
return container
|
|
171
|
+
}
|
|
172
|
+
const newContainer = doc.createElement('div')
|
|
173
|
+
newContainer.classList.add(className)
|
|
174
|
+
doc.head.appendChild(newContainer)
|
|
175
|
+
return newContainer
|
|
176
|
+
}
|
|
177
|
+
function updateDom(editor) {
|
|
178
|
+
const doc = editor.Canvas.getDocument()
|
|
179
|
+
if(doc) {
|
|
180
|
+
// Site head
|
|
181
|
+
getHeadContainer(doc, 'site-head')
|
|
182
|
+
.innerHTML = editor.getModel().get('settings').head || ''
|
|
183
|
+
// Pages head
|
|
184
|
+
getHeadContainer(doc, 'page-head')
|
|
185
|
+
.innerHTML = editor.Pages.getSelected().get('settings')?.head || ''
|
|
186
|
+
} else {
|
|
187
|
+
// No document??
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|