UNPKG

@silexlabs/silex

Version:

Free and easy website builder for everyone.

283 lines (268 loc) 10.4 kB
/* * 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?? } }