UNPKG

@silexlabs/grapesjs-data-source

Version:
373 lines (339 loc) 11.3 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 { StoredState, getState, getStateIds, removeState, setState } from '../model/state' import { Token } from '../types' import './state-editor' import { StateEditor } from './state-editor' import { Component, Editor } from 'grapesjs' import { PROPERTY_STYLES } from './defaultStyles' import { fromStored } from '../model/token' import { cleanStateName } from '../utils' interface Item { name: string publicState?: boolean state: StoredState } /** * Editor for selected element's states * */ export class CustomStatesEditor extends LitElement { @property({type: Boolean}) disabled = false @property({type: Boolean, attribute: 'private-state'}) privateState = false @property({type: String}) title = 'Custom states' @property({type: Boolean, attribute: 'default-fixed'}) defaultFixed = false @property({type: String, attribute: 'create-prompt'}) createPrompt = 'Name this state' @property({type: String, attribute: 'rename-prompt'}) renamePrompt = 'Rename this state' @property({type: String, attribute: 'default-name'}) defaultName = 'New state' // This is a comma separated list of reserved names // Or an array of reserved names @property({type: String, attribute: 'reserved-names'}) get reservedNames() { return this._reservedNames } set reservedNames(value: string | string[]) { if(typeof value === 'string') this._reservedNames = value.split(',').map(s => s.trim()) else this._reservedNames = value } @property({type: Boolean, attribute: 'hide-loop-data'}) hideLoopData = false @property({type: String, attribute: 'help-text'}) helpText = '' @property({type: String, attribute: 'help-link'}) helpLink = '' private _reservedNames: string[] = [] 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()) } getHead(selected: Component | null) { return html` <style> ${PROPERTY_STYLES} </style> <slot></slot> <section class="ds-section"> <div> <div class="gjs-traits-label"> <span>${this.title}</span> <span> ${ selected ? html` <button title="Add a new state" class="ds-states__add-button ds-states__button" @click=${() => { const item = this.createCustomState(selected) if(!item) return this.setState(selected, item.name, item.state) }} >+</button> ` : ''} ${this.helpText ? html` <details class="ds-states__help"> <summary>Help</summary> <div class="ds-states__help--tooltip"> <span>${ this.helpText }</span> ${this.helpLink ? html` <a class="ds-states__help-link" href="${this.helpLink}" target="_blank" >\u{1F517} Read more...</a> ` : ''} </div> </details> ` : ''} </span> </div> </div> </section> ` } override render() { super.render() this.redrawing = true const selected = this.editor?.getSelected() const empty = html` ${this.getHead(null)} <p class="ds-empty">Select an element to edit its states</p> ` if(!this.editor || this.disabled) { this.redrawing = false return html`` } if(!selected) { this.redrawing = false return empty } const items: Item[] = this.getStateIds(selected) .map(stateId => ({ name: stateId, publicState: !this.privateState, state: this.getState(selected, stateId)!, })) .filter(item => item.state && !item.state.hidden) const result = html` ${this.getHead(selected)} <div class="ds-states"> <div class="ds-states__items"> ${ items.length === 0 ? html` <p>Use the "+" button to add elements to this list</p> ` : ''} ${items .map((item, index) => html` <div class="ds-states__item"> ${this.getStateEditor(selected, item.state.label || '', item.name)} <div class="ds-states__buttons"> <button title="Remove this state" class="ds-states__remove-button ds-states__button" @click=${() => { this.removeState(selected, item.name) this.requestUpdate() }} >x</button> <button title="Rename this state" class="ds-states__rename-button ds-states__button" @click=${() => { const newItem = this.renameCustomState(item) if(!newItem || newItem === item) return this.removeState(selected, item.name) this.setState(selected, newItem.name, newItem.state) this.requestUpdate() }} >\u270F</button> <button title="Move this state up" class="ds-states__item-move-up ds-states__button${ index === 0 ? ' ds-states__button--disabled' : '' }" @click=${() => { items.splice(index - 1, 0, items.splice(index, 1)[0]) this.updateOrderCustomStates(selected, items) }} >\u2191</button> <button title="Move this state down" class="ds-states__item-move-down ds-states__button${ index === items.length - 1 ? ' ds-states__button--disabled' : '' }" @click=${() => { items.splice(index + 1, 0, items.splice(index, 1)[0]) this.updateOrderCustomStates(selected, items) }} >\u2193</button> </div> </div> <hr class="ds-states__sep" /> `)} </div> </div> ` this.redrawing = false return result } /** * Get the states for this type of editor */ getStateIds(component: Component): string[] { return getStateIds(component, !this.privateState) // Filter out the states which are properties .filter(stateId => !this.reservedNames.includes(stateId)) } /** * Get the states for this type of editor */ getState(component: Component, name: string): StoredState | null { return getState(component, name, !this.privateState) } /** * Set the states for this type of editor */ setState(component: Component, name: string, state: StoredState) { setState(component, name, state, !this.privateState) } /** * Remove the states for this type of editor */ removeState(component: Component, name: string) { removeState(component, name, !this.privateState) } getStateEditor(selected: Component, label: string, name: string) { return html` <state-editor .selected=${selected} .editor=${this.editor} id="${name}" name=${name} ?hide-loop-data=${this.hideLoopData} default-fixed=${this.defaultFixed} ${ref(el => { if (el) { const stateEditor = el as StateEditor stateEditor.data = this.getTokens(selected, name) } })} @change=${() => this.onChange(selected, name, label)} .disabled=${this.disabled} > <label slot="label">${label || name}</label> </state-editor> ` } onChange(component: Component, name: string, label: string) { if(this.redrawing) return const stateEditor = this.shadowRoot!.querySelector(`#${name}`) as StateEditor this.setState(component, name, { expression: stateEditor.data, label, }) } getTokens(component: Component, name: string): Token[] { const state = this.getState(component, name) if(!state || !state.expression) return [] return state.expression.map(token => { try { return fromStored(token, component.getId()) } catch { // FIXME: notify user console.error('Error while getting expression result type in getTokens', {expression: state.expression, component, name}) return { type: 'property', propType: 'field', fieldId: 'unknown', label: 'unknown', kind: 'scalar', typeIds: [], } } }) } /** * Rename a custom state */ renameCustomState(item: Item): Item { const label = prompt(this.renamePrompt, item.state.label) ?.toLowerCase() ?.replace(/[^a-z0-9]/g, '-') ?.replace(/^-+|-+$/g, '') if (!label || label === item.state.label) return item return { ...item, state: { ...item.state, label: label, }, } } /** * Update the custom states, in the order of the list */ updateOrderCustomStates(component: Component, items: Item[]) { const stateIds = this.getStateIds(component) // Remove all states stateIds.forEach(stateId => { if(items.map(item => item.name).includes(stateId)) { this.removeState(component, stateId) } }) // Add states in the order of the list items.forEach(item => { this.setState(component, item.name, item.state) }) } /** * Create a new custom state */ createCustomState(component: Component): Item | null { const label = cleanStateName(prompt(this.createPrompt, this.defaultName)) if (!label) return null if(this.reservedNames.includes(label)) { alert(`The name ${label} is reserved, please choose another name`) return null } const stateId = `${component.getId()}-${Math.random().toString(36).slice(2)}` const state: StoredState = { label, expression: [], } this.setState(component, stateId, state) return { name: stateId, state, publicState: !this.privateState, } } } if(!customElements.get('custom-states-editor')) { customElements.define('custom-states-editor', CustomStatesEditor) }