UNPKG

grapesjs-clot

Version:

Free and Open Source Web Builder Framework

296 lines (262 loc) 9.33 kB
import { Model } from 'backbone'; import Styleable from 'domain_abstract/model/Styleable'; import { isEmpty, forEach, isString, isArray } from 'underscore'; import Selectors from 'selector_manager/model/Selectors'; import { getMediaLength } from 'code_manager/model/CssGenerator'; import { isEmptyObj, hasWin } from 'utils/mixins'; const { CSS } = hasWin() ? window : {}; /** * @typedef CssRule * @property {Array<Selector>} selectors Array of selectors * @property {Object} style Object containing style definitions * @property {String} [selectorsAdd=''] Additional string css selectors * @property {String} [atRuleType=''] Type of at-rule, eg. `media`, 'font-face' * @property {String} [mediaText=''] At-rule value, eg. `(max-width: 1000px)` * @property {Boolean} [singleAtRule=false] This property is used only on at-rules, like 'page' or 'font-face', where the block containes only style declarations * @property {String} [state=''] State of the rule, eg: `hover`, `focused` * @property {Boolean|Array<String>} [important=false] If true, sets `!important` on all properties. You can also pass an array to specify properties on which use important * @property {Boolean} [stylable=true] Indicates if the rule is stylable from the editor * * [Device]: device.html * [State]: state.html * [Component]: component.html */ export default class CssRule extends Model.extend(Styleable) { defaults() { return { selectors: [], selectorsAdd: '', style: {}, mediaText: '', state: '', stylable: true, atRuleType: '', singleAtRule: false, important: false, group: '', _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); } /** * Returns 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 || opts.allowEmpty)) { result = singleAtRule ? style : `${selectors}{${style}}`; } return result; } /** * Get the Device the rule is related to. * @returns {[Device]|null} * @example * const device = rule.getDevice(); * console.log(device?.getName()); */ getDevice() { const { em } = this; const { atRuleType, mediaText } = this.attributes; const devices = em?.get('DeviceManager').getDevices() || []; const deviceDefault = devices.filter(d => d.getWidthMedia() === '')[0]; if (atRuleType !== 'media' || !mediaText) { return deviceDefault || null; } return ( devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] || null ); } /** * Get the State the rule is related to. * @returns {[State]|null} * @example * const state = rule.getState(); * console.log(state?.getLabel()); */ getState() { const { em } = this; const stateValue = this.get('state'); const states = em.get('SelectorManager').getStates() || []; return states.filter(s => s.getName() === stateValue)[0] || null; } /** * Returns the related Component (valid only for component-specific rules). * @returns {[Component]|null} * @example * const cmp = rule.getComponent(); * console.log(cmp?.toHTML()); */ getComponent() { const sel = this.getSelectors(); const sngl = sel.length == 1 && sel.at(0); const cmpId = sngl && sngl.isId() && sngl.get('name'); return (cmpId && this.em?.get('DomComponents').getById(cmpId)) || null; } /** * 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); if (block || opts.allowEmpty) { 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 * @returns {Boolean} * @private */ compare(selectors, state, width, ruleProps = {}) { const st = state || ''; const wd = width || ''; const selAdd = ruleProps.selectorsAdd || ''; let atRule = ruleProps.atRuleType || ''; const sel = !isArray(selectors) && !selectors.models ? [selectors] : selectors.models || selectors; // Fix atRuleType in case is not specified with width if (wd && !atRule) atRule = 'media'; const a1 = sel.map(model => model.getFullName()); const a2 = this.get('selectors').map(model => model.getFullName()); // Check selectors const a1S = a1.slice().sort(); const a2S = a2.slice().sort(); if (a1.length !== a2.length || !a1S.every((v, i) => v === a2S[i])) { return false; } // Check other properties if ( this.get('state') !== st || this.get('mediaText') !== wd || this.get('selectorsAdd') !== selAdd || this.get('atRuleType') !== atRule ) { return false; } return true; } }