@silexlabs/grapesjs-data-source
Version:
Grapesjs Data Source
237 lines (220 loc) • 7.79 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 {LitElement, html} from 'lit'
import { ref } from 'lit/directives/ref.js'
import {customElement, property} from 'lit/decorators.js'
import './state-editor'
import { StateEditor } from './state-editor'
import { Component } from 'grapesjs'
import { PROPERTY_STYLES } from './defaultStyles'
import { fromStored } from '../model/token'
import { BinariOperator, DataSourceEditor, Properties, Token, UnariOperator } from '../types'
import { getState, setState } from '../model/state'
import { DataTree } from '../model/DataTree'
import { getFixedToken } from '../utils'
/**
* Editor for selected element's properties
*
* Usage:
*
* ```
* <properties-editor disabled>
* <style> / * Custom styles * / </style>
* </properties-editor>
* ```
*
*/
('properties-editor')
export class PropertiesEditor extends LitElement {
({type: Boolean})
disabled = false
({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: DataSourceEditor | null = null
private redrawing = false
setEditor(editor: DataSourceEditor) {
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</div>
</div>
<details class="ds-states__help">
<summary>Help</summary>
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#properties">Learn more about element properties</a>
</details>
<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">
<div>
<div class="gjs-traits-label">Visibility</div>
</div>
<main>
${this.renderStateEditor(selected, '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(BinariOperator))
.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(BinariOperator).includes(selected.get('conditionOperator'))) }
</main>
</section>
<section class="ds-section">
<div>
<label class="gjs-traits-label ds-label">Loop</label>
</div>
<main>
${this.renderStateEditor(selected, '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(this.editor!.DataSourceManager.getDataTree(), 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
setState(component, name, {
expression: stateEditor.data,
}, publicState)
}
getTokens(dataTree: DataTree, component: Component, name: Properties, publicState: boolean): Token[] {
const state = getState(component, name, publicState)
if(!state || !state.expression) return []
return state.expression.map(token => fromStored(token, dataTree, component.getId()))
}
}
declare global {
interface HTMLElementTagNameMap {
'properties-editor': PropertiesEditor
}
}