UNPKG

drapcode-builder

Version:

Drapcode Builder Library

361 lines (324 loc) 11.2 kB
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(); const propsConfig = this.getPropsConfig(); this.layers = new LayersView({ collection: this.getLayers(), stackModel: model, preview: model.get('preview'), config: this.config, propsConfig }); // For detached properties, used in inputValueChanged (eg. clear all) const PropertiesView = require('./PropertiesView').default; this.propsView = new PropertiesView({ target: this.target, collection: model.get('properties'), stackModel: model, config: this.config, onChange: propsConfig.onChange, propTarget: propsConfig.propTarget }); }, /** * 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) { let data; if (!this.model.get('detached')) { data = PropertyCompositeView.prototype.targetUpdated.apply(this, args); } else { data = this._getTargetData(); this.setStatus(data.status); this.checkVisibility(); } // I have to wait the update of inner properites (like visibility) // before render layers setTimeout(() => this.refreshLayers(data)); }, /** * 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 prepend = model.get('prepend'); const properties = model.get('properties').deepClone(); properties.each(property => property.set('value', '')); const layer = layers.add( { properties }, { active: 1, ...(prepend && { at: 0 }) } ); // In detached mode inputValueChanged will add new 'layer value' // to all subprops this.inputValueChanged({ up: 1 }); // This will set subprops with a new default values model.set('stackIndex', layers.indexOf(layer)); }, inputValueChanged(opts = {}) { const model = this.model; opts.up && 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(opts = {}) { 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(); const updateOpts = { fromTarget: 1 }; let resultValue, style, targetAlt, targetAltDevice, valueTargetAlt, valueTrgAltDvc; // With detached layers values will be assigned to their properties if (detached) { style = opts.targetValue || {}; 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 || {}; } } resultValue = style; 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(); valueTargetAlt = targetAlt && targetAlt.getStyle()[property]; targetAltDevice = !valueTargetAlt && this._getParentTarget(this._getClassRule({ skipAdd: 0 })); valueTrgAltDvc = targetAltDevice && targetAltDevice.getStyle()[property]; value = valueTargetAlt || valueTrgAltDvc || valueComput; } } value = value == model.getDefaultValue() ? '' : value; resultValue = value; layersObj = layers.getLayersFromValue(value); } const toAdd = model.getLayersFromTarget(target, { resultValue, layersObj }) || layersObj; layers.reset(null, updateOpts); layers.add(toAdd, updateOpts); model.set({ stackIndex: null }, { silent: true }); }, getTargetValue(opts = {}) { const { model } = this; const { detached } = model.attributes; const target = this.getTarget(); let result = PropertyCompositeView.prototype.getTargetValue.call( this, opts ); // It might happen that the browser split properties on CSSOM parse if (isUndefined(result) && !detached) { result = model.getValueFromStyle(target.getStyle()); } else if (detached) { result = model.getValueFromTarget(target); } return result; }, getPropsConfig() { const self = this; const { model } = self; return { target: self.target, propTarget: self.propTarget, // Things to do when a single sub-property is changed onChange(el, view, opt) { const subModel = view.model; const status = model.get('status'); if (model.get('detached')) { const subProp = subModel.get('property'); const defVal = subModel.getDefaultValue(); const layers = self.getLayers(); const values = layers.getPropertyValues(subProp, defVal); view.updateTargetStyle(values, null, opt); // Update also the target with values of special hidden properties. // This fixes the case of update with computed layers if ( subProp == 'background-image' && !opt.avoidStore && status == 'computed' ) { model .get('properties') .filter(prop => prop.get('property').substr(0, 2) == '__') .forEach(prop => { const name = prop.get('property'); const value = layers.getPropertyValues( name, prop.getDefaultValue() ); self .getTargets() .forEach(tr => tr.addStyle({ [name]: value }, opt)); }); } } else { // Update only if there is an actual update (to avoid changes for computed styles) // ps: status is calculated in `targetUpdated` method if (status == 'updated') { const value = model.getFullValue(); model.set('value', value, opt); // Try to remove detached properties !value && view.updateTargetStyle(value, null, opt); } } } }; }, onRender() { const { el, layers, propsView } = this; const fieldEl = el.querySelector('[data-layers-wrapper]'); propsView.render(); // Will use it to propogate changes fieldEl.appendChild(layers.render().el); } });