diff --git a/docs/modules/Components.md b/docs/modules/Components.md index c0c6881b2..77078fe48 100644 --- a/docs/modules/Components.md +++ b/docs/modules/Components.md @@ -747,6 +747,45 @@ editor.on(`component:remove`, model => console.log('Global hook: component:remov +## Components & CSS + +::: warning +This section is referring to GrapesJS v0.17.27 or higher +::: + +If you need to add component-related styles, you can do it via `styles` property. + +```js +domc.addType('component-css', { + model: { + defaults: { + attributes: { class: 'cmp-css' }, + components: ` + Component with styles +
Component A
+
Component B
+ `, + styles: ` + .cmp-css { color: red } + .cmp-css-a { color: green } + .cmp-css-b { color: blue } + + @media (max-width: 992px) { + .cmp-css{ color: darkred; } + .cmp-css-a { color: darkgreen } + .cmp-css-b { color: darkblue } + } + `, + }, + }, +}); +``` +This approach allows the editor to group these styles (CSSRule instances) and remove them accordingly in case all references of the same component are removed. + + + + + ## Components & JS If you want to know how to create Components with javascript attached (eg. counters, galleries, slideshows, etc.) check the dedicated page diff --git a/src/css_composer/index.js b/src/css_composer/index.js index 6f448b64f..9db98b2c1 100644 --- a/src/css_composer/index.js +++ b/src/css_composer/index.js @@ -190,6 +190,7 @@ export default () => { opt.state = s; opt.mediaText = w; opt.selectors = []; + w && (opt.atRuleType = 'media'); rule = new CssRule(opt, c); rule.get('selectors').add(selectors, addOpts); rules.add(rule, addOpts); @@ -199,16 +200,16 @@ export default () => { /** * Get the rule - * @param {Array} selectors Array of selectors - * @param {String} state Css rule state - * @param {String} width For which device this style is oriented + * @param {String|Array} selectors Array of selectors or selector string, eg `.myClass1.myClass2` + * @param {String} state Css rule state, eg. 'hover' + * @param {String} width Media rule value, eg. '(max-width: 992px)' * @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'); + * const sm = editor.SelectorManager; + * const sel1 = sm.add('myClass1'); + * const sel2 = sm.add('myClass2'); + * const rule = cssComposer.get([sel1, sel2], 'hover', '(max-width: 992px)'); * // Update the style * rule.set('style', { * width: '300px', @@ -216,12 +217,18 @@ export default () => { * }); * */ 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; + let slc = selectors; + if (isString(selectors)) { + const sm = em.get('SelectorManager'); + const singleSel = selectors.split(',')[0].trim(); + const node = em + .get('Parser') + .parserCss.checkNode({ selectors: singleSel })[0]; + slc = sm.get(node.selectors); + } + return ( + rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null + ); }, /** diff --git a/src/css_composer/model/CssRule.js b/src/css_composer/model/CssRule.js index dc40f7b23..2ae46e08c 100644 --- a/src/css_composer/model/CssRule.js +++ b/src/css_composer/model/CssRule.js @@ -1,7 +1,6 @@ -import { map } from 'underscore'; import { Model } from 'backbone'; import Styleable from 'domain_abstract/model/Styleable'; -import { isEmpty, forEach, isString } from 'underscore'; +import { isEmpty, forEach, isString, isArray } from 'underscore'; import Selectors from 'selector_manager/model/Selectors'; import { isEmptyObj, hasWin } from 'utils/mixins'; @@ -19,7 +18,7 @@ export default class CssRule extends Model.extend(Styleable) { // Css properties style style: {}, - // On which device width this rule should be rendered, eg. @media (max-width: 1000px) + // On which device width this rule should be rendered, eg. `(max-width: 1000px)` mediaText: '', // State of the rule, eg: hover | pressed | focused @@ -33,12 +32,12 @@ export default class CssRule extends Model.extend(Styleable) { // This particolar property is used only on at-rules, like 'page' or // 'font-face', where the block containes only style declarations - singleAtRule: 0, + singleAtRule: false, // If true, sets '!important' on all properties // You can use an array to specify properties to set important // Used in view - important: 0, + important: false, group: '', @@ -205,41 +204,44 @@ export default class CssRule extends Model.extend(Styleable) { /** * 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} 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} + * @returns {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; + 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') !== selectorsAdd || - this.get('atRuleType') !== atRuleType + this.get('selectorsAdd') !== selAdd || + this.get('atRuleType') !== atRule ) { - return f; + return false; } return true; diff --git a/src/style_manager/index.js b/src/style_manager/index.js index 48f49ad2e..5731ed53e 100644 --- a/src/style_manager/index.js +++ b/src/style_manager/index.js @@ -287,7 +287,13 @@ export default () => { if (hasClasses && useClasses) { const deviceW = em.getCurrentMedia(); rule = cssC.get(valid, state, deviceW); - + console.log('SM', { + valid, + state, + deviceW, + rule, + validNames: valid.map(v => v.getFullName()) + }); if (!rule && !skipAdd) { rule = cssC.add(valid, state, deviceW, {}, addOpts); } diff --git a/test/specs/css_composer/index.js b/test/specs/css_composer/index.js index 1ee273b73..20dc78af5 100644 --- a/test/specs/css_composer/index.js +++ b/test/specs/css_composer/index.js @@ -1,8 +1,4 @@ -import Models from './model/CssModels'; -import CssRuleView from './view/CssRuleView'; -import CssRulesView from './view/CssRulesView'; import CssComposer from 'css_composer'; -import e2e from './e2e/CssComposer'; import utils from './../../test_utils.js'; import Editor from 'editor/model/Editor'; @@ -32,6 +28,12 @@ describe('Css Composer', () => { config.em = editorModel; }; + const getCSS = obj => + obj + .getAll() + .map(r => r.toCSS()) + .join(''); + beforeEach(() => { em = new Editor({}); config = { em }; @@ -39,7 +41,7 @@ describe('Css Composer', () => { }); afterEach(() => { - obj = null; + em.destroy(); }); test('Object exists', () => { @@ -341,5 +343,55 @@ describe('Css Composer', () => { expect(obj.get(ruleClass.getSelectors())).toBe(ruleClass); expect(obj.get(ruleId.getSelectors())).toBe(ruleId); }); + + describe('Collections', () => { + test('Add a single rule as CSS string', () => { + const cssRule = `.test-rule{color:red;}`; + obj.addCollection(cssRule); + expect(obj.getAll().length).toEqual(1); + expect(getCSS(obj)).toEqual(cssRule); + }); + test('Add multiple rules as CSS string', () => { + const cssRules = [ + `.test-rule{color:red;}`, + `.test-rule:hover{color:blue;}`, + `@media (max-width: 992px){.test-rule{color:darkred;}}`, + `@media (max-width: 992px){.test-rule:hover{color:darkblue;}}` + ]; + const cssString = cssRules.join(''); + obj.addCollection(cssString); + expect(obj.getAll().length).toEqual(cssRules.length); + expect(getCSS(obj)).toEqual(cssString); + }); + test('Able to return the rule inserted as string', () => { + const cssRule = ` + .test-rule1 {color:red;} + .test-rule2:hover { + color: blue; + } + @media (max-width: 992px) { + .test-rule3 { + color: darkred; + } + .test-rule4:hover { + color: darkblue; + } + } + `; + const result = obj.addCollection(cssRule); + const [rule1, rule2, rule3, rule4] = result; + expect(result.length).toEqual(4); + expect(obj.getAll().length).toEqual(4); + + expect(obj.get('.test-rule1')).toBe(rule1); + expect(obj.get('.test-rule1', 'hover')).toBe(null); + expect(obj.get('.test-rule2', 'hover')).toBe(rule2); + expect(rule3.get('mediaText')).toBe('(max-width: 992px)'); + expect(obj.get('.test-rule3', null, '(max-width: 992px)')).toBe(rule3); + expect(obj.get('.test-rule4', 'hover', '(max-width: 992px)')).toBe( + rule4 + ); + }); + }); }); });