UNPKG

@print-one/grapesjs

Version:

Free and Open Source Web Builder Framework

266 lines (224 loc) 7.42 kB
import { bindAll, isUndefined, each } from 'underscore'; import { Model } from '../../common'; import CssComposer from '../../css_composer'; import CssRule from '../../css_composer/model/CssRule'; import CssRules from '../../css_composer/model/CssRules'; import Component from '../../dom_components/model/Component'; import EditorModel from '../../editor/model/Editor'; import { hasWin } from '../../utils/mixins'; const maxValue = Number.MAX_VALUE; export const getMediaLength = (mediaQuery: string) => { const length = /(-?\d*\.?\d+)\w{0,}/.exec(mediaQuery); return !length ? '' : length[0]; }; export type CssGeneratorBuildOptions = { /** * Return an array of CssRules instead of the CSS string. */ json?: boolean; /** * Return only rules matched by the passed component. */ onlyMatched?: boolean; /** * Force keep all defined rules. Toggle on in case output looks different inside/outside of the editor. */ keepUnusedStyles?: boolean; rules?: CssRule[]; clearStyles?: boolean; }; type CssGeneratorBuildOptionsProps = CssGeneratorBuildOptions & { em?: EditorModel; cssc?: CssComposer; }; type AtRules = Record<string, CssRule[]>; export default class CssGenerator extends Model { compCls: string[]; ids: string[]; model?: Component; em?: EditorModel; constructor() { super(); bindAll(this, 'sortRules'); this.compCls = []; this.ids = []; } /** * Get CSS from a component * @param {Model} model * @return {String} */ buildFromModel(model: Component, opts: CssGeneratorBuildOptions = {}) { let code = ''; const em = this.em; const avoidInline = em && em.getConfig().avoidInlineStyle; const style = model.styleToString(); const classes = model.classes; this.ids.push(`#${model.getId()}`); // Let's know what classes I've found classes.forEach((model: any) => this.compCls.push(model.getFullName())); if (!avoidInline && style) { code = `#${model.getId()}{${style}}`; } const components = model.components(); components.forEach((model: Component) => (code += this.buildFromModel(model, opts))); return code; } build(model: Component, opts: CssGeneratorBuildOptionsProps = {}) { const { json } = opts; const em = opts.em; const cssc = opts.cssc || em?.Css; this.em = em; this.compCls = []; this.ids = []; this.model = model; const codeJson: CssRule[] = []; let code = model ? this.buildFromModel(model, opts) : ''; const clearStyles = isUndefined(opts.clearStyles) && em ? em.getConfig().clearStyles : opts.clearStyles; if (cssc) { let rules: CssRules | CssRule[] = opts.rules || cssc.getAll(); const atRules: AtRules = {}; const dump: CssRule[] = []; if (opts.onlyMatched && model && hasWin()) { rules = this.matchedRules(model, rules); } rules.forEach(rule => { const atRule = rule.getAtRule(); if (atRule) { const mRules = atRules[atRule]; if (mRules) { mRules.push(rule); } else { atRules[atRule] = [rule]; } return; } const res = this.buildFromRule(rule, dump, opts); if (json) { codeJson.push(res as CssRule); } else { code += res; } }); this.sortMediaObject(atRules).forEach(item => { let rulesStr = ''; const atRule = item.key; const mRules = item.value; mRules.forEach(rule => { const ruleStr = this.buildFromRule(rule, dump, opts); if (rule.get('singleAtRule')) { code += `${atRule}{${ruleStr}}`; } else { rulesStr += ruleStr; } json && codeJson.push(ruleStr as CssRule); }); if (rulesStr) { code += `${atRule}{${rulesStr}}`; } }); // @ts-ignore em && clearStyles && rules.remove && rules.remove(dump); } return json ? codeJson.filter(r => r) : code; } /** * Get CSS from the rule model * @param {Model} rule * @return {string} CSS string */ buildFromRule(rule: CssRule, dump: CssRule[], opts: CssGeneratorBuildOptions = {}) { let result: CssRule | string = ''; const { model } = this; const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 }); const selectorsAdd = rule.get('selectorsAdd'); const singleAtRule = rule.get('singleAtRule'); let found; // This will not render a rule if there is no its component rule.get('selectors')?.forEach(selector => { const name = selector.getFullName(); if (this.compCls.indexOf(name) >= 0 || this.ids.indexOf(name) >= 0 || opts.keepUnusedStyles) { found = 1; } }); if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule || !model) { const block = rule.getDeclaration({ body: 1 }); block && (opts.json ? (result = rule) : (result += block)); } else { dump.push(rule); } return result; } /** * Get matched rules of a component * @param {Component} component * @param {Array<CSSRule>} rules * @returns {Array<CSSRule>} */ matchedRules(component: Component, rules: CssRules | CssRule[]) { const el = component.getEl(); let result: CssRule[] = []; rules.forEach(rule => { try { if ( rule .selectorsToString() .split(',') .some(selector => el?.matches(this.__cleanSelector(selector))) ) { result.push(rule); } } catch (err) {} }); component.components().forEach((component: Component) => { result = result.concat(this.matchedRules(component, rules)); }); // Remove duplicates result = result.filter((rule, i) => result.indexOf(rule) === i); return result; } /** * Get the numeric length of the media query string * @param {String} mediaQuery Media query string * @return {Number} */ getQueryLength(mediaQuery: string) { const length = /(-?\d*\.?\d+)\w{0,}/.exec(mediaQuery); if (!length) return maxValue; return parseFloat(length[1]); } /** * Return a sorted array from media query object * @param {Object} items * @return {Array} */ sortMediaObject(items: AtRules = {}) { const itemsArr: { key: string; value: CssRule[] }[] = []; each(items, (value, key) => itemsArr.push({ key, value })); return itemsArr.sort((a, b) => { const isMobFirst = [a.key, b.key].every(mquery => mquery.indexOf('min-width') !== -1); const left = isMobFirst ? a.key : b.key; const right = isMobFirst ? b.key : a.key; return this.getQueryLength(left) - this.getQueryLength(right); }); } sortRules(a: CssRule, b: CssRule) { const getKey = (rule: CssRule) => rule.get('mediaText') || ''; const isMobFirst = [getKey(a), getKey(b)].every(q => q.indexOf('min-width') !== -1); const left = isMobFirst ? getKey(a) : getKey(b); const right = isMobFirst ? getKey(b) : getKey(a); return this.getQueryLength(left) - this.getQueryLength(right); } /** * Return passed selector without states * @param {String} selector * @returns {String} * @private */ __cleanSelector(selector: string) { return selector .split(' ') .map(item => item.split(':')[0]) .join(' '); } }