mirror of https://github.com/artf/grapesjs.git
nocodeframeworkdrag-and-dropsite-buildersite-generatortemplate-builderui-builderweb-builderweb-builder-frameworkwebsite-builderno-codepage-builder
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.
245 lines
6.9 KiB
245 lines
6.9 KiB
import { map } from 'underscore';
|
|
import { Model } from 'backbone';
|
|
import Styleable from 'domain_abstract/model/Styleable';
|
|
import { isEmpty, forEach, isString } from 'underscore';
|
|
import Selectors from 'selector_manager/model/Selectors';
|
|
import { isEmptyObj, hasWin } from 'utils/mixins';
|
|
|
|
const { CSS } = hasWin() ? window : {};
|
|
|
|
export default class CssRule extends Model.extend(Styleable) {
|
|
defaults() {
|
|
return {
|
|
// Css selectors
|
|
selectors: [],
|
|
|
|
// Additional string css selectors
|
|
selectorsAdd: '',
|
|
|
|
// Css properties style
|
|
style: {},
|
|
|
|
// On which device width this rule should be rendered, eg. @media (max-width: 1000px)
|
|
mediaText: '',
|
|
|
|
// State of the rule, eg: hover | pressed | focused
|
|
state: '',
|
|
|
|
// Indicates if the rule is stylable
|
|
stylable: true,
|
|
|
|
// Type of at-rule, eg. 'media', 'font-face', etc.
|
|
atRuleType: '',
|
|
|
|
// This particolar property is used only on at-rules, like 'page' or
|
|
// 'font-face', where the block containes only style declarations
|
|
singleAtRule: 0,
|
|
|
|
// If true, sets '!important' on all properties
|
|
// You can use an array to specify properties to set important
|
|
// Used in view
|
|
important: 0,
|
|
|
|
_undo: true
|
|
};
|
|
}
|
|
|
|
initialize(c, opt = {}) {
|
|
this.config = c || {};
|
|
this.opt = opt;
|
|
this.em = opt.em;
|
|
this.ensureSelectors();
|
|
this.on('change', this.__onChange);
|
|
}
|
|
|
|
__onChange(m, opts) {
|
|
const { em } = this;
|
|
const changed = this.changedAttributes();
|
|
!isEmptyObj(changed) && em && em.changesUp(opts);
|
|
}
|
|
|
|
clone() {
|
|
const opts = { ...this.opt };
|
|
const attr = { ...this.attributes };
|
|
attr.selectors = this.get('selectors').map(s => s.clone());
|
|
return new this.constructor(attr, opts);
|
|
}
|
|
|
|
ensureSelectors(m, c, opts) {
|
|
const { em } = this;
|
|
const sm = em && em.get('SelectorManager');
|
|
const toListen = [this, 'change:selectors', this.ensureSelectors];
|
|
let sels = this.getSelectors();
|
|
this.stopListening(...toListen);
|
|
|
|
if (sels.models) {
|
|
sels = [...sels.models];
|
|
}
|
|
|
|
sels = isString(sels) ? [sels] : sels;
|
|
|
|
if (Array.isArray(sels)) {
|
|
const res = sels.filter(i => i).map(i => (sm ? sm.add(i) : i));
|
|
sels = new Selectors(res);
|
|
}
|
|
|
|
this.set('selectors', sels, opts);
|
|
this.listenTo(...toListen);
|
|
}
|
|
|
|
/**
|
|
* Return the at-rule statement when exists, eg. '@media (...)', '@keyframes'
|
|
* @returns {String}
|
|
* @example
|
|
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
|
|
* atRuleType: 'media',
|
|
* atRuleParams: '(min-width: 500px)'
|
|
* });
|
|
* cssRule.getAtRule(); // "@media (min-width: 500px)"
|
|
*/
|
|
getAtRule() {
|
|
const type = this.get('atRuleType');
|
|
const condition = this.get('mediaText');
|
|
// Avoid breaks with the last condition
|
|
const typeStr = type ? `@${type}` : condition ? '@media' : '';
|
|
|
|
return typeStr + (condition && typeStr ? ` ${condition}` : '');
|
|
}
|
|
|
|
/**
|
|
* Return selectors of the rule as a string
|
|
* @param {Object} [opts] Options
|
|
* @param {Boolean} [opts.skipState] Skip state from the result
|
|
* @returns {String}
|
|
* @example
|
|
* const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' });
|
|
* cssRule.selectorsToString(); // ".class1:hover"
|
|
* cssRule.selectorsToString({ skipState: true }); // ".class1"
|
|
*/
|
|
selectorsToString(opts = {}) {
|
|
const result = [];
|
|
const state = this.get('state');
|
|
const wrapper = this.get('wrapper');
|
|
const addSelector = this.get('selectorsAdd');
|
|
const isBody = wrapper && opts.body;
|
|
const selOpts = {
|
|
escape: str => (CSS && CSS.escape ? CSS.escape(str) : str)
|
|
};
|
|
const selectors = isBody
|
|
? 'body'
|
|
: this.get('selectors').getFullString(0, selOpts);
|
|
const stateStr = state && !opts.skipState ? `:${state}` : '';
|
|
selectors && result.push(`${selectors}${stateStr}`);
|
|
addSelector && !opts.skipAdd && result.push(addSelector);
|
|
return result.join(', ');
|
|
}
|
|
|
|
/**
|
|
* Get declaration block (without the at-rule statement)
|
|
* @param {Object} [opts={}] Options (same as in `selectorsToString`)
|
|
* @returns {String}
|
|
* @example
|
|
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
|
|
* atRuleType: 'media',
|
|
* atRuleParams: '(min-width: 500px)'
|
|
* });
|
|
* cssRule.getDeclaration() // ".class1{color:red;}"
|
|
*/
|
|
getDeclaration(opts = {}) {
|
|
let result = '';
|
|
const selectors = this.selectorsToString(opts);
|
|
const style = this.styleToString(opts);
|
|
const singleAtRule = this.get('singleAtRule');
|
|
|
|
if ((selectors || singleAtRule) && style) {
|
|
result = singleAtRule ? style : `${selectors}{${style}}`;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Return the CSS string of the rule
|
|
* @param {Object} [opts={}] Options (same as in `getDeclaration`)
|
|
* @return {String} CSS string
|
|
* @example
|
|
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
|
|
* atRuleType: 'media',
|
|
* atRuleParams: '(min-width: 500px)'
|
|
* });
|
|
* cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}"
|
|
*/
|
|
toCSS(opts = {}) {
|
|
let result = '';
|
|
const atRule = this.getAtRule();
|
|
const block = this.getDeclaration(opts);
|
|
block && (result = block);
|
|
|
|
if (atRule && result) {
|
|
result = `${atRule}{${result}}`;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
toJSON(...args) {
|
|
const obj = Model.prototype.toJSON.apply(this, args);
|
|
|
|
if (this.em.getConfig('avoidDefaults')) {
|
|
const defaults = this.defaults();
|
|
|
|
forEach(defaults, (value, key) => {
|
|
if (obj[key] === value) {
|
|
delete obj[key];
|
|
}
|
|
});
|
|
|
|
if (isEmpty(obj.selectors)) delete obj.selectors;
|
|
if (isEmpty(obj.style)) delete obj.style;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Compare the actual model with parameters
|
|
* @param {Object} selectors Collection of selectors
|
|
* @param {String} state Css rule state
|
|
* @param {String} width For which device this style is oriented
|
|
* @param {Object} ruleProps Other rule props
|
|
* @return {Boolean}
|
|
* @private
|
|
*/
|
|
compare(selectors, state, width, ruleProps = {}) {
|
|
var st = state || '';
|
|
var wd = width || '';
|
|
var selectorsAdd = ruleProps.selectorsAdd || '';
|
|
var atRuleType = ruleProps.atRuleType || '';
|
|
if (!(selectors instanceof Array) && !selectors.models)
|
|
selectors = [selectors];
|
|
var a1 = map(selectors.models || selectors, model => model.getFullName());
|
|
var a2 = map(this.get('selectors').models, model => model.getFullName());
|
|
var f = false;
|
|
|
|
if (a1.length !== a2.length) return f;
|
|
|
|
for (var i = 0; i < a1.length; i++) {
|
|
var re = 0;
|
|
for (var j = 0; j < a2.length; j++) {
|
|
if (a1[i] === a2[j]) re = 1;
|
|
}
|
|
if (re === 0) return f;
|
|
}
|
|
|
|
if (
|
|
this.get('state') !== st ||
|
|
this.get('mediaText') !== wd ||
|
|
this.get('selectorsAdd') !== selectorsAdd ||
|
|
this.get('atRuleType') !== atRuleType
|
|
) {
|
|
return f;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|