@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
281 lines (241 loc) • 8.21 kB
text/typescript
import { bindAll, isUndefined, debounce } from 'underscore';
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { isObject } from '../../utils/mixins';
import Property from '../model/Property';
import { StyleProps } from '../../domain_abstract/model/StyleableModel';
const clearProp = 'data-clear-style';
export interface ICustomPropertyView {
create?: (data: ReturnType<PropertyView['_getClbOpts']>) => any;
destroy?: (data: ReturnType<PropertyView['_getClbOpts']>) => any;
update?: (data: ReturnType<PropertyView['_getClbOpts']> & { value: string }) => any;
emit?: (data: ReturnType<PropertyView['_getClbOpts']>, ...args: any) => any;
unset?: (data: ReturnType<PropertyView['_getClbOpts']>) => any;
}
export type CustomPropertyView<T> = ICustomPropertyView & T & ThisType<T & PropertyView>;
export default class PropertyView extends View<Property> {
em: EditorModel;
pfx: string;
ppfx: string;
config: any;
parent?: PropertyView;
__destroyFn!: Function;
create?: Function;
destroy?: Function;
update?: Function;
emit?: Function;
unset?: Function;
clearEl?: HTMLElement;
createdEl?: HTMLElement;
input?: HTMLInputElement;
$input?: any;
constructor(o = {}) {
super(o);
bindAll(this, '__change', '__updateStyle');
// @ts-ignore
const config = o.config || {};
const { em } = config;
this.config = config;
this.em = em;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.__destroyFn = this.destroy ? this.destroy.bind(this) : () => {};
const { model } = this;
// @ts-ignore
model.view = this;
// Put a sligh delay on debounce in order to execute the update
// post styleManager.__upProps trigger.
this.onValueChange = debounce(this.onValueChange.bind(this), 10);
this.updateStatus = debounce(this.updateStatus.bind(this), 0);
this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model, 'change:visible', this.updateVisibility);
this.listenTo(model, 'change:name change:className change:full', this.render);
this.listenTo(model, 'change:value', this.onValueChange);
this.listenTo(model, 'change:parentTarget', this.updateStatus);
this.listenTo(em, 'change:device', this.onValueChange);
// @ts-ignore
const init = this.init && this.init.bind(this);
init && init();
}
events() {
return {
change: 'inputValueChanged',
[`click [${clearProp}]`]: 'clear',
};
}
template(model: any) {
const { pfx, ppfx } = this;
return `
<div class="${pfx}label" data-sm-label></div>
<div class="${ppfx}fields" data-sm-fields></div>
`;
}
templateLabel(model: Property) {
const { pfx, em } = this;
const { parent } = model;
const { icon = '', info = '' } = model.attributes;
const icons = em?.getConfig().icons;
const iconClose = icons?.close || '';
return `
<span class="${pfx}icon ${icon}" title="${info}">
${model.getLabel()}
</span>
${!parent ? `<div class="${pfx}clear" style="display: none" ${clearProp}>${iconClose}</div>` : ''}
`;
}
templateInput(model: Property) {
return `
<div class="${this.ppfx}field">
<input placeholder="${model.getDefaultValue()}"/>
</div>
`;
}
remove() {
View.prototype.remove.apply(this, arguments as any);
// @ts-ignore
['em', 'input', '$input', 'view'].forEach(i => (this[i] = null));
this.__destroyFn(this._getClbOpts());
return this;
}
/**
* Triggers when the status changes. The status indicates if the value of
* the proprerty is changed or inherited
* @private
*/
updateStatus() {
const { model, pfx, ppfx, config } = this;
const updatedCls = `${ppfx}four-color`;
const computedCls = `${ppfx}color-warn`;
const labelEl = this.$el.children(`.${pfx}label`);
const clearStyleEl = this.getClearEl();
const clearStyle = clearStyleEl ? clearStyleEl.style : ({} as CSSStyleDeclaration);
labelEl.removeClass(`${updatedCls} ${computedCls}`);
clearStyle.display = 'none';
if (model.hasValue({ noParent: true }) && config.highlightChanged) {
labelEl.addClass(updatedCls);
config.clearProperties && (clearStyle.display = '');
} else if (model.hasValue() && config.highlightComputed) {
labelEl.addClass(computedCls);
}
this.parent?.updateStatus();
}
/**
* Clear the property from the target
*/
clear(ev: Event) {
ev && ev.stopPropagation();
this.model.clear();
}
/**
* Get clear element
* @return {HTMLElement}
*/
getClearEl() {
if (!this.clearEl) {
this.clearEl = this.el.querySelector(`[${clearProp}]`)!;
}
return this.clearEl;
}
/**
* 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(ev: any) {
ev && ev.stopPropagation();
// Skip the default update in case a custom emit method is defined
if (this.emit) return;
this.model.upValue(ev.target.value);
}
onValueChange(m: any, val: any, opt: any = {}) {
this.setValue(this.model.getFullValue(undefined, { skipImportant: true }));
this.updateStatus();
}
/**
* Update the element input.
* Usually the value is a result of `model.getFullValue()`
* @param {String} value The value from the model
* */
setValue(value: string) {
const { model } = this;
const result = isUndefined(value) || value === '' ? model.getDefaultValue() : value;
if (this.update) return this.__update(result);
this.__setValueInput(result);
}
__setValueInput(value: string) {
const input = this.getInputEl();
input && (input.value = value);
}
getInputEl() {
if (!this.input) {
this.input = this.el.querySelector('input')!;
}
return this.input;
}
updateVisibility() {
this.el.style.display = this.model.isVisible() ? '' : 'none';
}
clearCached() {
delete this.clearEl;
delete this.input;
delete this.$input;
}
__unset() {
const unset = this.unset && this.unset.bind(this);
unset && unset(this._getClbOpts());
}
__update(value: string) {
const update = this.update && this.update.bind(this);
update &&
update({
...this._getClbOpts(),
value,
});
}
__change(...args: any) {
const emit = this.emit && this.emit.bind(this);
emit && emit(this._getClbOpts(), ...args);
}
__updateStyle(value: string | StyleProps, { complete, partial, ...opts }: any = {}) {
const { model } = this;
const final = complete !== false && partial !== true;
if (isObject(value)) {
model.__upTargetsStyle(value as StyleProps, { avoidStore: !final });
} else {
model.upValue(value, { partial: !final });
}
}
_getClbOpts() {
const { model, el, createdEl } = this;
return {
el,
createdEl,
property: model,
props: model.attributes,
change: this.__change,
updateStyle: this.__updateStyle,
};
}
render() {
this.clearCached();
const { pfx, model, el, $el } = this;
const name = model.getName();
const type = model.getType();
const cls = model.get('className') || '';
const className = `${pfx}property`;
// Support old integer classname
const clsType = type === 'number' ? `${pfx}${type} ${pfx}integer` : `${pfx}${type}`;
this.createdEl && this.__destroyFn(this._getClbOpts());
$el.empty().append(this.template(model));
$el.find('[data-sm-label]').append(this.templateLabel(model));
const create = this.create && this.create.bind(this);
this.createdEl = create && create(this._getClbOpts());
$el.find('[data-sm-fields]').append(this.createdEl || this.templateInput(model));
el.className = `${className} ${clsType} ${className}__${name} ${cls}`.trim();
el.className += model.isFull() ? ` ${className}--full` : '';
const onRender = this.onRender && this.onRender.bind(this);
onRender && onRender();
this.setValue(model.getValue());
return this;
}
onRender() {}
}