diff --git a/src/canvas/view/CanvasView.js b/src/canvas/view/CanvasView.js index a42b89d4d..68fda7a75 100644 --- a/src/canvas/view/CanvasView.js +++ b/src/canvas/view/CanvasView.js @@ -1,10 +1,11 @@ +import { on, off } from 'utils/mixins'; const FrameView = require('./FrameView'); const $ = Backbone.$; module.exports = Backbone.View.extend({ initialize(o) { _.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff'); - window.onscroll = this.clearOff; + on(window, 'scroll resize', this.clearOff); this.config = o.config || {}; this.em = this.config.em || {}; this.ppfx = this.config.pStylePrefix || ''; diff --git a/src/code_manager/model/CssGenerator.js b/src/code_manager/model/CssGenerator.js index d715c50db..3b857cad0 100644 --- a/src/code_manager/model/CssGenerator.js +++ b/src/code_manager/model/CssGenerator.js @@ -43,19 +43,18 @@ module.exports = require('backbone').Model.extend({ if (cssc) { const rules = cssc.getAll(); - const mediaRules = {}; + const atRules = {}; const dump = []; rules.each(rule => { - const media = rule.get('mediaText'); + const atRule = rule.getAtRule(); - // If media is setted, I'll render it later - if (media) { - const mRules = mediaRules[media]; + if (atRule) { + const mRules = atRules[atRule]; if (mRules) { mRules.push(rule); } else { - mediaRules[media] = [rule]; + atRules[atRule] = [rule]; } return; } @@ -63,14 +62,14 @@ module.exports = require('backbone').Model.extend({ code += this.buildFromRule(rule, dump); }); - // Get media rules - for (let media in mediaRules) { + // Get at-rules + for (let atRule in atRules) { let rulesStr = ''; - const mRules = mediaRules[media]; + const mRules = atRules[atRule]; mRules.forEach(rule => (rulesStr += this.buildFromRule(rule, dump))); if (rulesStr) { - code += `@media ${media}{${rulesStr}}`; + code += `${atRule}{${rulesStr}}`; } } @@ -87,8 +86,9 @@ module.exports = require('backbone').Model.extend({ */ buildFromRule(rule, dump) { let result = ''; - const selectorStr = rule.selectorsToString(); 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 @@ -99,12 +99,9 @@ module.exports = require('backbone').Model.extend({ } }); - if ((selectorStrNoAdd && found) || rule.get('selectorsAdd')) { - const style = rule.styleToString(); - - if (style) { - result += `${selectorStr}{${style}}`; - } + if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule) { + const block = rule.getDeclaration(); + block && (result += block); } else { dump.push(rule); } diff --git a/src/css_composer/model/CssRule.js b/src/css_composer/model/CssRule.js index 1ea86c623..7599db4a1 100644 --- a/src/css_composer/model/CssRule.js +++ b/src/css_composer/model/CssRule.js @@ -23,6 +23,13 @@ module.exports = Backbone.Model.extend(Styleable).extend({ // Indicates if the rule is stylable stylable: true, + // Type of at-rule, eg. 'media', 'font-face', etc. + atRuleType: '', + + // This particolar property is used only on at-rules, like 'page' or + // 'font-face', where the block containes only style declarations + singleAtRule: 0, + // If true, sets '!important' on all properties // You can use an array to specify properties to set important // Used in view @@ -47,6 +54,19 @@ module.exports = Backbone.Model.extend(Styleable).extend({ this.set('selectors', new Selectors(selectors)); }, + /** + * Returns an at-rule statement if possible, eg. '@media (...)', '@keyframes' + * @return {string} + */ + 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 fo the rule as a string * @return {string} @@ -63,22 +83,36 @@ module.exports = Backbone.Model.extend(Styleable).extend({ }, /** - * Returns CSS string of the rule + * Get declaration block * @param {Object} [opts={}] Options * @return {string} */ - toCSS(opts = {}) { + getDeclaration(opts = {}) { let result = ''; - const media = this.get('mediaText'); - const style = this.styleToString(opts); const selectors = this.selectorsToString(); + const style = this.styleToString(opts); + const singleAtRule = this.get('singleAtRule'); - if (selectors && style) { - result = `${selectors}{${style}}`; + if ((selectors || singleAtRule) && style) { + result = singleAtRule ? style : `${selectors}{${style}}`; } - if (media && result) { - result = `@media ${media}{${result}}`; + return result; + }, + + /** + * Returns CSS string of the rule + * @param {Object} [opts={}] Options + * @return {string} + */ + toCSS(opts = {}) { + let result = ''; + const atRule = this.getAtRule(); + const block = this.getDeclaration(opts); + block && (result = block); + + if (atRule && result) { + result = `${atRule}{${result}}`; } return result; @@ -93,11 +127,11 @@ module.exports = Backbone.Model.extend(Styleable).extend({ * @return {Boolean} * @private */ - compare(selectors, state, width, ruleProps) { - var otherRule = ruleProps || {}; + compare(selectors, state, width, ruleProps = {}) { var st = state || ''; var wd = width || ''; - var selectorsAdd = otherRule.selectorsAdd || ''; + var selectorsAdd = ruleProps.selectorsAdd || ''; + var atRuleType = ruleProps.atRuleType || ''; var cId = 'cid'; //var a1 = _.pluck(selectors.models || selectors, cId); //var a2 = _.pluck(this.get('selectors').models, cId); @@ -117,11 +151,14 @@ module.exports = Backbone.Model.extend(Styleable).extend({ if (re === 0) return f; } - if (this.get('state') !== st) return f; - - if (this.get('mediaText') !== wd) return f; - - if (this.get('selectorsAdd') !== selectorsAdd) return f; + if ( + this.get('state') !== st || + this.get('mediaText') !== wd || + this.get('selectorsAdd') !== selectorsAdd || + this.get('atRuleType') !== atRuleType + ) { + return f; + } return true; } diff --git a/src/css_composer/view/CssGroupRuleView.js b/src/css_composer/view/CssGroupRuleView.js new file mode 100644 index 000000000..6450bde4c --- /dev/null +++ b/src/css_composer/view/CssGroupRuleView.js @@ -0,0 +1,12 @@ +module.exports = require('./CssRuleView').extend({ + _createElement: function(tagName) { + return document.createTextNode(''); + }, + + render() { + const model = this.model; + const important = model.get('important'); + this.el.textContent = model.getDeclaration({ important }); + return this; + } +}); diff --git a/src/css_composer/view/CssRulesView.js b/src/css_composer/view/CssRulesView.js index 0c2e132f7..03b38d74c 100644 --- a/src/css_composer/view/CssRulesView.js +++ b/src/css_composer/view/CssRulesView.js @@ -1,9 +1,10 @@ -var Backbone = require('backbone'); -var CssRuleView = require('./CssRuleView'); +const CssRuleView = require('./CssRuleView'); +const CssGroupRuleView = require('./CssGroupRuleView'); -module.exports = Backbone.View.extend({ +module.exports = require('backbone').View.extend({ initialize(o) { const config = o.config || {}; + this.atRules = {}; this.config = config; this.em = config.em; this.pfx = config.stylePrefix || ''; @@ -32,20 +33,44 @@ module.exports = Backbone.View.extend({ addToCollection(model, fragmentEl) { var fragment = fragmentEl || null; var viewObject = CssRuleView; + var config = this.config; + let rendered, view; + const opts = { model, config }; - var view = new viewObject({ - model, - config: this.config - }); - var rendered = view.render().el; + // I have to render keyframes of the same name together + // Unfortunately at the moment I didn't find the way of appending them + // if not staticly, via appendData + if (model.get('atRuleType') == 'keyframes') { + const atRule = model.getAtRule(); + let atRuleEl = this.atRules[atRule]; - if (fragment) fragment.appendChild(rendered); - else this.$el.append(rendered); + if (!atRuleEl) { + const styleEl = document.createElement('style'); + atRuleEl = document.createTextNode(''); + styleEl.appendChild(document.createTextNode(`${atRule}{`)); + styleEl.appendChild(atRuleEl); + styleEl.appendChild(document.createTextNode(`}`)); + this.atRules[atRule] = atRuleEl; + rendered = styleEl; + } + + view = new CssGroupRuleView(opts); + atRuleEl.appendData(view.render().el.textContent); + } else { + view = new CssRuleView(opts); + rendered = view.render().el; + } + + if (rendered) { + if (fragment) fragment.appendChild(rendered); + else this.$el.append(rendered); + } return rendered; }, render() { + this.atRules = {}; const $el = this.$el; const frag = document.createDocumentFragment(); $el.empty(); diff --git a/src/dom_components/view/ComponentTextNodeView.js b/src/dom_components/view/ComponentTextNodeView.js index 5510a2d8c..fb190d371 100644 --- a/src/dom_components/view/ComponentTextNodeView.js +++ b/src/dom_components/view/ComponentTextNodeView.js @@ -1,3 +1 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.View.extend({}); +module.exports = require('backbone').View.extend({}); diff --git a/src/parser/model/ParserCss.js b/src/parser/model/ParserCss.js index 7746a49eb..55360ec3f 100644 --- a/src/parser/model/ParserCss.js +++ b/src/parser/model/ParserCss.js @@ -1,3 +1,21 @@ +import { keys } from 'underscore'; + +// At-rules +// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants +const atRules = { + 4: 'media', + 5: 'font-face', + 6: 'page', + 7: 'keyframes', + 11: 'counter-style', + 12: 'supports', + 13: 'document', + 14: 'font-feature-values', + 15: 'viewport' +}; +const atRuleKeys = keys(atRules); +const singleAtRules = ['5', '6', '11', '15']; + module.exports = config => ({ /** * Parse selector string to array. @@ -14,7 +32,7 @@ module.exports = config => ({ * //add: ['.test2 .test3'] * //} */ - parseSelector(str) { + parseSelector(str = '') { var add = []; var result = []; var sels = str.split(','); @@ -35,55 +53,89 @@ module.exports = config => ({ }; }, + /** + * Parse style declarations of the node + * @param {CSSRule} node + * @return {Object} + */ + parseStyle(node) { + const stl = node.style; + const style = {}; + + for (var i = 0, len = stl.length; i < len; i++) { + const propName = stl[i]; + const propValue = stl.getPropertyValue(propName); + const important = stl.getPropertyPriority(propName); + style[propName] = `${propValue}${important ? ` !${important}` : ''}`; + } + + return style; + }, + + /** + * Get the condition when possible + * @param {CSSRule} node + * @return {string} + */ + parseCondition(node) { + const condition = + node.conditionText || + (node.media && node.media.mediaText) || + node.name || + node.selectorText || + ''; + return condition.trim(); + }, + /** * Fetch data from node - * @param {StyleSheet|CSSMediaRule} el + * @param {StyleSheet|CSSRule} el * @return {Array} */ parseNode(el) { var result = []; - var nodes = el.cssRules; + var nodes = el.cssRules || []; for (var i = 0, len = nodes.length; i < len; i++) { - var node = nodes[i]; - var sels = node.selectorText; - var selsAdd = []; - - // It's a CSSMediaRule - if (node.cssRules) { + const node = nodes[i]; + const type = node.type.toString(); + let singleAtRule = 0; + let atRuleType = ''; + let condition = ''; + // keyText is for CSSKeyframeRule + let sels = node.selectorText || node.keyText; + const isSingleAtRule = singleAtRules.indexOf(type) >= 0; + + // Check if the node is an at-rule + if (isSingleAtRule) { + singleAtRule = 1; + atRuleType = atRules[type]; + condition = this.parseCondition(node); + } else if (atRuleKeys.indexOf(type) >= 0) { var subRules = this.parseNode(node); - var mediaText = node.media.mediaText; + condition = this.parseCondition(node); for (var s = 0, lens = subRules.length; s < lens; s++) { var subRule = subRules[s]; - subRule.mediaText = mediaText ? mediaText.trim() : ''; + condition && (subRule.mediaText = condition); + subRule.atRuleType = atRules[type]; } - result = result.concat(subRules); } - if (!sels) continue; - - var selsParsed = this.parseSelector(sels); + if (!sels && !isSingleAtRule) continue; + const style = this.parseStyle(node); + const selsParsed = this.parseSelector(sels); + const selsAdd = selsParsed.add; sels = selsParsed.result; - selsAdd = selsParsed.add; - // Create style object from the big one - var stl = node.style; - var style = {}; - - for (var j = 0, len2 = stl.length; j < len2; j++) { - const propName = stl[j]; - const propValue = stl.getPropertyValue(propName); - const important = stl.getPropertyPriority(propName); - style[propName] = `${propValue}${important ? ` !${important}` : ''}`; - } - - var lastRule = ''; + let lastRule; // For each group of selectors for (var k = 0, len3 = sels.length; k < len3; k++) { var selArr = sels[k]; var model = {}; + singleAtRule && (model.singleAtRule = singleAtRule); + atRuleType && (model.atRuleType = atRuleType); //Isolate state from selector var stateArr = selArr[selArr.length - 1].split(/:(.+)/); @@ -106,13 +158,18 @@ module.exports = config => ({ if (lastRule) { lastRule.selectorsAdd = selsAddStr; } else { - result.push({ + const model = { selectors: [], selectorsAdd: selsAddStr, style - }); + }; + singleAtRule && (model.singleAtRule = singleAtRule); + atRuleType && (model.atRuleType = atRuleType); + condition && (model.mediaText = condition); + result.push(model); } } + // console.log('LAST PUSH', result[result.length - 1]); } return result; @@ -124,22 +181,15 @@ module.exports = config => ({ * @return {Object|Array} */ parse(str) { - var el = document.createElement('style'); - /* - el.innerHTML = ".cssClass {border: 2px solid black; background-color: blue;} " + - ".red, .red2 {color:red; padding:5px} .test1.red {color:black} .red:hover{color: blue} " + - "@media screen and (min-width: 480px){ .red{color:white} }"; - */ + const el = document.createElement('style'); el.innerHTML = str; - // There is no .sheet without adding it to the + // There is no .sheet before adding it to the document.head.appendChild(el); - var sheet = el.sheet; + const sheet = el.sheet; document.head.removeChild(el); - var result = this.parseNode(sheet); + const result = this.parseNode(sheet); - if (result.length == 1) result = result[0]; - - return result; + return result.length == 1 ? result[0] : result; } }); diff --git a/test/specs/css_composer/e2e/CssComposer.js b/test/specs/css_composer/e2e/CssComposer.js index ed7675446..07cf70471 100644 --- a/test/specs/css_composer/e2e/CssComposer.js +++ b/test/specs/css_composer/e2e/CssComposer.js @@ -140,6 +140,8 @@ module.exports = { protected: false } ], + atRuleType: '', + singleAtRule: 0, important: 0, selectorsAdd: '', state: '', @@ -178,6 +180,8 @@ module.exports = { rule1Out = JSON.parse(JSON.stringify(rule1Out)); rule2Out = JSON.parse(JSON.stringify(rule2Out)); var rule1Result = { + atRuleType: '', + singleAtRule: 0, important: 0, mediaText: '', selectors: [], @@ -190,6 +194,8 @@ module.exports = { } }; var rule2Result = { + atRuleType: '', + singleAtRule: 0, important: 0, mediaText: '', selectors: [], diff --git a/test/specs/css_composer/model/CssModels.js b/test/specs/css_composer/model/CssModels.js index 44ea7753b..8817413c6 100644 --- a/test/specs/css_composer/model/CssModels.js +++ b/test/specs/css_composer/model/CssModels.js @@ -74,11 +74,38 @@ module.exports = { it('toCSS wraps correctly inside media rule', () => { const media = '(max-width: 768px)'; + obj.set('atRuleType', 'media'); obj.set('mediaText', media); obj.get('selectors').add({ name: 'test1' }); obj.setStyle({ color: 'red' }); expect(obj.toCSS()).toEqual(`@media ${media}{.test1{color:red;}}`); }); + + it('toCSS with a generic at-rule', () => { + obj.set('atRuleType', 'supports'); + obj.get('selectors').add({ name: 'test1' }); + obj.setStyle({ 'font-family': 'Open Sans' }); + expect(obj.toCSS()).toEqual( + `@supports{.test1{font-family:Open Sans;}}` + ); + }); + + it('toCSS with a generic single at-rule', () => { + obj.set('atRuleType', 'font-face'); + obj.set('singleAtRule', 1); + obj.setStyle({ 'font-family': 'Sans' }); + expect(obj.toCSS()).toEqual(`@font-face{font-family:Sans;}`); + }); + + it('toCSS with a generic at-rule and condition', () => { + obj.set('atRuleType', 'font-face'); + obj.set('mediaText', 'some-condition'); + obj.get('selectors').add({ name: 'test1' }); + obj.setStyle({ 'font-family': 'Open Sans' }); + expect(obj.toCSS()).toEqual( + `@font-face some-condition{.test1{font-family:Open Sans;}}` + ); + }); }); describe('CssRules', () => { diff --git a/test/specs/parser/model/ParserCss.js b/test/specs/parser/model/ParserCss.js index f0e439580..6b126f8b8 100644 --- a/test/specs/parser/model/ParserCss.js +++ b/test/specs/parser/model/ParserCss.js @@ -133,6 +133,7 @@ module.exports = { var str = '@media only screen and (max-width: 992px){ .test1.test2:hover{ color:red }}'; var result = { + atRuleType: 'media', selectors: ['test1', 'test2'], style: { color: 'red' }, state: 'hover', @@ -145,6 +146,7 @@ module.exports = { it('Parse rule inside media query', () => { var str = '@media (max-width: 992px){ .test1.test2:hover{ color:red }}'; var result = { + atRuleType: 'media', selectors: ['test1', 'test2'], style: { color: 'red' }, state: 'hover', @@ -167,11 +169,13 @@ module.exports = { selectors: ['test1', 'test2'], style: { color: 'red' }, state: 'hover', + atRuleType: 'media', mediaText: '(max-width: 992px)' }, { selectors: ['test2'], style: { color: 'blue' }, + atRuleType: 'media', mediaText: '(max-width: 992px)' } ]; @@ -234,6 +238,43 @@ module.exports = { }; expect(obj.parse(str)).toEqual(result); }); + + // Can't test keyframes https://github.com/NV/CSSOM/issues/95 + it.skip('Parse rule with a keyframes at-rule', () => { + var str = `@keyframes { + from {opacity: 0;} + to {opacity: 1;} + }`; + var result = [ + { + selectors: [], + atRuleType: 'keyframes', + selectorsAdd: 'from', + style: { opacity: '0' } + }, + { + selectors: [], + atRuleType: 'keyframes', + selectorsAdd: 'to', + style: { opacity: '1' } + } + ]; + expect(obj.parse(str)).toEqual(result); + }); + + it('Parse rule with font-face at-rule', () => { + var str = `@font-face { + font-family: "Open Sans"; + }`; + var result = { + selectors: [], + selectorsAdd: '', + atRuleType: 'font-face', + singleAtRule: 1, + style: { 'font-family': '"Open Sans"' } + }; + expect(obj.parse(str)).toEqual(result); + }); }); } };