@silexlabs/silex
Version:
Free and easy website builder for everyone.
283 lines (268 loc) • 10.4 kB
text/typescript
/*
* Silex website builder, free/libre no-code tool for makers.
* Copyright (c) 2023 lexoyo and Silex Labs foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {html, render, TemplateResult} from 'lit-html'
import {defaultSections, idCodeWrapper, isSite, SettingsSection} from './settings-sections'
/**
* @fileoverview This file contains the settings dialog. The config API lets plugins add sections to the settings dialog.
*/
import { WebsiteSettings } from '../../types'
import { ClientEvent } from '../events'
import { SILEX_VERSION } from '../../constants'
const sectionsSite = [...defaultSections]
const sectionsPage = [...defaultSections]
const el = document.createElement('div')
let modal
export const cmdOpenSettings = 'open-settings'
let headEditor = null
export const settingsDialog = (editor, opts) => {
editor.Commands.add(cmdOpenSettings, {
run: (_, sender, {page}) => {
modal = editor.Modal.open({
title: page ? 'Page settings' : 'Site Settings',
content: '',
attributes: { class: 'settings-dialog' },
})
.onceClose(() => {
sender?.set && sender.set('active', 0) // Deactivate the button to make it ready to be clicked again, only in case of a grapesjs button (not in the pages panel)
editor.stopCommand(cmdOpenSettings) // apparently this is needed to be able to run the command several times
})
displaySettings(editor, opts, page)
modal.setContent(el)
const form = el.querySelector('form')
form.onsubmit = event => {
event.preventDefault()
editor.trigger(ClientEvent.SETTINGS_SAVE_START, page)
saveSettings(editor, opts, page)
editor.trigger(ClientEvent.SETTINGS_SAVE_END, page)
editor.stopCommand(cmdOpenSettings)
}
form.querySelector('input')?.focus()
// Notify other plugins
editor.trigger(ClientEvent.SETTINGS_OPEN, page)
// Return the dialog
return modal
},
stop: () => {
modal.close()
editor.trigger(ClientEvent.SETTINGS_CLOSE)
},
})
// add settings to the website
editor.on('storage:start:store', (data) => {
data.settings = editor.getModel().get('settings')
data.name = editor.getModel().get('name')
})
editor.on('storage:end:load', (data) => {
const model = editor.getModel()
model.set('settings', data.settings || {})
model.set('name', data.name)
})
// Normal way to apply the head content to the DOM
// fix #1559 Custom <head> code must be reapplied
editor.on('canvas:frame:load', () => {
updateDom(editor)
})
// When the page changes, update the dom
editor.on('page', (e) => {
updateDom(editor)
})
headEditor = editor.CodeManager.createViewer({
readOnly: false,
codeName: 'htmlmixed',
lineNumbers: true,
lineWrapping: true,
autoFormat: false,
})
}
function showSection(item: SettingsSection) {
// **
// Handle the side bar
const aside = el.querySelector('aside.silex-bar') as HTMLElement
const ul = aside.querySelector('ul.silex-list--menu') as HTMLUListElement
const li = ul.querySelector('li#settings-sidebar-' + item.id) as HTMLLIElement
currentSection = item
if(!li) {
console.warn('Cannot find section', item.id, 'in the side bar, fallback to the first section')
if(!defaultSections[0] || defaultSections[0].id === item.id) {
console.error('Cannot find the default section in the side bar')
return
}
showSection(defaultSections[0]) // Fallback to the first section
return
}
// Update active
Array.from(ul.querySelectorAll('.active')).forEach(el => el.classList.remove('active'))
li.classList.add('active')
// **
// Handle the main section
const main = el.querySelector('main#settings__main') as HTMLElement
const mainItem = main.querySelector(`#settings-${item.id}`)
if(!mainItem) {
console.warn('Cannot find section', item.id, 'in the settings dialog, fallback to the first section')
if(!defaultSections[0] || defaultSections[0].id === item.id) {
console.error('Cannot find the default section in the settings dialog')
return
}
showSection(defaultSections[0]) // Fallback to the first section
return
}
// Update hidden
Array.from(main.querySelectorAll('.silex-hideable')).forEach(el => el.classList.add('silex-hidden'))
mainItem.classList.remove('silex-hidden')
// This messes up with the save / cancel mechanism
// displaySettings(editor, config, model)
// Refresh the code editor just in case it went from hidden to visible
// This makes it ready to be used when the user clicks on the tab
headEditor.refresh()
}
export function addSection(section, siteOrPage: 'site' | 'page', position: 'first' | 'last' | number) {
const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
if (position === 'first') {
sections.unshift(section)
} else if (position === 'last') {
sections.push(section)
} else if (typeof position === 'number') {
sections.splice(position, 0, section)
} else {
throw new Error('Invalid position for settings section')
}
}
export function removeSection(id, siteOrPage: 'site' | 'page') {
const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
const index = sections.findIndex(section => section.id === id)
if(index === -1) throw new Error(`Cannot find section with id ${id}`)
sections.splice(index, 1)
}
let currentSection
function displaySettings(editor, config, model = editor.getModel()) {
// Update the model with the current settings
const settings = model.get('settings') || {} as WebsiteSettings
model.set('settings', settings)
// Get the current sections for page or site
const menuItemsCurrent = isSite(model) ? sectionsSite : sectionsPage
// Init the current section selection
currentSection = currentSection || menuItemsCurrent[0]
const sections: TemplateResult[] = menuItemsCurrent.map(item => {
try {
return item.render(settings, model)
} catch (e) {
console.error('Error rendering settings section', item.id, e)
return html`<div class="silex-error">Error rendering settings section ${item.id}</div>`
}
})
// Render the settings dialog
render(html`
<form class="silex-form">
<div class="silex-help">
${isSite(model) ? html`
<p>Here you can set the name of your website, SEO and social networks sharing data.</p>
<p>These settings are overriden by the page settings, <a href="https://github.com/silexlabs/Silex/wiki/Settings" target="_blank">more info about settings here</a>.</p>
` : html`
<p>Here you can set the name of your page, SEO and social networks sharing data.</p>
<p>These settings override the site settings, <a href="https://github.com/silexlabs/Silex/wiki/Settings" target="_blank">more info about settings here</a>.</p>
`}
</div>
<section class="silex-sidebar-dialog">
<aside class="silex-bar">
<ul class="silex-list silex-list--menu">
${menuItemsCurrent.map(item => html`
<li
id="settings-sidebar-${item.id}"
class=${item.id === currentSection.id ? 'active' : ''}
@click=${e => {
e.preventDefault()
// Show the new section
showSection(item)
// Notify other plugins
editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
}}
>
${item.label}
</li>
`)}
</ul>
</aside>
<main id="settings__main">
${ sections }
</main>
</section>
<footer>
<p class="silex-version">Silex ${SILEX_VERSION}</p>
<input class="silex-button" type="button" @click=${e => editor.stopCommand(cmdOpenSettings)} value="Cancel">
<input class="silex-button" type="submit" value="Apply">
</footer>
</form>
`, el)
// Display the current section
showSection(currentSection)
// Init the code editor
el.querySelector(`#${idCodeWrapper}`)?.appendChild(headEditor.getElement())
headEditor.setContent(settings.head || '')
}
function saveSettings(editor, config, model = editor.getModel()) {
const form = el.querySelector('form')
const formData = new FormData(form)
const data = Array.from(formData as any)
.reduce((aggregate, [key, value]) => {
// Keep only the values that are set
if(value !== '') {
aggregate[key] = value
}
return aggregate
}, {}) as {[key: string]: any}
// take the name out to the main model (by design in grapesjs pages)
const { name, ...settings } = data
model.set({
settings: {
...data,
head: headEditor.getContent(),
},
name,
})
// save if auto save is on
editor.getModel().set('changesCount', editor.getDirtyCount() + 1)
// update the DOM
updateDom(editor)
}
function getHeadContainer(doc, className) {
const container = doc.head.querySelector(`.${className}`)
if(container) {
return container
}
const newContainer = doc.createElement('div')
newContainer.classList.add(className)
doc.head.appendChild(newContainer)
return newContainer
}
function updateDom(editor) {
const doc = editor.Canvas.getDocument()
const settings = editor.getModel().get('settings')
const pageSettings = editor.Pages.getSelected().get('settings')
if(doc && settings) {
// Delay the update to let the DOM be ready
setTimeout(() => {
// Site head
getHeadContainer(doc, 'site-head')
.innerHTML = settings.head || ''
// Pages head
getHeadContainer(doc, 'page-head')
.innerHTML = pageSettings?.head || ''
})
} else {
// No document??
}
}