@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
266 lines (224 loc) • 7.42 kB
text/typescript
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(' ');
}
}