UNPKG

@silexlabs/grapesjs-data-source

Version:
250 lines (232 loc) 8.23 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 {LitElement, html} from 'lit' import { ref } from 'lit/directives/ref.js' import {property} from 'lit/decorators.js' import './state-editor' import { StateEditor } from './state-editor' import { Component, Editor } from 'grapesjs' import { PROPERTY_STYLES } from './defaultStyles' import { fromStored } from '../model/token' import { BinaryOperator, Properties, Token, UnariOperator } from '../types' import { getState, setState } from '../model/state' import { getFixedToken } from '../utils' /** * Editor for selected element's properties * * Usage: * * ``` * <properties-editor disabled> * <style> / * Custom styles * / </style> * </properties-editor> * ``` * */ export class PropertiesEditor extends LitElement { @property({type: Boolean}) disabled = false @property({type: Boolean, attribute: 'default-fixed'}) defaultFixed = false inputs: Record<Properties, {stateEditor: StateEditor, selected: Component | undefined} | undefined> = { innerHTML: undefined, condition: undefined, condition2: undefined, __data: undefined, } private editor: Editor | null = null private redrawing = false setEditor(editor: Editor) { if (this.editor) { console.warn('property-editor setEditor already set') return } this.editor = editor // Update the UI when a page is added/renamed/removed this.editor.on('page', () => this.requestUpdate()) // Update the UI on component selection change this.editor.on('component:selected', () => this.requestUpdate()) // Update the UI on component change this.editor.on('component:update', () => this.requestUpdate()) } override render() { super.render() this.redrawing = true const selected = this.editor?.getSelected() const head = html` <style> ${PROPERTY_STYLES} </style> <slot></slot> ` const empty = html` ${head} <p class="ds-empty">Select an element to edit its properties</p> ` if(!this.editor || this.disabled) { this.resetInputs() this.redrawing = false return html`` } if(!selected || selected.get('tagName') === 'body') { this.resetInputs() this.redrawing = false return empty } const result = html` ${head} <section class="ds-section"> <div> <div class="gjs-traits-label"> Properties <details class="ds-states__help"> <summary>Help</summary> <div class="ds-states__help--tooltip"> Elements properties are expressions that can replace the HTML attributes of the element or it's whole content (innerHTML). <a target="_blank" href="https://docs.silex.me/en/user/cms-concepts#properties">Learn more about element properties</a> </div> </details> </div> </div> <main> ${[ {label: 'HTML content', name: Properties.innerHTML, publicState: false}, ].map(({label, name, publicState}) => this.renderStateEditor(selected, label, name, publicState))} </main> </section> <section class="ds-section"> <main> ${this.renderStateEditor(selected, 'Visibility Condition', Properties.condition, false)} <div> <span>... is</span> <select class="ds-visibility__condition-operator" @change=${(e: Event) => { const select = e.target as HTMLSelectElement const value = select.value if(!value) throw new Error('Selection required for operator select element') selected.set('conditionOperator', value) this.requestUpdate() }} > </div> ${ Object.values<string>(UnariOperator) .concat(Object.values(BinaryOperator)) .map(operator => html` <option value="${operator}" .selected=${selected.get('conditionOperator') === operator} >${operator}</option> `) } </select> ${ this.renderStateEditor(selected, '', Properties.condition2, false, false, selected.has('conditionOperator') && Object.values(BinaryOperator).includes(selected.get('conditionOperator'))) } </main> </section> <section class="ds-section"> <main> ${this.renderStateEditor(selected, 'Loop Data', Properties.__data, false, true)} </main> </section> ` this.redrawing = false return result } resetInputs() { this.inputs = { innerHTML: undefined, condition: undefined, condition2: undefined, __data: undefined, } } renderStateEditor(selected: Component, label: string, name: Properties, publicState: boolean, hideLoopData = false, visible = true) { return html` <state-editor .style=${visible ? '' : 'display: none;'} .selected=${selected} .editor=${this.editor} id="${name}" name=${name} default-fixed=${this.defaultFixed} ?hide-loop-data=${hideLoopData} ${ref(el => { // Get the stateEditor ref if (el) { // Set the editor - we could do this only once const stateEditor = el as StateEditor // Store the stateEditor ref and the component it is representing if (!this.inputs[name]) { this.inputs[name] = { stateEditor, selected: undefined, // clear the selected component so that we update the data } } } // Finally update the data if (this.inputs[name]) { const stateEditorFinally = this.inputs[name]!.stateEditor this.redrawing = true try { stateEditorFinally.data = this.getTokens(selected, name, publicState) } catch (e) { console.error('Error setting data', e) stateEditorFinally.data = [getFixedToken(`Error setting data: ${e}`)] } this.redrawing = false // Store the selected component this.inputs[name]!.selected = selected } })} @change=${() => this.onChange(selected, name, publicState)} ?disabled=${this.disabled} > <label slot="label">${label}</label> </state-editor> ` } onChange(component: Component, name: Properties, publicState: boolean) { const {stateEditor} = this.inputs[name]! if(this.redrawing) return if (name === Properties.__data) { // Handle the case when data is empty (after clearing) if (stateEditor.data.length === 0) { setState(component, name, { expression: [], }, publicState) } else { setState(component, name, { expression: stateEditor.data.slice(0, -1).concat({ ...stateEditor.data[stateEditor.data.length - 1], previewIndex: 0, } as unknown as Token), }, publicState) } } else { setState(component, name, { expression: stateEditor.data, }, publicState) } } getTokens(component: Component, name: Properties, publicState: boolean): Token[] { const state = getState(component, name, publicState) if(!state || !state.expression) return [] return state.expression .filter(token => token && typeof token === 'object' && token.type) // Filter out invalid tokens .map(token => fromStored(token, component.getId())) } } if(!window.customElements.get('properties-editor')) { window.customElements.define('properties-editor', PropertiesEditor) }