Free and Open source Web Builder Framework. Next generation tool for building templates without coding
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

307 lines
9.3 KiB

import { isUndefined, keys } from 'underscore';
import PropertyCompositeView from './PropertyCompositeView';
import LayersView from './LayersView';
import CssGenerator from 'code_manager/model/CssGenerator';
const cssGen = new CssGenerator();
export default PropertyCompositeView.extend({
templateInput() {
const pfx = this.pfx;
const ppfx = this.ppfx;
return `
<div class="${pfx}field ${pfx}stack">
<button type="button" id="${pfx}add" data-add-layer>+</button>
<div data-layers-wrapper></div>
</div>
`;
},
init() {
const model = this.model;
const pfx = this.pfx;
model.set('stackIndex', null);
this.events[`click [data-add-layer]`] = 'addLayer';
this.listenTo(model, 'change:stackIndex', this.indexChanged);
this.listenTo(model, 'updateValue', this.inputValueChanged);
this.delegateEvents();
},
/**
* Fired when the target is updated.
* With detached mode the component will be always empty as its value
* so we gonna check all props and find if it has any difference
* */
targetUpdated(...args) {
if (!this.model.get('detached')) {
PropertyCompositeView.prototype.targetUpdated.apply(this, args);
} else {
const { status } = this._getTargetData();
this.setStatus(status);
this.checkVisibility();
}
this.refreshLayers();
},
/**
* Returns the collection of layers
* @return {Collection}
*/
getLayers() {
return this.model.get('layers');
},
/**
* Triggered when another layer has been selected.
* This allow to move all rendered properties to a new
* selected layer
* @param {Event}
*
* @return {Object}
* */
indexChanged(e) {
const model = this.model;
this.getLayers().active(model.get('stackIndex'));
},
addLayer() {
const model = this.model;
const layers = this.getLayers();
const properties = model.get('properties').deepClone();
properties.each(property => property.set('value', ''));
const layer = layers.add({ properties });
// In detached mode inputValueChanged will add new 'layer value'
// to all subprops
this.inputValueChanged();
// This will set subprops with a new default values
model.set('stackIndex', layers.indexOf(layer));
},
inputValueChanged() {
const model = this.model;
this.elementUpdated();
// If not detached I'll just put all the values from layers to property
// eg. background: layer1Value, layer2Value, layer3Value, ...
if (!model.get('detached')) {
model.set('value', this.getLayerValues());
} else {
model.get('properties').each(prop => prop.trigger('change:value'));
}
},
/**
* There is no need to handle input update by the property itself,
* this will be done by layers
* @private
*/
setValue() {},
/**
* Create value by layers
* @return string
* */
getLayerValues() {
return this.getLayers().getFullValue();
},
_getClassRule(opts = {}) {
const { em } = this;
const { skipAdd = 1 } = opts;
const selected = em.getSelected();
const targetAlt = em.get('StyleManager').getModelToStyle(selected, {
skipAdd,
useClasses: 1
});
return targetAlt !== selected && targetAlt;
},
/**
* Return the parent style rule of the passed one
* @private
*/
_getParentTarget(target, opts = {}) {
const { em, model } = this;
const property = model.get('property');
const isValid = opts.isValid || (rule => rule.getStyle()[property]);
const targetsDevice = em
.get('CssComposer')
.getAll()
.filter(rule => rule.selectorsToString() === target.getSelectorsString());
const map = targetsDevice.reduce((acc, rule) => {
acc[rule.getAtRule()] = rule;
return acc;
}, {});
const mapSorted = cssGen.sortMediaObject(map);
const sortedRules = mapSorted.map(item => item.value);
const currIndex = sortedRules.indexOf(target);
const rulesToCheck = sortedRules.splice(0, currIndex);
let result;
for (let i = rulesToCheck.length - 1; i > -1; i--) {
const rule = rulesToCheck[i];
if (isValid(rule)) {
// only for not detached
result = rule;
break;
}
}
return result;
},
/**
* Refresh layers
* */
refreshLayers() {
let layersObj = [];
const { model, em } = this;
const layers = this.getLayers();
const detached = model.get('detached');
const property = model.get('property');
const target = this.getTarget();
const valueComput = this.getComputedValue();
const selected = em.getSelected();
let style, targetAlt, targetAltDevice, valueTargetAlt, valueTrgAltDvc;
// With detached layers values will be assigned to their properties
if (detached) {
style = target ? target.getStyle() : {};
const hasDetachedStyle = rule => {
const name = model
.get('properties')
.at(0)
.get('property');
return rule && !isUndefined(rule.getStyle()[name]);
};
// If the style object is empty but the target has a computed value,
// that means the style might exist in some other place
if (!keys(style).length && valueComput && selected) {
// Styles of the same target but with a higher rule
const parentOpts = { isValid: rule => hasDetachedStyle(rule) };
targetAltDevice = this._getParentTarget(target, parentOpts);
if (targetAltDevice) {
style = targetAltDevice.getStyle();
} else {
// The target is a component but the style is in the class rules
targetAlt = this._getClassRule();
valueTargetAlt = hasDetachedStyle(targetAlt) && targetAlt.getStyle();
targetAltDevice =
!valueTargetAlt &&
this._getParentTarget(
this._getClassRule({ skipAdd: 0 }),
parentOpts
);
valueTrgAltDvc =
hasDetachedStyle(targetAltDevice) && targetAltDevice.getStyle();
style = valueTargetAlt || valueTrgAltDvc || {};
}
}
layersObj = layers.getLayersFromStyle(style);
} else {
const valueTrg = this.getTargetValue({ ignoreDefault: 1 });
let value = valueTrg;
// Try to check if the style is in another rule
if (!value && valueComput) {
// Styles of the same target but with a higher rule
targetAltDevice = this._getParentTarget(target);
if (targetAltDevice) {
value = targetAltDevice.getStyle()[property];
} else {
// Computed value is not always reliable due to the browser's CSSOM parser
// here we try to look for the style in class rules
targetAlt = this._getClassRule();
const valueTargetAlt = targetAlt && targetAlt.getStyle()[property];
targetAltDevice =
!valueTargetAlt &&
this._getParentTarget(this._getClassRule({ skipAdd: 0 }));
const valueTrgAltDvc =
targetAltDevice && targetAltDevice.getStyle()[property];
value = valueTargetAlt || valueTrgAltDvc || valueComput;
}
}
value = value == model.getDefaultValue() ? '' : value;
layersObj = layers.getLayersFromValue(value);
}
const toAdd = model.getLayersFromTarget(target) || layersObj;
layers.reset();
layers.add(toAdd);
model.set({ stackIndex: null }, { silent: true });
},
getTargetValue(opts = {}) {
let result = PropertyCompositeView.prototype.getTargetValue.call(
this,
opts
);
const { detached } = this.model.attributes;
// It might happen that the browser split properties on CSSOM parse
if (isUndefined(result) && !detached) {
result = this.model.getValueFromStyle(this.getTarget().getStyle());
}
return result;
},
onRender() {
const self = this;
const model = this.model;
const fieldEl = this.el.querySelector('[data-layers-wrapper]');
const PropertiesView = require('./PropertiesView').default;
const propsConfig = {
target: this.target,
propTarget: this.propTarget,
// Things to do when a single sub-property is changed
onChange(el, view, opt) {
const subModel = view.model;
if (model.get('detached')) {
const subProp = subModel.get('property');
const values = self.getLayers().getPropertyValues(subProp);
view.updateTargetStyle(values, null, opt);
} else {
// Update only if there is an actual update (to avoid changes for computed styles)
// ps: status is calculated in `targetUpdated` method
if (model.get('status') == 'updated') {
const value = model.getFullValue();
model.set('value', value, opt);
// Try to remove detached properties
!value && view.updateTargetStyle(value, null, opt);
}
}
}
};
const layers = new LayersView({
collection: this.getLayers(),
stackModel: model,
preview: model.get('preview'),
config: this.config,
propsConfig
}).render().el;
// Will use it to propogate changes
new PropertiesView({
target: this.target,
collection: this.model.get('properties'),
stackModel: model,
config: this.config,
onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget
}).render();
//model.get('properties')
fieldEl.appendChild(layers);
}
});