UNPKG

grapesjs_codeapps

Version:

Free and Open Source Web Builder Framework/SC Modification

522 lines (450 loc) 13.4 kB
import Backbone from 'backbone'; import { bindAll, isArray, isUndefined, debounce } from 'underscore'; import { camelCase } from 'utils/mixins'; const clearProp = 'data-clear-style'; module.exports = Backbone.View.extend({ template(model) { const pfx = this.pfx; return ` <div class="${pfx}label"> ${this.templateLabel(model)} </div> <div class="${this.ppfx}fields"> ${this.templateInput(model)} </div> `; }, templateLabel(model) { const pfx = this.pfx; const icon = model.get('icon'); const info = model.get('info'); return ` <span class="${pfx}icon ${icon}" title="${info}"> ${window.lang[model.get('id')] || model.get('name')} </span> <b class="${pfx}clear" ${clearProp}>&Cross;</b> `; }, templateInput(model) { return ` <div class="${this.ppfx}field"> <input placeholder="${model.getDefaultValue()}"/> </div> `; }, events: { change: 'inputValueChanged', [`click [${clearProp}]`]: 'clear' }, initialize(o = {}) { bindAll(this, 'targetUpdated'); this.config = o.config || {}; const em = this.config.em; this.em = em; this.pfx = this.config.stylePrefix || ''; this.ppfx = this.config.pStylePrefix || ''; this.target = o.target || {}; this.propTarget = o.propTarget || {}; this.onChange = o.onChange; this.onInputRender = o.onInputRender || {}; this.customValue = o.customValue || {}; const model = this.model; this.property = model.get('property'); this.input = null; const pfx = this.pfx; this.inputHolderId = '#' + pfx + 'input-holder'; this.sector = model.collection && model.collection.sector; model.view = this; if (!model.get('value')) { model.set('value', model.getDefaultValue()); } em && em.on(`update:component:style:${this.property}`, this.targetUpdated); //em && em.on(`styleable:change:${this.property}`, this.targetUpdated); this.listenTo( this.propTarget, 'update styleManager:update', this.targetUpdated ); this.listenTo(model, 'destroy remove', this.remove); this.listenTo(model, 'change:value', this.modelValueChanged); this.listenTo(model, 'targetUpdated', this.targetUpdated); this.listenTo(model, 'change:visible', this.updateVisibility); this.listenTo(model, 'change:status', this.updateStatus); const init = this.init && this.init.bind(this); init && init(); }, /** * Triggers when the status changes. The status indicates if the value of * the proprerty is changed or inherited * @private */ updateStatus() { const status = this.model.get('status'); const pfx = this.pfx; const ppfx = this.ppfx; const config = this.config; const updatedCls = `${ppfx}four-color`; const computedCls = `${ppfx}color-warn`; const labelEl = this.$el.children(`.${pfx}label`); const clearStyle = this.getClearEl().style; labelEl.removeClass(`${updatedCls} ${computedCls}`); clearStyle.display = 'none'; switch (status) { case 'updated': labelEl.addClass(updatedCls); if (config.clearProperties) { clearStyle.display = 'inline'; } break; case 'computed': labelEl.addClass(computedCls); break; } }, /** * Clear the property from the target */ clear(e) { e && e.stopPropagation(); this.model.clearValue(); this.targetUpdated(); }, /** * Get clear element * @return {HTMLElement} */ getClearEl() { if (!this.clearEl) { this.clearEl = this.el.querySelector(`[${clearProp}]`); } return this.clearEl; }, /** * Returns selected target which should have 'style' property * @return {Model|null} */ getTarget() { return this.getTargetModel(); }, /** * Returns Styleable model * @return {Model|null} */ getTargetModel() { return this.propTarget && this.propTarget.model; }, /** * Returns helper Styleable model * @return {Model|null} */ getHelperModel() { return this.propTarget && this.propTarget.helper; }, /** * Triggers when the value of element input/s is changed, so have to update * the value of the model which will propogate those changes to the target */ inputValueChanged(e) { e && e.stopPropagation(); this.model.setValue(this.getInputValue(), 1, { fromInput: 1 }); this.elementUpdated(); }, /** * Fired when the element of the property is updated */ elementUpdated() { this.setStatus('updated'); }, setStatus(value) { this.model.set('status', value); const parent = this.model.parent; parent && parent.set('status', value); }, emitUpdateTarget: debounce(function() { const em = this.config.em; em && em.trigger('styleManager:update:target', this.getTarget()); }), /** * Fired when the target is changed * */ targetUpdated() { this.emitUpdateTarget(); if (!this.checkVisibility()) { return; } const config = this.config; const em = config.em; const model = this.model; let value = ''; let status = ''; let targetValue = this.getTargetValue({ ignoreDefault: 1 }); let defaultValue = model.getDefaultValue(); let computedValue = this.getComputedValue(); if (targetValue) { value = targetValue; if (config.highlightChanged) { status = 'updated'; } } else if ( computedValue && config.showComputed && computedValue != defaultValue ) { value = computedValue; if (config.highlightComputed) { status = 'computed'; } } else { value = defaultValue; status = ''; } if (['top', 'left', 'position'].includes(model['attributes']['property'])) { //console.log(model.attributes); //this.updateTargetStyle(value, model['attributes']['property']); } model.setValue(value, 0, { fromTarget: 1 }); this.setStatus(status); if (em) { em.trigger('styleManager:change', this); em.trigger(`styleManager:change:${model.get('property')}`, this); } }, checkVisibility() { var result = 1; // Check if need to hide the property if (this.config.hideNotStylable) { if (!this.isTargetStylable() || !this.isComponentStylable()) { this.hide(); result = 0; } else { this.show(); } // Sector is not passed to Composite and Stack types if (this.sector) { this.sector.trigger('updateVisibility'); } } return result; }, /** * Get the value of this property from the target (eg, Component, CSSRule) * @param {Object} [opts] Options * @param {Boolean} [options.fetchFromFunction] * @param {Boolean} [options.ignoreDefault] * @return string * @private */ getTargetValue(opts = {}) { var result; var model = this.model; var target = this.getTargetModel(); var customFetchValue = this.customValue; if (!target) { return result; } result = target.getStyle()[model.get('property')]; if (!result && !opts.ignoreDefault) { result = model.getDefaultValue(); } if (typeof customFetchValue == 'function' && !opts.ignoreCustomValue) { let index = model.collection.indexOf(model); let customValue = customFetchValue(this, index); if (customValue) { result = customValue; } } return result; }, /** * Returns computed value * @return {String} * @private */ getComputedValue() { const target = this.propTarget; const computed = target.computed || {}; const computedDef = target.computedDefault || {}; const avoid = this.config.avoidComputed || []; const property = this.model.get('property'); const notToSkip = avoid.indexOf(property) < 0; const value = computed[property]; const valueDef = computedDef[camelCase(property)]; return computed && notToSkip && valueDef !== value && value; }, /** * Returns value from input * @return {string} */ getInputValue() { const input = this.getInputEl(); return input ? input.value : ''; }, /** * Triggers when the `value` of the model changes, so the target and * the input element should be updated * @param {Object} e Event * @param {Mixed} val Value * @param {Object} opt Options * */ modelValueChanged(e, val, opt = {}) { const em = this.config.em; const model = this.model; const value = model.getFullValue(); const target = this.getTarget(); const onChange = this.onChange; // Avoid element update if the change comes from it if (!opt.fromInput) { this.setValue(value); } // Check if component is allowed to be styled if (!target || !this.isTargetStylable() || !this.isComponentStylable()) { return; } // Avoid target update if the changes comes from it if (!opt.fromTarget) { // The onChange is used by Composite/Stack properties, so I'd avoid sending // it back if the change comes from one of those if (onChange && !opt.fromParent) { onChange(target, this, opt); } else { this.updateTargetStyle(value, null, opt); } } if (em) { em.trigger('component:update', target); em.trigger('component:styleUpdate', target); em.trigger('component:styleUpdate:' + model.get('property'), target); } }, /** * Update target style * @param {string} value * @param {string} name * @param {Object} opts */ updateTargetStyle(value, name = '', opts = {}) { const property = name || this.model.get('property'); const target = this.getTarget(); const style = target.getStyle(); if (value) { style[property] = value; } else { delete style[property]; } target.setStyle(style, opts); // Helper is used by `states` like ':hover' to show its preview const helper = this.getHelperModel(); helper && helper.setStyle(style, opts); }, /** * Check if target is stylable with this property * The target could be the Component as the CSS Rule * @return {Boolean} */ isTargetStylable(target) { const trg = target || this.getTarget(); const model = this.model; const id = model.get('id'); const property = model.get('property'); const toRequire = model.get('toRequire'); const unstylable = trg.get('unstylable'); const stylableReq = trg.get('stylable-require'); let stylable = trg.get('stylable'); // Stylable could also be an array indicating with which property // the target could be styled if (isArray(stylable)) { stylable = stylable.indexOf(property) >= 0; } // Check if the property was signed as unstylable if (isArray(unstylable)) { stylable = unstylable.indexOf(property) < 0; } // Check if the property is available only if requested if (toRequire) { stylable = !target || (stylableReq && (stylableReq.indexOf(id) >= 0 || stylableReq.indexOf(property) >= 0)); } return stylable; }, /** * Check if the selected component is stylable with this property * The target could be the Component as the CSS Rule * @return {Boolean} */ isComponentStylable() { const em = this.em; const component = em && em.getWrapper(); if (!component) { return true; } return this.isTargetStylable(component); }, /** * Passed a raw value you have to update the input element, generally * is the value fetched from targets, so you can receive values with * functions, units, etc. (eg. `rotateY(45deg)`) * get also * @param {string} value * @private */ setRawValue(value) { this.setValue(this.model.parseValue(value)); }, /** * Update the element input. * Usually the value is a result of `model.getFullValue()` * @param {String} value The value from the model * */ setValue(value) { const model = this.model; let val = isUndefined(value) ? model.getDefaultValue() : value; const input = this.getInputEl(); input && (input.value = val); }, getInputEl() { if (!this.input) { this.input = this.el.querySelector('input'); } return this.input; }, updateVisibility() { this.el.style.display = this.model.get('visible') ? 'block' : 'none'; }, show() { this.model.set('visible', 1); }, hide() { this.model.set('visible', 0); }, /** * Clean input * */ cleanValue() { this.setValue(''); }, clearCached() { this.clearEl = null; this.input = null; this.$input = null; }, render() { this.clearCached(); const pfx = this.pfx; const model = this.model; const el = this.el; const property = model.get('property'); const full = model.get('full'); const className = `${pfx}property`; el.innerHTML = this.template(model); el.className = `${className} ${pfx}${model.get( 'type' )} ${className}__${property}`; el.className += full ? ` ${className}--full` : ''; this.updateStatus(); const onRender = this.onRender && this.onRender.bind(this); onRender && onRender(); this.setValue(model.get('value'), { targetUpdate: 1 }); } });