@silexlabs/grapesjs-data-source
Version:
Grapesjs Data Source
564 lines • 22.5 kB
JavaScript
;
/*
* 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