grapesjs-clot
Version:
Free and Open Source Web Builder Framework
481 lines (422 loc) • 14 kB
JavaScript
import { keys, isUndefined, isArray, isString, isNumber } from 'underscore';
import PropertyComposite, { isNumberType } from './PropertyComposite';
import PropertyBase from './Property';
import Layers from './Layers';
import { camelCase } from 'utils/mixins';
const VALUES_REG = /,(?![^\(]*\))/;
const PARTS_REG = /\s(?![^(]*\))/;
/**
*
* [Layer]: layer.html
*
*
* @typedef PropertyStack
* @property {Boolean} [preview=false] Indicate if the layer should display a preview.
* @property {String|RegExp} [layerSeparator=', '] The separator used to split layer values.
* @property {String} [layerJoin=', '] Value used to join layer values.
* @property {Function} [layerLabel] Custom logic for creating layer labels.
* \n
* ```js
* layerLabel: (layer) => {
* const values = layer.getValues();
* return `A: ${values['prop-a']} B: ${values['prop-b']}`;
* }
* ```
*
*/
export default class PropertyStack extends PropertyComposite {
defaults() {
return {
...PropertyComposite.getDefaults(),
layers: [],
layerSeparator: ', ',
layerJoin: '',
prepend: 0,
preview: false,
layerLabel: null,
selectedLayer: null,
};
}
initialize(props = {}, opts = {}) {
PropertyComposite.callParentInit(PropertyComposite, this, props, opts);
const layers = this.get('layers');
const layersColl = new Layers(layers, { prop: this });
layersColl.property = this;
layersColl.properties = this.get('properties');
this.set('layers', layersColl, { silent: true });
this.on('change:selectedLayer', this.__upSelected);
this.listenTo(layersColl, 'add remove', this.__upLayers);
PropertyComposite.callInit(this, props, opts);
}
/**
* Get all available layers.
* @returns {Array<[Layer]>}
*/
getLayers() {
return this.__getLayers().models;
}
__getLayers() {
return this.get('layers');
}
/**
* Get layer by index.
* @param {Number} [index=0] Layer index position.
* @returns {[Layer]|null}
* @example
* // Get the first layer
* const layerFirst = property.getLayer(0);
* // Get the last layer
* const layers = this.getLayers();
* const layerLast = property.getLayer(layers.length - 1);
*/
getLayer(index = 0) {
return this.__getLayers().at(index) || null;
}
/**
* Get selected layer.
* @returns {[Layer] | null}
*/
getSelectedLayer() {
const layer = this.get('selectedLayer');
return layer && layer.getIndex() >= 0 ? layer : null;
}
/**
* Select layer.
* Without a selected layer any update made on inner properties has no effect.
* @param {[Layer]} layer Layer to select
* @example
* const layer = property.getLayer(0);
* property.selectLayer(layer);
*/
selectLayer(layer) {
return this.set('selectedLayer', layer, { __select: true });
}
/**
* Select layer by index.
* @param {Number} index Index of the layer to select.
* @example
* property.selectLayerAt(1);
*/
selectLayerAt(index = 0) {
const layer = this.getLayer(index);
return layer && this.selectLayer(layer);
}
/**
* Move layer by index.
* @param {[Layer]} layer Layer to move.
* @param {Number} index New layer index.
* @example
* const layer = property.getLayer(1);
* property.moveLayer(layer, 0);
*/
moveLayer(layer, index = 0) {
const currIndex = layer ? layer.getIndex() : -1;
if (currIndex >= 0 && isNumber(index) && index >= 0 && index < this.getLayers().length && currIndex !== index) {
this.removeLayer(layer);
this.__getLayers().add(layer, { at: index });
}
}
/**
* Add new layer to the stack.
* @param {Object} [props={}] Custom property values to use in a new layer.
* @param {Object} [opts={}] Options
* @param {Number} [opts.at] Position index (by default the layer will be appended at the end).
* @returns {[Layer]} Added layer.
* @example
* // Add new layer at the beginning of the stack with custom values
* property.addLayer({ 'sub-prop1': 'value1', 'sub-prop2': 'value2' }, { at: 0 });
*/
addLayer(props = {}, opts = {}) {
const values = {};
this.getProperties().forEach(prop => {
const key = prop.getId();
const value = props[key];
values[key] = isUndefined(value) ? prop.getDefaultValue() : value;
});
const layer = this.get('layers').push({ values }, opts);
return layer;
}
/**
* Remove layer.
* @param {[Layer]} layer Layer to remove.
* @returns {[Layer]} Removed layer
* @example
* const layer = property.getLayer(0);
* property.removeLayer(layer);
*/
removeLayer(layer) {
return this.get('layers').remove(layer);
}
/**
* Remove layer by index.
* @param {Number} index Index of the layer to remove
* @returns {[Layer]|null} Removed layer
* @example
* property.removeLayerAt(0);
*/
removeLayerAt(index = 0) {
const layer = this.getLayer(index);
return layer ? this.removeLayer(layer) : null;
}
/**
* Get the layer label. The label can be customized with the `layerLabel` property.
* @param {[Layer]} layer
* @returns {String}
* @example
* const layer = this.getLayer(1);
* const label = this.getLayerLabel(layer);
*/
getLayerLabel(layer) {
let result = '';
if (layer) {
const layerLabel = this.get('layerLabel');
const values = layer.getValues();
const index = layer.getIndex();
if (layerLabel) {
result = layerLabel(layer, { index, values, property: this });
} else {
const parts = [];
this.getProperties().map(prop => {
parts.push(values[prop.getId()]);
});
result = parts.filter(Boolean).join(' ');
}
}
return result;
}
/**
* Get style object from the layer.
* @param {[Layer]} layer
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.camelCase] Return property names in camelCase.
* @param {Object} [opts.number] Limit the result of the number types, eg. `number: { min: -3, max: 3 }`
* @returns {Object} Style object
*/
getStyleFromLayer(layer, opts = {}) {
const join = this.__getJoin();
const joinLayers = this.__getJoinLayers();
const toStyle = this.get('toStyle');
const name = this.getName();
const values = layer.getValues();
let style;
if (toStyle) {
style = toStyle(values, { join, joinLayers, name, layer, property: this });
} else {
const result = this.getProperties().map(prop => {
const name = prop.getName();
const val = values[prop.getId()];
let value = isUndefined(val) ? prop.getDefaultValue() : val;
// Limit number values if necessary (useful for previews)
if (opts.number && isNumberType(prop.getType())) {
const newVal = prop.parseValue(val, opts.number);
value = `${newVal.value}${newVal.unit}`;
}
return { name, value };
});
style = this.isDetached()
? result.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {})
: {
[this.getName()]: result.map(r => r.value).join(join),
};
}
return opts.camelCase
? Object.keys(style).reduce((res, key) => {
res[camelCase(key)] = style[key];
return res;
}, {})
: style;
}
/**
* Get preview style object from the layer.
* If the property has `preview: false` the returned object will be empty.
* @param {[Layer]} layer
* @param {Object} [opts={}] Options. Same of `getStyleFromLayer`
* @returns {Object} Style object
*/
getStylePreview(layer, opts = {}) {
let result = {};
const preview = this.get('preview');
if (preview) {
result = this.getStyleFromLayer(layer, opts);
}
return result;
}
/**
* Get layer separator.
* @return {RegExp}
*/
getLayerSeparator() {
const sep = this.get('layerSeparator');
return isString(sep) ? new RegExp(`${sep}(?![^\\(]*\\))`) : sep;
}
__upProperties(prop, opts = {}) {
const layer = this.getSelectedLayer();
if (!layer) return;
layer.upValues({ [prop.getId()]: prop.__getFullValue() });
if (opts.__up) return;
this.__upTargetsStyleProps(opts);
}
__upLayers(m, c, o) {
this.__upTargetsStyleProps(o || c);
}
__upTargets(p, opts = {}) {
if (opts.__select) return;
return PropertyBase.prototype.__upTargets.call(this, p, opts);
}
__upTargetsStyleProps(opts = {}) {
this.__upTargetsStyle(this.getStyleFromLayers(), opts);
}
__upTargetsStyle(style, opts) {
return PropertyBase.prototype.__upTargetsStyle.call(this, style, opts);
}
__upSelected({ noEvent } = {}, opts = {}) {
const sm = this.em.get('StyleManager');
const selected = this.getSelectedLayer();
const values = selected?.getValues();
// Update properties by layer value
values &&
this.getProperties().forEach(prop => {
const value = values[prop.getId()];
prop.__getFullValue() !== value && prop.upValue(value, { ...opts, __up: true });
});
!noEvent && sm.__trgEv(sm.events.layerSelect, { property: this });
}
_up(props, opts = {}) {
const { __layers = [], ...rest } = props;
// Detached props will update their layers later in sm.__upProp
!this.isDetached() && this.__setLayers(__layers);
this.__upSelected({ noEvent: true }, opts);
return PropertyBase.prototype._up.call(this, rest, opts);
}
__setLayers(newLayers = []) {
const layers = this.__getLayers();
const layersNew = newLayers.map(values => ({ values }));
if (layers.length === layersNew.length) {
layersNew.map((layer, n) => layers.at(n)?.upValues(layer.values));
} else {
this.__getLayers().reset(layersNew);
}
this.__upSelected({ noEvent: true });
}
__parseValue(value) {
const result = this.parseValue(value);
result.__layers = value
.split(VALUES_REG)
.map(v => v.trim())
.map(v => this.__parseLayer(v))
.filter(Boolean);
return result;
}
__parseLayer(value) {
const parseFn = this.get('parseLayer');
const values = value.split(PARTS_REG);
const properties = this.getProperties();
return parseFn
? parseFn({ value, values })
: properties.reduce((acc, prop, i) => {
const value = values[i];
acc[prop.getId()] = !isUndefined(value) ? value : prop.getDefaultValue();
return acc;
}, {});
}
__getLayersFromStyle(style = {}) {
if (!this.__styleHasProps(style)) return null;
const name = this.getName();
const props = this.getProperties();
const sep = this.getLayerSeparator();
const fromStyle = this.get('fromStyle');
let result = fromStyle ? fromStyle(style, { property: this, name, separatorLayers: sep }) : [];
if (!fromStyle) {
// Get layers from the main property
const layers = this.__splitStyleName(style, name, sep)
.map(value => value.split(this.getSplitSeparator()))
.map(parts => {
const result = {};
props.forEach((prop, i) => {
const value = parts[i];
result[prop.getId()] = !isUndefined(value) ? value : prop.getDefaultValue();
});
return result;
});
// Get layers from the inner properties
props.forEach(prop => {
const id = prop.getId();
this.__splitStyleName(style, prop.getName(), sep)
.map(value => ({ [id]: value || prop.getDefaultValue() }))
.forEach((inLayer, i) => {
layers[i] = layers[i] ? { ...layers[i], ...inLayer } : inLayer;
});
});
result = layers;
}
return isArray(result) ? result : [result];
}
getStyle(opts) {
return this.getStyleFromLayers(opts);
}
getStyleFromLayers(opts) {
let result = {};
const name = this.getName();
const layers = this.getLayers();
const props = this.getProperties();
const styles = layers.map(l => this.getStyleFromLayer(l, opts));
styles.forEach(style => {
keys(style).map(key => {
if (!result[key]) result[key] = [];
result[key].push(style[key]);
});
});
keys(result).map(key => {
result[key] = result[key].join(this.__getJoinLayers());
});
if (this.isDetached()) {
result[name] = '';
!layers.length &&
props.map(prop => {
result[prop.getName()] = '';
});
} else {
const style = props.reduce((acc, prop) => {
acc[prop.getName()] = '';
return acc;
}, {});
result[name] = result[name] || '';
result = { ...result, ...style };
}
return result;
}
__getJoinLayers() {
const join = this.get('layerJoin');
const sep = this.get('layerSeparator');
return join || (isString(sep) ? sep : join);
}
__getFullValue() {
if (this.get('detached')) return '';
const style = this.getStyleFromLayers();
return style[this.getName()];
}
/**
* Extended
* @private
*/
hasValue(opts = {}) {
const { noParent } = opts;
const parentValue = noParent && this.getParentTarget();
return this.getLayers().length > 0 && !parentValue;
}
/**
* Extended
* @private
*/
clear(opts = {}) {
this.__getLayers().reset();
this.__upTargetsStyleProps(opts);
return PropertyBase.prototype.clear.call(this);
}
__canClearProp() {
return false;
}
}