Browse Source

Add support for keyframes and font-face at-rules. Fixes #276

pull/803/head
Artur Arseniev 9 years ago
parent
commit
571c69cbf7
  1. 3
      src/canvas/view/CanvasView.js
  2. 31
      src/code_manager/model/CssGenerator.js
  3. 69
      src/css_composer/model/CssRule.js
  4. 12
      src/css_composer/view/CssGroupRuleView.js
  5. 45
      src/css_composer/view/CssRulesView.js
  6. 4
      src/dom_components/view/ComponentTextNodeView.js
  7. 134
      src/parser/model/ParserCss.js
  8. 6
      test/specs/css_composer/e2e/CssComposer.js
  9. 27
      test/specs/css_composer/model/CssModels.js
  10. 41
      test/specs/parser/model/ParserCss.js

3
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 || '';

31
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);
}

69
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;
}

12
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;
}
});

45
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();

4
src/dom_components/view/ComponentTextNodeView.js

@ -1,3 +1 @@
var Backbone = require('backbone');
module.exports = Backbone.View.extend({});
module.exports = require('backbone').View.extend({});

134
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<Object>}
*/
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<Object>}
*/
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 <head>
// There is no .sheet before adding it to the <head>
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;
}
});

6
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: [],

27
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', () => {

41
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);
});
});
}
};

Loading…
Cancel
Save