UNPKG

grapesjs_codeapps

Version:

Free and Open Source Web Builder Framework/SC Modification

395 lines (357 loc) 11.7 kB
/** * This module contains and manage CSS rules for the template inside the canvas. * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js) * ```js * const editor = grapesjs.init({ * cssComposer: { * // options * } * }) * ``` * * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance * * ```js * const cssComposer = editor.CssComposer; * ``` * * * [load](#load) * * [store](#store) * * [add](#add) * * [get](#get) * * [getAll](#getall) * * [clear](#clear) * * [setIdRule](#setidrule) * * [getIdRule](#getidrule) * * [setClassRule](#setclassrule) * * [getClassRule](#getclassrule) * * @module CssComposer */ import { isArray } from 'underscore'; module.exports = () => { let em; var c = {}, defaults = require('./config/config'), CssRule = require('./model/CssRule'), CssRules = require('./model/CssRules'), CssRulesView = require('./view/CssRulesView'); const Selectors = require('selector_manager/model/Selectors'); const Selector = require('selector_manager/model/Selector'); var rules, rulesView; return { Selectors, /** * Name of the module * @type {String} * @private */ name: 'CssComposer', /** * Mandatory for the storage manager * @type {String} * @private */ storageKey() { var keys = []; var smc = (c.stm && c.stm.getConfig()) || {}; if (smc.storeCss) keys.push('css'); if (smc.storeStyles) keys.push('styles'); return keys; }, /** * Initializes module. Automatically called with a new instance of the editor * @param {Object} config Configurations * @private */ init(config) { c = config || {}; for (var name in defaults) { if (!(name in c)) c[name] = defaults[name]; } var ppfx = c.pStylePrefix; if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; var elStyle = (c.em && c.em.config.style) || ''; c.rules = elStyle || c.rules; em = c.em; rules = new CssRules([], c); rulesView = new CssRulesView({ collection: rules, config: c }); return this; }, /** * On load callback * @private */ onLoad() { rules.add(c.rules); }, /** * Do stuff after load * @param {Editor} em * @private */ postLoad(em) { const ev = 'add remove'; const rules = this.getAll(); const um = em.get('UndoManager'); um && um.add(rules); em.stopListening(rules, ev, this.handleChange); em.listenTo(rules, ev, this.handleChange); rules.each(rule => this.handleChange(rule, { avoidStore: 1 })); }, /** * Handle rule changes * @private */ handleChange(model, opts = {}) { const ev = 'change:style'; const um = em.get('UndoManager'); um && um.add(model); const handleUpdates = em.handleUpdates.bind(em); em.stopListening(model, ev, handleUpdates); em.listenTo(model, ev, handleUpdates); !opts.avoidStore && handleUpdates('', '', opts); }, /** * Load data from the passed object, if the object is empty will try to fetch them * autonomously from the storage manager. * The fetched data will be added to the collection * @param {Object} data Object of data to load * @return {Object} Loaded rules */ load(data) { var d = data || ''; if (!d && c.stm) { d = c.em.getCacheLoad(); } var obj = d.styles || ''; if (d.styles) { try { obj = JSON.parse(d.styles); } catch (err) {} } else if (d.css) { obj = c.em.get('Parser').parseCss(d.css); } if (isArray(obj)) { obj.length && rules.reset(obj); } else if (obj) { rules.reset(obj); } return obj; }, /** * Store data to the selected storage * @param {Boolean} noStore If true, won't store * @return {Object} Data to store */ store(noStore) { if (!c.stm) return; var obj = {}; var keys = this.storageKey(); if (keys.indexOf('css') >= 0) obj.css = c.em.getCss(); if (keys.indexOf('styles') >= 0) obj.styles = JSON.stringify(rules); if (!noStore) c.stm.store(obj); return obj; }, /** * Add new rule to the collection, if not yet exists with the same selectors * @param {Array<Selector>} selectors Array of selectors * @param {String} state Css rule state * @param {String} width For which device this style is oriented * @param {Object} opts Other options for the rule * @return {Model} * @example * var sm = editor.SelectorManager; * var sel1 = sm.add('myClass1'); * var sel2 = sm.add('myClass2'); * var rule = cssComposer.add([sel1, sel2], 'hover'); * rule.set('style', { * width: '100px', * color: '#fff', * }); * */ add(selectors, state, width, opts = {}) { var s = state || ''; var w = width || ''; var opt = { ...opts }; var rule = this.get(selectors, s, w, opt); // do not create rules that were found before // unless this is a single at-rule, for which multiple declarations // make sense (e.g. multiple `@font-type`s) if (rule && rule.config && !rule.config.singleAtRule) { return rule; } else { opt.state = s; opt.mediaText = w; opt.selectors = ''; rule = new CssRule(opt, c); rule.get('selectors').add(selectors); rules.add(rule); return rule; } }, /** * Get the rule * @param {Array<Selector>} selectors Array 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 {Model|null} * @example * var sm = editor.SelectorManager; * var sel1 = sm.add('myClass1'); * var sel2 = sm.add('myClass2'); * var rule = cssComposer.get([sel1, sel2], 'hover'); * // Update the style * rule.set('style', { * width: '300px', * color: '#000', * }); * */ get(selectors, state, width, ruleProps) { var rule = null; rules.each(m => { if (rule) return; if (m.compare(selectors, state, width, ruleProps)) rule = m; }); return rule; }, /** * Get the collection of rules * @return {Collection} * */ getAll() { return rules; }, /** * Remove all rules * @return {this} */ clear() { this.getAll().reset(); return this; }, /** * Add a raw collection of rule objects * This method overrides styles, in case, of already defined rule * @param {Array<Object>} data Array of rule objects, eg . [{selectors: ['class1'], style: {....}}, ..] * @param {Object} opts Options * @return {Array<Model>} * @private */ addCollection(data, opts = {}) { var result = []; var d = data instanceof Array ? data : [data]; for (var i = 0, l = d.length; i < l; i++) { var rule = d[i] || {}; if (!rule.selectors) continue; var sm = c.em && c.em.get('SelectorManager'); if (!sm) console.warn('Selector Manager not found'); var sl = rule.selectors; var sels = sl instanceof Array ? sl : [sl]; var newSels = []; for (var j = 0, le = sels.length; j < le; j++) { var selec = sm.add(sels[j]); newSels.push(selec); } var modelExists = this.get(newSels, rule.state, rule.mediaText, rule); var model = this.add(newSels, rule.state, rule.mediaText, rule); var updateStyle = !modelExists || !opts.avoidUpdateStyle; const style = rule.style || {}; if (updateStyle) { let styleUpdate = opts.extend ? { ...model.get('style'), ...style } : style; model.set('style', styleUpdate); } result.push(model); } return result; }, /** * Add/update the CSS rule with id selector * @param {string} name Id selector name, eg. 'my-id' * @param {Object} style Style properties and values * @param {Object} [opts={}] Custom options, like `state` and `mediaText` * @return {CssRule} The new/updated rule * @example * const rule = cc.setIdRule('myid', { color: 'red' }); * const ruleHover = cc.setIdRule('myid', { color: 'blue' }, { state: 'hover' }); * // This will add current CSS: * // #myid { color: red } * // #myid:hover { color: blue } */ setIdRule(name, style = {}, opts = {}) { const state = opts.state || ''; const media = opts.mediaText || em.getCurrentMedia(); const sm = em.get('SelectorManager'); const selector = sm.add({ name, type: Selector.TYPE_ID }); const rule = this.add(selector, state, media); rule.setStyle(style, opts); return rule; }, /** * Get the CSS rule by id selector * @param {string} name Id selector name, eg. 'my-id' * @param {Object} [opts={}] Custom options, like `state` and `mediaText` * @return {CssRule} * @example * const rule = cc.getIdRule('myid'); * const ruleHover = cc.setIdRule('myid', { state: 'hover' }); */ getIdRule(name, opts = {}) { const state = opts.state || ''; const media = opts.mediaText || em.getCurrentMedia(); const selector = em.get('SelectorManager').get(name, Selector.TYPE_ID); return selector && this.get(selector, state, media); }, /** * Add/update the CSS rule with class selector * @param {string} name Class selector name, eg. 'my-class' * @param {Object} style Style properties and values * @param {Object} [opts={}] Custom options, like `state` and `mediaText` * @return {CssRule} The new/updated rule * @example * const rule = cc.setClassRule('myclass', { color: 'red' }); * const ruleHover = cc.setClassRule('myclass', { color: 'blue' }, { state: 'hover' }); * // This will add current CSS: * // .myclass { color: red } * // .myclass:hover { color: blue } */ setClassRule(name, style = {}, opts = {}) { const state = opts.state || ''; const media = opts.mediaText || em.getCurrentMedia(); const sm = em.get('SelectorManager'); const selector = sm.add({ name, type: Selector.TYPE_CLASS }); const rule = this.add(selector, state, media); rule.setStyle(style, opts); return rule; }, /** * Get the CSS rule by class selector * @param {string} name Class selector name, eg. 'my-class' * @param {Object} [opts={}] Custom options, like `state` and `mediaText` * @return {CssRule} * @example * const rule = cc.getClassRule('myclass'); * const ruleHover = cc.getClassRule('myclass', { state: 'hover' }); */ getClassRule(name, opts = {}) { const state = opts.state || ''; const media = opts.mediaText || em.getCurrentMedia(); const selector = em.get('SelectorManager').get(name, Selector.TYPE_CLASS); return selector && this.get(selector, state, media); }, /** * Render the block of CSS rules * @return {HTMLElement} * @private */ render() { return rulesView.render().el; } }; };