UNPKG

@silexlabs/grapesjs-data-source

Version:
564 lines 22.5 kB
"use strict"; /* * 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/>. */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StateEditor = void 0; const lit_1 = require("lit"); const decorators_js_1 = require("lit/decorators.js"); const ref_js_1 = require("lit/directives/ref.js"); const style_map_js_1 = require("lit/directives/style-map.js"); const defaultStyles_1 = require("./defaultStyles"); const types_1 = require("../types"); const utils_1 = require("../utils"); require("@silexlabs/expression-input"); const completion_1 = require("../model/completion"); const expressionEvaluator_1 = require("../model/expressionEvaluator"); const dataSourceRegistry_1 = require("../model/dataSourceRegistry"); const dataSourceManager_1 = require("../model/dataSourceManager"); const token_1 = require("../model/token"); /** * Editor for a state of the selected element's properties * * Usage: * * ``` * <state-editor * name="state" * disabled * hide-loop-data * parent-name="parent" * no-filters * root-type="root" * default-fixed * dismiss-current-component-states * ></state-editor> * ``` * */ class StateEditor extends lit_1.LitElement { constructor() { super(...arguments); this.disabled = false; this.name = ''; this.hideLoopData = false; /** * used in the expressions found in filters options * This will be used to filter states which are not defined yet */ this.parentName = ''; this.noFilters = false; this.rootType = ''; this.defaultFixed = false; // Note: dismissCurrentComponentStates not used in this project anymore this.dismissCurrentComponentStates = false; this._selected = null; /** * Form id * This is the same API as input elements */ this.for = ''; /** * Binded listeners */ this.onFormdata_ = this.onFormdata.bind(this); this.renderBinded = () => this.requestUpdate(); /** * Form setter * Handle formdata event to add the current value to the form */ this._form = null; /** * Structured data */ this._data = []; this._editor = null; this.redrawing = false; this.expressionInputRef = (0, ref_js_1.createRef)(); this.popinsRef = []; } get selected() { return this._selected; } set selected(value) { this._selected = value; this.requestUpdate(); } /** * Value string for for submissions */ get value() { return JSON.stringify(this.data); } set value(newValue) { const expression = (0, utils_1.toExpression)(newValue); if (!expression) { this.data = newValue; return; } this.data = expression; } connectedCallback() { var _a; super.connectedCallback(); // Use the form to add formdata if (this.for) { const form = document.querySelector(`form#${this.for}`); if (form) { this.form = form; } } else { this.form = this.closest('form'); } (_a = this.editor) === null || _a === void 0 ? void 0 : _a.on(`${types_1.DATA_SOURCE_CHANGED} ${types_1.DATA_SOURCE_DATA_LOAD_END}`, this.renderBinded); } disconnectedCallback() { var _a; this.form = null; super.disconnectedCallback(); (_a = this.editor) === null || _a === void 0 ? void 0 : _a.off(`${types_1.DATA_SOURCE_CHANGED} ${types_1.DATA_SOURCE_DATA_LOAD_END}`, this.renderBinded); } /** * Handle formdata event to add the current value to the form */ onFormdata(event) { event.preventDefault(); const formData = event.formData; formData.set(this.name, this.value); } set form(newForm) { if (this._form) { this._form.removeEventListener('formdata', this.onFormdata_); } if (newForm) { newForm.addEventListener('formdata', this.onFormdata_); } } get form() { return this._form; } get data() { const input = this.expressionInputRef.value; if (!this._selected || !this.editor) { console.error('selected and editor are required', this._selected, this.editor); //throw new Error('selected and editor are required') return []; } if (!input || input.value.length === 0) return []; if (input.fixed) { return [(0, utils_1.getFixedToken)(input.value[0] || '')]; } else { const ids = input.value; return ids .filter((id) => !!id) .map((id) => { try { return (0, utils_1.fromString)(this.editor, id, this.selected.getId()); } catch (e) { console.error(`Error while getting token from id ${id}`, e); // Return unknown return { type: 'property', propType: 'field', fieldId: 'unknown', label: 'Unknown', kind: 'scalar', typeIds: [], options: {}, }; } }) // Here the data is missing options as data comes from completion // Add the options .map((token, idx) => { var _a; const popin = (_a = this.popinsRef[idx]) === null || _a === void 0 ? void 0 : _a.value; switch (token.type) { case 'property': case 'filter': token.options = (popin === null || popin === void 0 ? void 0 : popin.value) || token.options; break; default: break; } return token; }); } } set data(value) { if (typeof value === 'string') { this._data = value === '' ? [] : [(0, utils_1.getFixedToken)(value)]; } else { this._data = value; } if (this.editor) this.requestUpdate(); } get editor() { return this._editor; } set editor(value) { this._editor = value; this.requestUpdate(); } render() { var _a, _b; this.noFilters = false; this.redrawing = true; super.render(); if (!this.name) throw new Error('name is required on state-editor'); if (!this.editor || !this.selected) { console.error('editor and selected are required', this.editor, this.selected); return (0, lit_1.html) `<div class="ds-section ds-section--error">Error rendering state-editor component: editor and selected are required</div>`; } const selected = this.selected; // FIXME: fromStored every time we render is not efficient, it is supposed to have been done before const _currentValue = this._data.map(token => (0, token_1.fromStored)(token, selected.getId())); // Get the data to show in the "+" drop down const manager = (0, dataSourceManager_1.getManager)(); const completion = (0, completion_1.getCompletion)({ component: this.dismissCurrentComponentStates ? selected.parent() : selected, expression: _currentValue || [], rootType: this.rootType, currentStateId: this.parentName || this.name, hideLoopData: this.hideLoopData, manager, }) .filter(token => token.type !== 'filter' || !this.noFilters); const groupedCompletion = (0, utils_1.groupByType)(this.editor, selected, completion, _currentValue); // Check if the expression has a fixed value and nothing else const fixed = ((_currentValue === null || _currentValue === void 0 ? void 0 : _currentValue.length) === 1 && _currentValue[0].type === 'property' && _currentValue[0].fieldId === types_1.FIXED_TOKEN_ID) // If the value is empty and the default is fixed, then the input is fixed || (this.defaultFixed && _currentValue.length === 0) // If there is no completion and the value is empty || (completion.length === 0 && _currentValue.length === 0); // Fixed text const text = fixed ? ((_b = (_a = _currentValue[0]) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.value) || '' : ''; let currentData = ''; try { const context = { dataSources: (0, dataSourceRegistry_1.getAllDataSources)(), filters: (0, dataSourceManager_1.getFilters)(), previewData: (0, dataSourceManager_1.getPreviewData)(), component: selected, resolvePreviewIndex: true, }; const realData = (0, expressionEvaluator_1.evaluateExpressionTokens)(_currentValue || [], context); currentData = realData; } catch (e) { console.error('Current data could not be retrieved:', e); } if (typeof currentData === 'undefined' || currentData === null) currentData = ''; // Build the expression input const result = (0, lit_1.html) ` <expression-input @change=${(event) => this.onChangeValue(event)} data-is-input ${(0, ref_js_1.ref)(this.expressionInputRef)} .fixed=${fixed} class="ds-section" name=${this.name} reactive > <style> ${defaultStyles_1.PROPERTY_STYLES} </style> <slot name="label" slot="label"></slot> <div slot="fixed" class="ds-slot-fixed"> <input type="text" class="ds-expression-input__fixed" placeholder="Enter a text or switch to expression mode" .value=${text} /> </div> ${_currentValue && _currentValue.length > 0 ? (0, lit_1.html) ` ${_currentValue.map((token, idx) => { this.popinsRef[idx] = (0, ref_js_1.createRef)(); const optionsForm = this.getOptions(selected, _currentValue, idx); const partialExpression = _currentValue.slice(0, idx); const _partialCompletion = (0, completion_1.getCompletion)({ component: this.dismissCurrentComponentStates ? selected.parent() : selected, expression: partialExpression, rootType: this.rootType, currentStateId: idx === 0 ? this.parentName || this.name : undefined, hideLoopData: this.hideLoopData, manager, }); const partialCompletion = this.noFilters ? _partialCompletion .filter(token => token.type !== 'filter') : _partialCompletion; const partialGroupedCompletion = (0, utils_1.groupByType)(this.editor, selected, partialCompletion, _currentValue.slice(0, idx)); const id = (0, utils_1.toId)(token); return (0, lit_1.html) ` <select> <option value="">-</option> ${Object.entries(partialGroupedCompletion) .reverse() .map(([type, completion]) => { return (0, lit_1.html) ` <optgroup label="${type}"> ${completion .map(partialToken => ({ displayName: (0, utils_1.getTokenDisplayName)(selected, partialToken), partialToken, })) .sort((a, b) => a.displayName.localeCompare(b.displayName)) .map(({ partialToken, displayName }) => { const partialId = (0, utils_1.toId)(partialToken); return (0, lit_1.html) ` <option value=${(0, utils_1.toValue)(partialToken)} .selected=${partialId === id}>${displayName}</option> `; })} </optgroup> `; })} </select> <button class="ds-expression-input__options-button" style=${(0, style_map_js_1.styleMap)({ display: optionsForm === '' ? 'none' : '' })} @click=${(e) => { var _a; (_a = this.popinsRef[idx].value) === null || _a === void 0 ? void 0 : _a.openAt(e.currentTarget); }} >...</button> <popin-form ${(0, ref_js_1.ref)(this.popinsRef[idx])} hidden name=${`${this.name}_options_${idx}`} @change=${(event) => this.onChangeOptions(event, selected, this.popinsRef[idx].value, idx)} > ${optionsForm} </popin-form> `; })} ` : ''} ${Object.entries(groupedCompletion).length ? (0, lit_1.html) ` <select class="ds-expression-input__add" ${(0, ref_js_1.ref)(el => el && (el.value = ''))} > <option value="" selected>+</option> ${Object.entries(groupedCompletion) .reverse() .map(([type, completion]) => { return (0, lit_1.html) ` <optgroup label="${type}"> ${completion .map(token => ({ displayName: (0, utils_1.getTokenDisplayName)(selected, token), token, })) .sort((a, b) => a.displayName.localeCompare(b.displayName)) .map(({ displayName, token }) => { return (0, lit_1.html) `<option value="${(0, utils_1.toValue)(token)}">${displayName}</option>`; })} </optgroup> `; })} </select> ` : ''} </expression-input> <div class="ds-real-data"> <code class="ds-real-data__display"> ${Array.isArray(currentData) ? (0, lit_1.html) `${currentData.length} objects with ${Object.keys(currentData[0] || {}).filter(k => k !== '__typename').join(', ')}` : currentData} </code> </div> `; this.redrawing = false; return result; } onChangeValue(event) { var _a; if (this.redrawing) return; const idx = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.idx; if (idx >= 0) { // Custom event coming from the expression input // Remove the tokens after the changed one const data = this.data.slice(0, idx + 1); if (data.length > idx) { // Clear options if (data[idx].type === 'property' || data[idx].type === 'filter') { data[idx].options = {}; } } else { // We selected the "-" option, do nothing, this step will be removed } this.data = data; } else if (idx === -1) { // Clear expression (case when user clicks reset/clear button) this.data = []; } else { // Event coming from the options (no idx in detail) } // Stop default behavior of inputs event.preventDefault(); event.stopImmediatePropagation(); event.stopPropagation(); // Let the redraw update this.data setTimeout(() => this.dispatchEvent(new Event('change', { bubbles: true }))); } onChangeOptions(event, component, popin, idx) { if (this.redrawing) return; if (!this.editor) throw new Error('editor is required'); const input = this.expressionInputRef.value; const tokensStrings = input.value; // Get tokens as objects const tokens = tokensStrings .filter((id) => !!id) .map((id) => { try { return (0, utils_1.fromString)(this.editor, id, component.getId()); } catch (e) { // FIXME: notify user console.error('Error while getting token from string', { id }, e); // Return unknown return { type: 'property', propType: 'field', fieldId: 'unknown', label: 'Unknown', kind: 'scalar', typeIds: [], options: {}, }; } }); // Get the selected options const options = input.options .filter((o) => o.selected); tokens[idx].options = popin.value; // Update the dom options[idx].value = (0, utils_1.toValue)(tokens[idx]); // Update the state this.requestUpdate(); // Stop the original event event.preventDefault(); event.stopImmediatePropagation(); event.stopPropagation(); // Notify the owner this.dispatchEvent(new Event('change', { bubbles: true })); } getOptions(component, tokens, idx) { if (!this.editor) throw new Error('editor is required'); const token = tokens[idx]; const beforeToken = tokens.slice(0, idx); const fields = beforeToken .map(token => { try { return (0, token_1.getExpressionResultType)(tokens.concat(token), component); } catch (e) { // FIXME: notify the user console.error(`Error while getting expression result type for token ${token} on component ${component.getName()}#${component.get('id')}.${component.getClasses().join('.')} (${component.cid})`, e); return null; } }); switch (token.type) { case 'property': case 'filter': if (token.optionsForm) { const form = token.optionsForm(component, fields[fields.length - 1], token.options || {}, this.parentName || this.name); return form || ''; } return ''; default: return ''; } } } exports.StateEditor = StateEditor; __decorate([ (0, decorators_js_1.property)({ type: Boolean }), __metadata("design:type", Object) ], StateEditor.prototype, "disabled", void 0); __decorate([ (0, decorators_js_1.property)({ type: String }), __metadata("design:type", Object) ], StateEditor.prototype, "name", void 0); __decorate([ (0, decorators_js_1.property)({ type: Boolean, attribute: 'hide-loop-data' }), __metadata("design:type", Object) ], StateEditor.prototype, "hideLoopData", void 0); __decorate([ (0, decorators_js_1.property)({ type: String, attribute: 'parent-name' }), __metadata("design:type", Object) ], StateEditor.prototype, "parentName", void 0); __decorate([ (0, decorators_js_1.property)({ type: Boolean, attribute: 'no-filters' }), __metadata("design:type", Object) ], StateEditor.prototype, "noFilters", void 0); __decorate([ (0, decorators_js_1.property)({ type: String, attribute: 'root-type' }), __metadata("design:type", Object) ], StateEditor.prototype, "rootType", void 0); __decorate([ (0, decorators_js_1.property)({ type: Boolean, attribute: 'default-fixed' }), __metadata("design:type", Object) ], StateEditor.prototype, "defaultFixed", void 0); __decorate([ (0, decorators_js_1.property)({ type: Boolean, attribute: 'dismiss-current-component-states' }), __metadata("design:type", Object) ], StateEditor.prototype, "dismissCurrentComponentStates", void 0); __decorate([ (0, decorators_js_1.property)({ type: Object }), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], StateEditor.prototype, "selected", null); __decorate([ (0, decorators_js_1.property)(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], StateEditor.prototype, "value", null); __decorate([ (0, decorators_js_1.property)({ type: String, attribute: 'for' }), __metadata("design:type", Object) ], StateEditor.prototype, "for", void 0); __decorate([ (0, decorators_js_1.property)({ type: Object }), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], StateEditor.prototype, "editor", null); if (!window.customElements.get('state-editor')) { window.customElements.define('state-editor', StateEditor); } //# sourceMappingURL=state-editor.js.map