Browse Source

Start updating stack view

up-style-manager
Artur Arseniev 5 years ago
parent
commit
0b25829a51
  1. 140
      src/style_manager/view/LayerView.js
  2. 18
      src/style_manager/view/LayersView.js
  3. 374
      src/style_manager/view/PropertyStackView.js

140
src/style_manager/view/LayerView.js

@ -1,24 +1,21 @@
import { isString, each } from 'underscore';
import Backbone from 'backbone';
import PropertiesView from './PropertiesView';
import { View } from 'backbone';
export default Backbone.View.extend({
export default View.extend({
events: {
click: 'active',
'click [data-close-layer]': 'removeItem',
'mousedown [data-move-layer]': 'initSorter',
'touchstart [data-move-layer]': 'initSorter'
'touchstart [data-move-layer]': 'initSorter',
},
template(model) {
const { pfx, ppfx, em } = this;
const label = `${em && em.t('styleManager.layer')} ${model.get('index')}`;
template() {
const { pfx, ppfx } = this;
return `
<div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
<i class="fa fa-arrows"></i>
</div>
<div id="${pfx}label">${label}</div>
<div id="${pfx}label" data-label></div>
<div id="${pfx}preview-box">
<div id="${pfx}preview" data-preview></div>
</div>
@ -31,18 +28,19 @@ export default Backbone.View.extend({
},
initialize(o = {}) {
let model = this.model;
const { model } = this;
this.stackModel = o.stackModel;
this.propertyView = o.propertyView;
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.sorter = o.sorter || null;
this.propsConfig = o.propsConfig || {};
this.customPreview = o.onPreview;
this.pModel = this.propertyView.model;
this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model, 'change:active', this.updateVisibility);
this.listenTo(model.get('properties'), 'change', this.updatePreview);
this.listenTo(model, 'change:values', this.updateLabel);
// For the sorter
model.view = this;
@ -63,76 +61,9 @@ export default Backbone.View.extend({
this.remove();
},
remove(opts = {}) {
const { model, props } = this;
const coll = model.collection;
const stackModel = this.stackModel;
Backbone.View.prototype.remove.apply(this, arguments);
coll && coll.contains(model) && coll.remove(model);
if (stackModel && stackModel.set) {
stackModel.set({ stackIndex: null }, { silent: true });
!opts.fromTarget && stackModel.trigger('updateValue');
}
props && props.remove();
},
/**
* Default method for changing preview box
* @param {Collection} props
* @param {Element} $el
*/
onPreview(value) {
const { stackModel } = this;
const detach = stackModel && stackModel.get('detached');
const values = value.split(' ');
const lim = 3;
const result = [];
const resultObj = {};
this.model.get('properties').each((prop, index) => {
const property = prop.get('property');
let value = detach ? prop.getFullValue() : values[index] || '';
if (value) {
if (prop.get('type') == 'integer') {
let valueInt = parseInt(value, 10);
let unit = value.replace(valueInt, '');
valueInt = !isNaN(valueInt) ? valueInt : 0;
valueInt = valueInt > lim ? lim : valueInt;
valueInt = valueInt < -lim ? -lim : valueInt;
value = valueInt + unit;
}
}
result.push(value);
resultObj[property] = value;
});
return detach ? resultObj : result.join(' ');
},
updatePreview() {
const stackModel = this.stackModel;
const customPreview = this.customPreview;
const previewEl = this.getPreviewEl();
const value = this.model.getFullValue();
const preview = customPreview
? customPreview(value)
: this.onPreview(value);
if (preview && stackModel && previewEl) {
const { style } = previewEl;
if (isString(preview)) {
style[stackModel.get('property')] = preview;
} else {
let prvStr = [];
each(preview, (val, prop) => prvStr.push(`${prop}:${val}`));
previewEl.setAttribute('style', prvStr.join(';'));
}
}
remove() {
this.pModel.removeLayer(this.model);
View.prototype.remove.apply(this, arguments);
},
getPropertiesWrapper() {
@ -149,40 +80,43 @@ export default Backbone.View.extend({
return this.previewEl;
},
getLabelEl() {
if (!this.labelEl) {
this.labelEl = this.el.querySelector('[data-label]');
}
return this.labelEl;
},
active() {
const model = this.model;
const collection = model.collection;
collection.active(collection.indexOf(model));
const { model, propertyView } = this;
const pm = propertyView.model;
if (pm.getSelectedLayer() === model) return;
pm.selectLayer(model);
model.collection.active(model.getIndex());
},
updateVisibility() {
const pfx = this.pfx;
const { pfx, model, propertyView } = this;
const wrapEl = this.getPropertiesWrapper();
const active = this.model.get('active');
const active = model.get('active');
wrapEl.style.display = active ? '' : 'none';
this.$el[active ? 'addClass' : 'removeClass'](`${pfx}active`);
active && wrapEl.appendChild(propertyView.props.el);
},
updateLabel() {
const { model, propertyView } = this;
const label = propertyView.model.getLayerLabel(model);
this.getLabelEl().innerHTML = label;
},
render() {
const propsConfig = this.propsConfig;
const { model, el, pfx } = this;
const preview = model.get('preview');
const properties = new PropertiesView({
collection: model.get('properties'),
config: { ...this.config, fromLayer: 1 },
target: propsConfig.target,
customValue: propsConfig.customValue,
propTarget: propsConfig.propTarget,
onChange: propsConfig.onChange
});
const propsEl = properties.render().el;
el.innerHTML = this.template(model);
el.innerHTML = this.template();
el.className = `${pfx}layer${!preview ? ` ${pfx}no-preview` : ''}`;
this.props = properties;
this.getPropertiesWrapper().appendChild(propsEl);
this.updateLabel();
this.updateVisibility();
this.updatePreview();
return this;
}
},
});

18
src/style_manager/view/LayersView.js

@ -5,6 +5,7 @@ export default Backbone.View.extend({
initialize(o) {
this.config = o.config || {};
this.stackModel = o.stackModel;
this.propertyView = o.propertyView;
this.preview = o.preview;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
@ -27,7 +28,7 @@ export default Backbone.View.extend({
ignoreViewChildren: 1,
containerSel: `.${pfx}layers`,
itemSel: `.${pfx}layer`,
pfx: this.config.pStylePrefix
pfx: this.config.pStylePrefix,
})
: '';
@ -58,8 +59,8 @@ export default Backbone.View.extend({
* */
addToCollection(model, fragmentEl, index) {
var fragment = fragmentEl || null;
const { propertyView, config } = this;
const stackModel = this.stackModel;
const config = this.config;
const sorter = this.sorter;
const propsConfig = this.propsConfig;
@ -72,7 +73,8 @@ export default Backbone.View.extend({
config,
sorter,
stackModel,
propsConfig
propsConfig,
propertyView,
});
const rendered = view.render().el;
this.items.push(view);
@ -91,11 +93,7 @@ export default Backbone.View.extend({
// In case the added is new in the collection index will be -1
if (index < 0) {
this.$el.append(rendered);
} else
this.$el
.children()
.eq(index)
[method](rendered);
} else this.$el.children().eq(index)[method](rendered);
} else this.$el.append(rendered);
}
@ -120,7 +118,7 @@ export default Backbone.View.extend({
var fragment = document.createDocumentFragment();
this.$el.empty();
this.collection.each(function(model) {
this.collection.each(function (model) {
this.addToCollection(model, fragment);
}, this);
@ -140,5 +138,5 @@ export default Backbone.View.extend({
clearItems(opts) {
this.items.forEach(item => item.remove(opts));
this.items = [];
}
},
});

374
src/style_manager/view/PropertyStackView.js

@ -1,14 +1,18 @@
import { isUndefined, keys } from 'underscore';
import PropertyCompositeView from './PropertyCompositeView';
import PropertiesView from './PropertiesView';
import LayersView from './LayersView';
import CssGenerator from 'code_manager/model/CssGenerator';
const cssGen = new CssGenerator();
export default PropertyCompositeView.extend({
events() {
return {
...PropertyCompositeView.prototype.events,
'click [data-add-layer]': 'addLayer',
change: '',
};
},
templateInput() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const { pfx } = this;
return `
<div class="${pfx}field ${pfx}stack">
<button type="button" id="${pfx}add" data-add-layer>+</button>
@ -17,109 +21,10 @@ export default PropertyCompositeView.extend({
`;
},
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'));
},
init() {},
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'));
}
this.model.addLayer({}, { at: 0 });
},
/**
@ -129,232 +34,43 @@ export default PropertyCompositeView.extend({
*/
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;
remove() {
this.layersView?.remove();
PropertyCompositeView.prototype.remove.apply(this, arguments);
},
/**
* 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.getFirstTarget();
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 });
clearCached() {
PropertyCompositeView.prototype.clearCached.apply(this, arguments);
this.layersView = null;
},
getTargetValue(opts = {}) {
const { model } = this;
const { detached } = model.attributes;
const target = this.getFirstTarget();
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);
onRender() {
const { model, el } = this;
const props = model.getProperties();
if (props.length && !this.props) {
const propsView = new PropertiesView({
config: {
...this.config,
highlightComputed: false,
highlightChanged: false,
},
collection: props,
parent: this,
});
propsView.render();
const layersView = new LayersView({
collection: model.getLayers(),
config: this.config,
propertyView: this,
});
layersView.render();
const fieldEl = el.querySelector('[data-layers-wrapper]');
fieldEl.appendChild(layersView.el);
this.props = propsView;
this.layersView = layersView;
}
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);
}
});

Loading…
Cancel
Save