Browse Source

Merge pull request #803 from artf/dev

Merge from dev
pull/883/head
Artur Arseniev 9 years ago
committed by GitHub
parent
commit
3932bad352
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      .github/ISSUE_TEMPLATE.md
  2. 46
      CODE_OF_CONDUCT.md
  3. 52
      CONTRIBUTING.md
  4. 2
      LICENSE
  5. 1
      README.md
  6. 2
      dist/css/grapes.min.css
  7. 915
      dist/grapes.js
  8. 6
      dist/grapes.min.js
  9. 2
      package-lock.json
  10. 2
      package.json
  11. 2
      src/asset_manager/view/AssetImageView.js
  12. 3
      src/canvas/view/CanvasView.js
  13. 45
      src/code_manager/model/CssGenerator.js
  14. 1
      src/css_composer/index.js
  15. 69
      src/css_composer/model/CssRule.js
  16. 4
      src/css_composer/model/CssRules.js
  17. 12
      src/css_composer/view/CssGroupRuleView.js
  18. 45
      src/css_composer/view/CssRulesView.js
  19. 4
      src/dom_components/index.js
  20. 50
      src/dom_components/model/Component.js
  21. 5
      src/dom_components/model/ComponentImage.js
  22. 2
      src/dom_components/model/ComponentVideo.js
  23. 44
      src/dom_components/model/Components.js
  24. 4
      src/dom_components/view/ComponentTextNodeView.js
  25. 5
      src/dom_components/view/ComponentView.js
  26. 18
      src/editor/config/config.js
  27. 1
      src/editor/model/Editor.js
  28. 43
      src/index.js
  29. 134
      src/parser/model/ParserCss.js
  30. 3
      src/storage_manager/model/RemoteStorage.js
  31. 23
      src/style_manager/index.js
  32. 3
      src/style_manager/view/PropertyView.js
  33. 91
      src/style_manager/view/SectorsView.js
  34. 2
      src/styles/scss/_gjs_canvas.scss
  35. 22
      src/undo_manager/index.js
  36. 3
      src/utils/Droppable.js
  37. 12
      src/utils/Sorter.js
  38. 52
      src/utils/extender.js
  39. 6
      test/specs/css_composer/e2e/CssComposer.js
  40. 27
      test/specs/css_composer/model/CssModels.js
  41. 41
      test/specs/parser/model/ParserCss.js
  42. 9
      test/specs/selector_manager/e2e/ClassManager.js

26
.github/ISSUE_TEMPLATE.md

@ -0,0 +1,26 @@
## WARNING
READ and FOLLOW next 5 steps, then REMOVE them before posting the issue
1. Follow CONTRIBUTING Guidelines https://github.com/artf/grapesjs/blob/master/CONTRIBUTING.md
2. Use the GitHub Issues EXCLUSIVELY for BUGS, FEATURE REQUESTS or QUESTIONS. Prefix the title of the issue with its context, eg. `[Bug]: ....`
3. Do a quick SEARCH first, to see if someone else didn't open the same issue
4. DON'T ASK to create examples/code for you, read DOCS and APIs first, then you can post what you have tried (we'd like to see your code) and what you're unable to achieve
5. All relative statements/questions have to be filled/answered, otherwise, the issue might be CLOSED
## You're submitting a BUG
1. Are you using the latest release (older versions are NOT supported)?
1. If you're not sure, type `grapesjs.version` in console and press ENTER
1. Are you facing the bug with your local copy of GrapesJS or with the current demo?
1. If a local copy
1. Indicate all informations about your OS, browser and GrapesJS version.
1. Are you able to reproduce the bug from the demo?
1. What is the expected behavior?
1. What happens instead?
1. If you're able to reproduce the bug indicate all the necessary steps
1. Attach screenshots (using KAP/LICEcap), screencasts or live demo
1. JSFiddle Starter template https://jsfiddle.net/szLp8h4n
1. CodeSandbox Starter template https://codesandbox.io/s/1r0w2pk1vl
## You're submitting a FEATURE REQUEST
1. Be sure to work on the latest version, as the feature might be already there
1. Keep in mind that the feature should be considered valid to use for everyone, not only for your case

46
CODE_OF_CONDUCT.md

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at artur.catch@hotmail.it. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

52
CONTRIBUTING.md

@ -0,0 +1,52 @@
# Contribute
## Introduction
First of all, thank you for considering contributing to GrapesJS!
We welcome any type of contribution, not only code. Like for example:
- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)
- **Marketing**: writing blog posts, howto's, tutorials, etc.
- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, etc.
- **Money**: We welcome financial contributions in full transparency on our [Open Collective].
## Your First Contribution
Working on your first Pull Request? You can learn how from this **free** series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
## Submitting code
Any code change should be submitted as a pull request. Before start working on something make always a search in opened issues and pull requests, this might help you to avoid waisting time.
A pull request could be a bug fix, new feature and much more, but in all cases, **open a new issue** and talk about what you want to do. Often happens to work on something already fixed (ready to release) or in progress.
The title should be brief but comprehensive, the description contains a link to the opened issue and the proposed solution. The pull request should contain tests whenever possible. Keep in mind that the bigger is the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
## Styleguide
The code is auto formatted with [prettier](https://github.com/prettier/prettier) on any commit, therefore you can write in any style you prefer
## Expenses
Anyone can file an expense (code, marketing, etc.) via our [Open Collective]. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
Before submitting an expense contact core contributors via the current active chat room ([Discord](https://discord.gg/QAbgGXq)) and explain your intents
## Questions
If you have any questions, create an [issue](https://github.com/artf/grapesjs/issues) (protip: do a quick search first to see if someone else didn't ask the same question before!).
## Credits
Thank you to all the people who have already contributed to GrapesJS!
<a href="/artf/grapesjs/graphs/contributors"><img src="https://opencollective.com/grapesjs/contributors.svg?width=890" /></a>
[Open Collective]: <https://opencollective.com/grapesjs>

2
LICENSE

@ -1,4 +1,4 @@
Copyright (c) 2017, Artur Arseniev Copyright (c) 2017-current, Artur Arseniev
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

1
README.md

@ -36,6 +36,7 @@ Newsletter Demo - http://grapesjs.com/demo-newsletter-editor.html
* [Plugins](#plugins) * [Plugins](#plugins)
* [Support](#support) * [Support](#support)
* [Changelog](https://github.com/artf/grapesjs/releases) * [Changelog](https://github.com/artf/grapesjs/releases)
* [Contributing](https://github.com/artf/grapesjs/blob/master/CONTRIBUTING.md)
* [License](#license) * [License](#license)

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

915
dist/grapes.js

File diff suppressed because it is too large

6
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
package-lock.json

@ -1,6 +1,6 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"version": "0.13.5", "version": "0.13.8",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

2
package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.13.5", "version": "0.13.8",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",

2
src/asset_manager/view/AssetImageView.js

@ -9,7 +9,7 @@ module.exports = require('./AssetView').extend({
const pfx = this.pfx; const pfx = this.pfx;
const src = this.model.get('src'); const src = this.model.get('src');
return ` return `
<div class="${pfx}preview" style="background-image: url(${src});"></div> <div class="${pfx}preview" style="background-image: url('${src}');"></div>
<div class="${pfx}preview-bg ${this.ppfx}checker-bg"></div> <div class="${pfx}preview-bg ${this.ppfx}checker-bg"></div>
`; `;
}, },

3
src/canvas/view/CanvasView.js

@ -1,10 +1,11 @@
import { on, off } from 'utils/mixins';
const FrameView = require('./FrameView'); const FrameView = require('./FrameView');
const $ = Backbone.$; const $ = Backbone.$;
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
initialize(o) { initialize(o) {
_.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff'); _.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff');
window.onscroll = this.clearOff; on(window, 'scroll resize', this.clearOff);
this.config = o.config || {}; this.config = o.config || {};
this.em = this.config.em || {}; this.em = this.config.em || {};
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';

45
src/code_manager/model/CssGenerator.js

@ -35,42 +35,45 @@ module.exports = require('backbone').Model.extend({
build(model, opts = {}) { build(model, opts = {}) {
const cssc = opts.cssc; const cssc = opts.cssc;
this.em = opts.em || ''; const em = opts.em || '';
this.em = em;
this.compCls = []; this.compCls = [];
this.ids = []; this.ids = [];
var code = this.buildFromModel(model, opts); var code = this.buildFromModel(model, opts);
if (cssc) { if (cssc) {
const rules = cssc.getAll(); const rules = cssc.getAll();
const mediaRules = {}; const atRules = {};
const dump = [];
rules.each(rule => { rules.each(rule => {
const media = rule.get('mediaText'); const atRule = rule.getAtRule();
// If media is setted, I'll render it later if (atRule) {
if (media) { const mRules = atRules[atRule];
const mRules = mediaRules[media];
if (mRules) { if (mRules) {
mRules.push(rule); mRules.push(rule);
} else { } else {
mediaRules[media] = [rule]; atRules[atRule] = [rule];
} }
return; return;
} }
code += this.buildFromRule(rule); code += this.buildFromRule(rule, dump);
}); });
// Get media rules // Get at-rules
for (let media in mediaRules) { for (let atRule in atRules) {
let rulesStr = ''; let rulesStr = '';
const mRules = mediaRules[media]; const mRules = atRules[atRule];
mRules.forEach(rule => (rulesStr += this.buildFromRule(rule))); mRules.forEach(rule => (rulesStr += this.buildFromRule(rule, dump)));
if (rulesStr) { if (rulesStr) {
code += `@media ${media}{${rulesStr}}`; code += `${atRule}{${rulesStr}}`;
} }
} }
em && em.getConfig('clearStyles') && rules.remove(dump);
} }
return code; return code;
@ -81,10 +84,11 @@ module.exports = require('backbone').Model.extend({
* @param {Model} rule * @param {Model} rule
* @return {string} CSS string * @return {string} CSS string
*/ */
buildFromRule(rule) { buildFromRule(rule, dump) {
let result = ''; let result = '';
const selectorStr = rule.selectorsToString();
const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 }); const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 });
const selectorsAdd = rule.get('selectorsAdd');
const singleAtRule = rule.get('singleAtRule');
let found; let found;
// This will not render a rule if there is no its component // This will not render a rule if there is no its component
@ -95,12 +99,11 @@ module.exports = require('backbone').Model.extend({
} }
}); });
if ((selectorStrNoAdd && found) || rule.get('selectorsAdd')) { if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule) {
const style = rule.styleToString(); const block = rule.getDeclaration();
block && (result += block);
if (style) { } else {
result += `${selectorStr}{${style}}`; dump.push(rule);
}
} }
return result; return result;

1
src/css_composer/index.js

@ -68,7 +68,6 @@ module.exports = () => {
var elStyle = (c.em && c.em.config.style) || ''; var elStyle = (c.em && c.em.config.style) || '';
c.rules = elStyle || c.rules; c.rules = elStyle || c.rules;
c.sm = c.em;
em = c.em; em = c.em;
rules = new CssRules([], c); rules = new CssRules([], c);
rulesView = new CssRulesView({ rulesView = new CssRulesView({

69
src/css_composer/model/CssRule.js

@ -23,6 +23,13 @@ module.exports = Backbone.Model.extend(Styleable).extend({
// Indicates if the rule is stylable // Indicates if the rule is stylable
stylable: true, 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 // If true, sets '!important' on all properties
// You can use an array to specify properties to set important // You can use an array to specify properties to set important
// Used in view // Used in view
@ -47,6 +54,19 @@ module.exports = Backbone.Model.extend(Styleable).extend({
this.set('selectors', new Selectors(selectors)); 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 selectors fo the rule as a string
* @return {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 * @param {Object} [opts={}] Options
* @return {string} * @return {string}
*/ */
toCSS(opts = {}) { getDeclaration(opts = {}) {
let result = ''; let result = '';
const media = this.get('mediaText');
const style = this.styleToString(opts);
const selectors = this.selectorsToString(); const selectors = this.selectorsToString();
const style = this.styleToString(opts);
const singleAtRule = this.get('singleAtRule');
if (selectors && style) { if ((selectors || singleAtRule) && style) {
result = `${selectors}{${style}}`; result = singleAtRule ? style : `${selectors}{${style}}`;
} }
if (media && result) { return result;
result = `@media ${media}{${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; return result;
@ -93,11 +127,11 @@ module.exports = Backbone.Model.extend(Styleable).extend({
* @return {Boolean} * @return {Boolean}
* @private * @private
*/ */
compare(selectors, state, width, ruleProps) { compare(selectors, state, width, ruleProps = {}) {
var otherRule = ruleProps || {};
var st = state || ''; var st = state || '';
var wd = width || ''; var wd = width || '';
var selectorsAdd = otherRule.selectorsAdd || ''; var selectorsAdd = ruleProps.selectorsAdd || '';
var atRuleType = ruleProps.atRuleType || '';
var cId = 'cid'; var cId = 'cid';
//var a1 = _.pluck(selectors.models || selectors, cId); //var a1 = _.pluck(selectors.models || selectors, cId);
//var a2 = _.pluck(this.get('selectors').models, 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 (re === 0) return f;
} }
if (this.get('state') !== st) return f; if (
this.get('state') !== st ||
if (this.get('mediaText') !== wd) return f; this.get('mediaText') !== wd ||
this.get('selectorsAdd') !== selectorsAdd ||
if (this.get('selectorsAdd') !== selectorsAdd) return f; this.get('atRuleType') !== atRuleType
) {
return f;
}
return true; return true;
} }

4
src/css_composer/model/CssRules.js

@ -4,13 +4,13 @@ var CssRule = require('./CssRule');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
initialize(models, opt) { initialize(models, opt) {
// Inject editor // Inject editor
if (opt && opt.sm) this.editor = opt.sm; if (opt && opt.em) this.editor = opt.em;
// Not used // Not used
this.model = (attrs, options) => { this.model = (attrs, options) => {
var model; var model;
if (!options.sm && opt && opt.sm) options.sm = opt.sm; if (!options.em && opt && opt.em) options.em = opt.em;
switch (1) { switch (1) {
default: default:

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'); const CssRuleView = require('./CssRuleView');
var CssRuleView = require('./CssRuleView'); const CssGroupRuleView = require('./CssGroupRuleView');
module.exports = Backbone.View.extend({ module.exports = require('backbone').View.extend({
initialize(o) { initialize(o) {
const config = o.config || {}; const config = o.config || {};
this.atRules = {};
this.config = config; this.config = config;
this.em = config.em; this.em = config.em;
this.pfx = config.stylePrefix || ''; this.pfx = config.stylePrefix || '';
@ -32,20 +33,44 @@ module.exports = Backbone.View.extend({
addToCollection(model, fragmentEl) { addToCollection(model, fragmentEl) {
var fragment = fragmentEl || null; var fragment = fragmentEl || null;
var viewObject = CssRuleView; var viewObject = CssRuleView;
var config = this.config;
let rendered, view;
const opts = { model, config };
var view = new viewObject({ // I have to render keyframes of the same name together
model, // Unfortunately at the moment I didn't find the way of appending them
config: this.config // if not staticly, via appendData
}); if (model.get('atRuleType') == 'keyframes') {
var rendered = view.render().el; const atRule = model.getAtRule();
let atRuleEl = this.atRules[atRule];
if (fragment) fragment.appendChild(rendered); if (!atRuleEl) {
else this.$el.append(rendered); 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; return rendered;
}, },
render() { render() {
this.atRules = {};
const $el = this.$el; const $el = this.$el;
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
$el.empty(); $el.empty();

4
src/dom_components/index.js

@ -211,7 +211,7 @@ module.exports = () => {
} }
component = new Component(wrapper, { component = new Component(wrapper, {
sm: em, em,
config: c, config: c,
componentTypes componentTypes
}); });
@ -230,7 +230,7 @@ module.exports = () => {
* @private * @private
*/ */
onLoad() { onLoad() {
this.getComponents().reset(c.components); this.setComponents(c.components);
}, },
/** /**

50
src/dom_components/model/Component.js

@ -141,7 +141,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
}, },
initialize(props = {}, opt = {}) { initialize(props = {}, opt = {}) {
const em = opt.sm || opt.em || ''; const em = opt.em;
// Propagate properties from parent if indicated // Propagate properties from parent if indicated
const parent = this.parent(); const parent = this.parent();
@ -171,12 +171,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
opt.em = em; opt.em = em;
this.opt = opt; this.opt = opt;
this.sm = em;
this.em = em; this.em = em;
this.config = opt.config || {}; this.config = opt.config || {};
this.ccid = Component.createId(this); this.ccid = Component.createId(this);
this.set('attributes', this.get('attributes') || {}); this.set('attributes', this.get('attributes') || {});
this.on('destroy', this.handleRemove); this.on('remove', this.handleRemove);
this.listenTo(this, 'change:script', this.scriptUpdated); this.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:traits', this.traitsUpdated); this.listenTo(this, 'change:traits', this.traitsUpdated);
this.listenTo(this, 'change:tagName', this.tagUpdated); this.listenTo(this, 'change:tagName', this.tagUpdated);
@ -235,6 +234,19 @@ const Component = Backbone.Model.extend(Styleable).extend(
return result; return result;
}, },
/**
* Find closest model by query string
* ATTENTION: this method works only with alredy rendered component
* @param {string} query Query string
* @return {Component}
* @example
* model.closest('div');
*/
closest(query) {
const result = this.view.$el.closest(query);
return result.length && result.data('model');
},
/** /**
* Once the tag is updated I have to remove the node and replace it * Once the tag is updated I have to remove the node and replace it
*/ */
@ -277,6 +289,17 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.set('attributes', attrs); this.set('attributes', attrs);
}, },
/**
* Add attributes to the model
* @param {Object} attrs Key value attributes
* @example
* model.addAttributes({id: 'test'});
*/
addAttributes(attrs) {
const newAttrs = { ...this.getAttributes(), ...attrs };
this.setAttributes(newAttrs);
},
getStyle() { getStyle() {
const em = this.em; const em = this.em;
@ -555,10 +578,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
*/ */
normalizeClasses(arr) { normalizeClasses(arr) {
var res = []; var res = [];
const em = this.em;
if (!this.sm.get) return; if (!em) return;
var clm = this.sm.get('SelectorManager'); var clm = em.get('SelectorManager');
if (!clm) return; if (!clm) return;
arr.forEach(val => { arr.forEach(val => {
@ -577,10 +601,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Override original clone method * Override original clone method
* @private * @private
*/ */
clone(reset) { clone() {
const em = this.em; const em = this.em;
const style = this.getStyle(); const style = this.getStyle();
const attr = { ...this.attributes }; const attr = { ...this.attributes };
const opts = { ...this.opt };
attr.attributes = { ...attr.attributes }; attr.attributes = { ...attr.attributes };
delete attr.attributes.id; delete attr.attributes.id;
attr.components = []; attr.components = [];
@ -588,7 +613,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
attr.traits = []; attr.traits = [];
this.get('components').each((md, i) => { this.get('components').each((md, i) => {
attr.components[i] = md.clone(1); attr.components[i] = md.clone();
}); });
this.get('traits').each((md, i) => { this.get('traits').each((md, i) => {
attr.traits[i] = md.clone(); attr.traits[i] = md.clone();
@ -599,16 +624,13 @@ const Component = Backbone.Model.extend(Styleable).extend(
attr.status = ''; attr.status = '';
attr.view = ''; attr.view = '';
opts.collection = null;
if (reset) {
this.opt.collection = null;
}
if (em && em.getConfig('avoidInlineStyle') && !isEmpty(style)) { if (em && em.getConfig('avoidInlineStyle') && !isEmpty(style)) {
attr.style = style; attr.style = style;
} }
return new this.constructor(attr, this.opt); return new this.constructor(attr, opts);
}, },
/** /**
@ -734,7 +756,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
scr = scrStr.trim(); scr = scrStr.trim();
} }
var config = this.sm.config || {}; var config = this.em.getConfig();
var tagVarStart = escapeRegExp(config.tagVarStart || '{[ '); var tagVarStart = escapeRegExp(config.tagVarStart || '{[ ');
var tagVarEnd = escapeRegExp(config.tagVarEnd || ' ]}'); var tagVarEnd = escapeRegExp(config.tagVarEnd || ' ]}');
var reg = new RegExp(`${tagVarStart}([\\w\\d-]*)${tagVarEnd}`, 'g'); var reg = new RegExp(`${tagVarStart}([\\w\\d-]*)${tagVarEnd}`, 'g');
@ -751,7 +773,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
emitUpdate(property) { emitUpdate(property) {
const em = this.em; const em = this.em;
const event = 'component:update' + (property ? `:${property}` : ''); const event = 'component:update' + (property ? `:${property}` : '');
em && em.trigger(event, this.model); em && em.trigger(event, this);
} }
}, },
{ {

5
src/dom_components/model/ComponentImage.js

@ -26,9 +26,10 @@ module.exports = Component.extend(
initToolbar(...args) { initToolbar(...args) {
Component.prototype.initToolbar.apply(this, args); Component.prototype.initToolbar.apply(this, args);
const em = this.em;
if (this.sm && this.sm.get) { if (em) {
var cmd = this.sm.get('Commands'); var cmd = em.get('Commands');
var cmdName = 'image-editor'; var cmdName = 'image-editor';
// Add Image Editor button only if the default command exists // Add Image Editor button only if the default command exists

2
src/dom_components/model/ComponentVideo.js

@ -125,7 +125,7 @@ module.exports = Component.extend(
this.set('tagName', 'video'); this.set('tagName', 'video');
} }
this.loadTraits(traits); this.loadTraits(traits);
this.sm.trigger('change:selectedComponent'); this.em.trigger('change:selectedComponent');
}, },
// Listen provider change and switch traits, in TraitView listen traits change // Listen provider change and switch traits, in TraitView listen traits change

44
src/dom_components/model/Components.js

@ -3,27 +3,17 @@ import { isEmpty } from 'underscore';
const Backbone = require('backbone'); const Backbone = require('backbone');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
initialize(models, opt) { initialize(models, opt = {}) {
this.on('add', this.onAdd); this.listenTo(this, 'add', this.onAdd);
this.config = opt.config;
this.config = opt && opt.config ? opt.config : null; this.em = opt.em;
// Inject editor
if (opt && (opt.sm || opt.em)) this.editor = opt.sm || opt.em;
this.model = (attrs, options) => { this.model = (attrs, options) => {
var model; var model;
if (!options.sm && opt && opt.sm) options.sm = opt.sm;
if (!options.em && opt && opt.em) options.em = opt.em;
if (opt && opt.config) options.config = opt.config;
if (opt && opt.componentTypes)
options.componentTypes = opt.componentTypes;
var df = opt.componentTypes; var df = opt.componentTypes;
options.em = opt.em;
options.config = opt.config;
options.componentTypes = df;
for (var it = 0; it < df.length; it++) { for (var it = 0; it < df.length; it++) {
var dfId = df[it].id; var dfId = df[it].id;
@ -44,10 +34,10 @@ module.exports = Backbone.Collection.extend({
add(models, opt = {}) { add(models, opt = {}) {
if (typeof models === 'string') { if (typeof models === 'string') {
var parsed = this.editor.get('Parser').parseHtml(models); var parsed = this.em.get('Parser').parseHtml(models);
models = parsed.html; models = parsed.html;
var cssc = this.editor.get('CssComposer'); var cssc = this.em.get('CssComposer');
if (parsed.css && cssc) { if (parsed.css && cssc) {
var { avoidUpdateStyle } = opt; var { avoidUpdateStyle } = opt;
@ -62,8 +52,8 @@ module.exports = Backbone.Collection.extend({
}, },
onAdd(model, c, opts) { onAdd(model, c, opts) {
const em = this.editor; const em = this.em;
const style = model.get('style'); const style = model.getStyle();
const avoidInline = em && em.getConfig('avoidInlineStyle'); const avoidInline = em && em.getConfig('avoidInlineStyle');
if ( if (
@ -71,14 +61,12 @@ module.exports = Backbone.Collection.extend({
!avoidInline && !avoidInline &&
em && em &&
em.get && em.get &&
em.get('Config').forceClass em.getConfig('forceClass')
) { ) {
var cssC = this.editor.get('CssComposer'); const name = model.cid;
var newClass = this.editor.get('SelectorManager').add(model.cid); const rule = em.get('CssComposer').setClassRule(name, style);
model.set({ style: {} }); model.setStyle({});
model.get('classes').add(newClass); model.addClass(name);
var rule = cssC.add(newClass);
rule.set('style', style);
} }
} }
}); });

4
src/dom_components/view/ComponentTextNodeView.js

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

5
src/dom_components/view/ComponentView.js

@ -338,6 +338,9 @@ module.exports = Backbone.View.extend({
this.updateContent(); this.updateContent();
this.renderChildren(); this.renderChildren();
this.updateScript(); this.updateScript();
this.onRender();
return this; return this;
} },
onRender() {}
}); });

18
src/editor/config/config.js

@ -2,9 +2,15 @@ module.exports = {
// Style prefix // Style prefix
stylePrefix: 'gjs-', stylePrefix: 'gjs-',
//TEMP // HTML string or object of components
components: '', components: '',
// CSS string or object of rules
style: '',
// If true, will fetch HTML and CSS from selected container
fromElement: 0,
// Show an alert before unload the page with unsaved changes // Show an alert before unload the page with unsaved changes
noticeOnUnload: true, noticeOnUnload: true,
@ -74,6 +80,16 @@ module.exports = {
// When `avoidInlineStyle` is true all styles are inserted inside the css rule // When `avoidInlineStyle` is true all styles are inserted inside the css rule
avoidInlineStyle: 0, avoidInlineStyle: 0,
// (experimental)
// The structure of components is always on the screen but it's not the same
// for style rules. When you delete a component you might leave a lot of styles
// which will never be used again, therefore they might be removed.
// With this option set to true, styles not used from the CSS generator (so in
// any case where `CssGenerator.build` is used) will be removed automatically.
// But be careful, not always leaving the style not used mean you wouldn't
// use it later, but this option comes really handy when deal with big templates.
clearStyles: 0,
// Dom element // Dom element
el: '', el: '',

1
src/editor/model/Editor.js

@ -50,6 +50,7 @@ module.exports = Backbone.Model.extend({
this.config = c; this.config = c;
this.set('Config', c); this.set('Config', c);
this.set('modules', []); this.set('modules', []);
this.set('toLoad', []);
if (c.el && c.fromElement) this.config.components = c.el.innerHTML; if (c.el && c.fromElement) this.config.components = c.el.innerHTML;

43
src/index.js

@ -1,33 +1,17 @@
import $ from 'cash-dom'; import $ from 'cash-dom';
import { defaults } from 'underscore'; import Editor from './editor';
import PluginManager from './plugin_manager';
import polyfills from 'utils/polyfills'; import polyfills from 'utils/polyfills';
polyfills(); polyfills();
module.exports = (() => { module.exports = (() => {
const Editor = require('editor');
const PluginManager = require('plugin_manager');
const plugins = new PluginManager(); const plugins = new PluginManager();
const editors = []; const editors = [];
const defaultConfig = { const defaultConfig = {
// If true renders editor on init // If true renders editor on init
autorender: 1, autorender: 1,
// Where init the editor
container: '',
// HTML string or object of components
components: '',
// CSS string or object of rules
style: '',
// If true, will fetch HTML and CSS from selected container
fromElement: 0,
// Storage Manager
storageManager: {},
// Array of plugins to init // Array of plugins to init
plugins: [], plugins: [],
@ -49,12 +33,10 @@ module.exports = (() => {
* Initializes an editor based on passed options * Initializes an editor based on passed options
* @param {Object} config Configuration object * @param {Object} config Configuration object
* @param {string|HTMLElement} config.container Selector which indicates where render the editor * @param {string|HTMLElement} config.container Selector which indicates where render the editor
* @param {Object|string} config.components='' HTML string or Component model in JSON format * @param {Boolean} [config.autorender=true] If true, auto-render the content
* @param {Object|string} config.style='' CSS string or CSS model in JSON format
* @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container
* @param {Boolean} [config.undoManager=true] Enable/Disable undo manager
* @param {Array} [config.plugins=[]] Array of plugins to execute on start * @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @return {grapesjs.Editor} GrapesJS editor instance * @param {Object} [config.pluginsOpts={}] Custom options for plugins
* @return {Editor} Editor instance
* @example * @example
* var editor = grapesjs.init({ * var editor = grapesjs.init({
* container: '#myeditor', * container: '#myeditor',
@ -64,14 +46,10 @@ module.exports = (() => {
*/ */
init(config = {}) { init(config = {}) {
const els = config.container; const els = config.container;
if (!els) throw new Error("'container' is required");
if (!els) { config = { ...defaultConfig, ...config };
throw new Error("'container' is required"); const ilEl = els instanceof window.HTMLElement;
} config.el = ilEl ? els : document.querySelector(els);
defaults(config, defaultConfig);
config.el =
els instanceof window.HTMLElement ? els : document.querySelector(els);
const editor = new Editor(config).init(); const editor = new Editor(config).init();
// Load plugins // Load plugins
@ -89,10 +67,9 @@ module.exports = (() => {
// A plugin might have extended/added some custom type so this // A plugin might have extended/added some custom type so this
// is a good point to load stuff like components, css rules, etc. // is a good point to load stuff like components, css rules, etc.
editor.getModel().loadOnStart(); editor.getModel().loadOnStart();
config.autorender && editor.render(); config.autorender && editor.render();
editors.push(editor); editors.push(editor);
return editor; return editor;
} }
}; };

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 => ({ module.exports = config => ({
/** /**
* Parse selector string to array. * Parse selector string to array.
@ -14,7 +32,7 @@ module.exports = config => ({
* //add: ['.test2 .test3'] * //add: ['.test2 .test3']
* //} * //}
*/ */
parseSelector(str) { parseSelector(str = '') {
var add = []; var add = [];
var result = []; var result = [];
var sels = str.split(','); 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 * Fetch data from node
* @param {StyleSheet|CSSMediaRule} el * @param {StyleSheet|CSSRule} el
* @return {Array<Object>} * @return {Array<Object>}
*/ */
parseNode(el) { parseNode(el) {
var result = []; var result = [];
var nodes = el.cssRules; var nodes = el.cssRules || [];
for (var i = 0, len = nodes.length; i < len; i++) { for (var i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i]; const node = nodes[i];
var sels = node.selectorText; const type = node.type.toString();
var selsAdd = []; let singleAtRule = 0;
let atRuleType = '';
// It's a CSSMediaRule let condition = '';
if (node.cssRules) { // 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 subRules = this.parseNode(node);
var mediaText = node.media.mediaText; condition = this.parseCondition(node);
for (var s = 0, lens = subRules.length; s < lens; s++) { for (var s = 0, lens = subRules.length; s < lens; s++) {
var subRule = subRules[s]; var subRule = subRules[s];
subRule.mediaText = mediaText ? mediaText.trim() : ''; condition && (subRule.mediaText = condition);
subRule.atRuleType = atRules[type];
} }
result = result.concat(subRules); result = result.concat(subRules);
} }
if (!sels) continue; if (!sels && !isSingleAtRule) continue;
const style = this.parseStyle(node);
var selsParsed = this.parseSelector(sels); const selsParsed = this.parseSelector(sels);
const selsAdd = selsParsed.add;
sels = selsParsed.result; sels = selsParsed.result;
selsAdd = selsParsed.add;
// Create style object from the big one let lastRule;
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 = '';
// For each group of selectors // For each group of selectors
for (var k = 0, len3 = sels.length; k < len3; k++) { for (var k = 0, len3 = sels.length; k < len3; k++) {
var selArr = sels[k]; var selArr = sels[k];
var model = {}; var model = {};
singleAtRule && (model.singleAtRule = singleAtRule);
atRuleType && (model.atRuleType = atRuleType);
//Isolate state from selector //Isolate state from selector
var stateArr = selArr[selArr.length - 1].split(/:(.+)/); var stateArr = selArr[selArr.length - 1].split(/:(.+)/);
@ -106,13 +158,18 @@ module.exports = config => ({
if (lastRule) { if (lastRule) {
lastRule.selectorsAdd = selsAddStr; lastRule.selectorsAdd = selsAddStr;
} else { } else {
result.push({ const model = {
selectors: [], selectors: [],
selectorsAdd: selsAddStr, selectorsAdd: selsAddStr,
style 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; return result;
@ -124,22 +181,15 @@ module.exports = config => ({
* @return {Object|Array<Object>} * @return {Object|Array<Object>}
*/ */
parse(str) { parse(str) {
var el = document.createElement('style'); const 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} }";
*/
el.innerHTML = str; 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); document.head.appendChild(el);
var sheet = el.sheet; const sheet = el.sheet;
document.head.removeChild(el); document.head.removeChild(el);
var result = this.parseNode(sheet); const result = this.parseNode(sheet);
if (result.length == 1) result = result[0]; return result.length == 1 ? result[0] : result;
return result;
} }
}); });

3
src/storage_manager/model/RemoteStorage.js

@ -55,7 +55,8 @@ module.exports = require('backbone').Model.extend({
const em = this.get('em'); const em = this.get('em');
const complete = this.get('onComplete'); const complete = this.get('onComplete');
const typeJson = this.get('contentTypeJson'); const typeJson = this.get('contentTypeJson');
const res = typeJson && typeof text === 'string' ? JSON.parse(text) : text; const parsable = text && typeof text === 'string';
const res = typeJson && parsable ? JSON.parse(text) : text;
complete && complete(res); complete && complete(res);
clb && clb(res); clb && clb(res);
em && em.trigger('storage:response', res); em && em.trigger('storage:response', res);

23
src/style_manager/index.js

@ -251,20 +251,35 @@ module.exports = () => {
if (em) { if (em) {
const config = em.getConfig(); const config = em.getConfig();
const um = em.get('UndoManager');
const cssC = em.get('CssComposer'); const cssC = em.get('CssComposer');
const state = !config.devicePreviewMode ? model.get('state') : ''; const state = !config.devicePreviewMode ? model.get('state') : '';
const valid = classes.getStyleable(); const valid = classes.getStyleable();
const hasClasses = valid.length; const hasClasses = valid.length;
const opts = { state }; const opts = { state };
let rule;
if (hasClasses) { if (hasClasses) {
const deviceW = em.getCurrentMedia(); const deviceW = em.getCurrentMedia();
const CssRule = cssC.get(valid, state, deviceW); rule = cssC.get(valid, state, deviceW);
if (CssRule) return CssRule;
if (!rule) {
// I stop undo manager here as after adding the CSSRule (generally after
// selecting the component) and calling undo() it will remove the rule from
// the collection, therefore updating it in style manager will not affect it
// #268
um.stop();
rule = cssC.add(valid, state, deviceW);
rule.setStyle(model.getStyle());
model.setStyle({});
um.start();
}
} else if (config.avoidInlineStyle) { } else if (config.avoidInlineStyle) {
const rule = cssC.getIdRule(id, opts); rule = cssC.getIdRule(id, opts);
return rule ? rule : cssC.setIdRule(id, {}, opts); !rule && (rule = cssC.setIdRule(id, {}, opts));
} }
rule && (model = rule);
} }
return model; return model;

3
src/style_manager/view/PropertyView.js

@ -373,9 +373,6 @@ module.exports = Backbone.View.extend({
* @return {Boolean} * @return {Boolean}
*/ */
isTargetStylable(target) { isTargetStylable(target) {
if (this.model.get('id') == 'flex-width') {
//debugger;
}
const trg = target || this.getTarget(); const trg = target || this.getTarget();
const model = this.model; const model = this.model;
const property = model.get('property'); const property = model.get('property');

91
src/style_manager/view/SectorsView.js

@ -19,7 +19,7 @@ module.exports = Backbone.View.extend({
this.propTarget = target; this.propTarget = target;
const coll = this.collection; const coll = this.collection;
const events = const events =
'change:selectedComponent component:update:classes change:device'; 'change:selectedComponent component:update:classes component:update:state change:device';
this.listenTo(coll, 'add', this.addTo); this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.render); this.listenTo(coll, 'reset', this.render);
this.listenTo(this.target, events, this.targetUpdated); this.listenTo(this.target, events, this.targetUpdated);
@ -40,44 +40,33 @@ module.exports = Backbone.View.extend({
* @private * @private
*/ */
targetUpdated() { targetUpdated() {
var em = this.target; const em = this.target;
const pt = this.propTarget;
let model = em.getSelected(); let model = em.getSelected();
const um = em.get('UndoManager'); if (!model) return;
const cc = em.get('CssComposer');
const avoidInline = em.getConfig('avoidInlineStyle');
if (!model) {
return;
}
const id = model.getId();
const config = em.get('Config'); const config = em.get('Config');
var classes = model.get('classes');
var pt = this.propTarget;
const state = !config.devicePreviewMode ? model.get('state') : ''; const state = !config.devicePreviewMode ? model.get('state') : '';
const opts = { state }; const el = model.getEl();
var stateStr = state ? `:${state}` : null;
var view = model.view;
const media = em.getCurrentMedia();
pt.helper = null; pt.helper = null;
if (view) { // Create computed style container
pt.computed = window.getComputedStyle( if (el) {
view.el, const stateStr = state ? `:${state}` : null;
state ? `:${state}` : null pt.computed = window.getComputedStyle(el, stateStr);
);
} }
// Create a new rule for the state as a helper
const appendStateRule = (style = {}) => { const appendStateRule = (style = {}) => {
const sm = em.get('SelectorManager'); const cc = em.get('CssComposer');
const helperClass = sm.add('hc-state'); const helperCls = 'hc-state';
let helperRule = cc.get([helperClass]); const rules = cc.getAll();
let helperRule = cc.getClassRule(helperCls);
if (!helperRule) { if (!helperRule) {
helperRule = cc.add([helperClass]); helperRule = cc.setClassRule(helperCls);
} else { } else {
// I will make it last again, otherwise it could be overridden // I will make it last again, otherwise it could be overridden
const rules = cc.getAll();
rules.remove(helperRule); rules.remove(helperRule);
rules.add(helperRule); rules.add(helperRule);
} }
@ -87,54 +76,8 @@ module.exports = Backbone.View.extend({
pt.helper = helperRule; pt.helper = helperRule;
}; };
// If true the model will be always a rule model = em.get('StyleManager').getModelToStyle(model);
if (avoidInline) { state && appendStateRule(model.getStyle());
const ruleId = cc.getIdRule(id, opts);
if (!ruleId) {
model = cc.setIdRule(id, {}, opts);
} else {
model = ruleId;
}
}
if (classes.length) {
var valid = classes.getStyleable();
var iContainer = cc.get(valid, state, media);
if (!iContainer && valid.length) {
// I stop undo manager here as after adding the CSSRule (generally after
// selecting the component) and calling undo() it will remove the rule from
// the collection, therefore updating it in style manager will not affect it
// #268
um.stop();
iContainer = cc.add(valid, state, media);
iContainer.setStyle(model.getStyle());
model.setStyle({});
um.start();
}
if (!iContainer) {
// In this case it's just a Component without any valid selector
pt.model = model;
pt.trigger('update');
return;
}
// If the state is not empty, there should be a helper rule in play
// The helper rule will get the same style of the iContainer
state && appendStateRule(iContainer.getStyle());
pt.model = iContainer;
pt.trigger('update');
return;
}
if (state) {
const ruleState = cc.getIdRule(id, opts);
state && appendStateRule(ruleState && ruleState.getStyle());
}
pt.model = model; pt.model = model;
pt.trigger('update'); pt.trigger('update');
}, },

2
src/styles/scss/_gjs_canvas.scss

@ -5,7 +5,7 @@ $canvasTop: 40px;
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);
box-sizing: border-box; box-sizing: border-box;
width: (100% - $leftWidth); width: (100% - $leftWidth);
height: calc(100% - $canvasTop); height: calc(100% - #{$canvasTop});
bottom: 0; bottom: 0;
overflow: hidden; overflow: hidden;
z-index: 1; z-index: 1;

22
src/undo_manager/index.js

@ -30,6 +30,28 @@ module.exports = () => {
this.em = em; this.em = em;
um = new UndoManager({ track: true, register: [] }); um = new UndoManager({ track: true, register: [] });
um.changeUndoType('change', { condition: false }); um.changeUndoType('change', { condition: false });
um.changeUndoType('add', {
on(model, collection, options = {}) {
if (options.avoidStore) return;
return {
object: collection,
before: undefined,
after: model,
options: { ...options }
};
}
});
um.changeUndoType('remove', {
on(model, collection, options = {}) {
if (options.avoidStore) return;
return {
object: collection,
before: model,
after: undefined,
options: { ...options }
};
}
});
const customUndoType = { const customUndoType = {
on(object, value, opt = {}) { on(object, value, opt = {}) {
!beforeCache && (beforeCache = object.previousAttributes()); !beforeCache && (beforeCache = object.previousAttributes());

3
src/utils/Droppable.js

@ -119,6 +119,7 @@ export default class Droppable {
const em = this.em; const em = this.em;
const types = dataTransfer.types; const types = dataTransfer.types;
const files = dataTransfer.files; const files = dataTransfer.files;
const dragContent = em.get('dragContent');
let content = dataTransfer.getData('text'); let content = dataTransfer.getData('text');
if (files.length) { if (files.length) {
@ -135,6 +136,8 @@ export default class Droppable {
}); });
} }
} }
} else if (dragContent) {
content = dragContent;
} else if (types.indexOf('text/html') >= 0) { } else if (types.indexOf('text/html') >= 0) {
content = dataTransfer content = dataTransfer
.getData('text/html') .getData('text/html')

12
src/utils/Sorter.js

@ -311,12 +311,14 @@ module.exports = Backbone.View.extend({
if (dropContent && em) { if (dropContent && em) {
if (!dropModel) { if (!dropModel) {
let comps = em.get('DomComponents').getComponents(); let comps = em.get('DomComponents').getComponents();
let tempModel = comps.add(dropContent, { const opts = {
avoidStore: 1,
avoidChildren: 1, avoidChildren: 1,
avoidUpdateStyle: 1, avoidUpdateStyle: 1,
temporary: 1 temporary: 1
}); };
dropModel = comps.remove(tempModel, { temporary: 1 }); let tempModel = comps.add(dropContent, opts);
dropModel = comps.remove(tempModel, opts);
this.dropModel = dropModel instanceof Array ? dropModel[0] : dropModel; this.dropModel = dropModel instanceof Array ? dropModel[0] : dropModel;
} }
return dropModel; return dropModel;
@ -972,7 +974,7 @@ module.exports = Backbone.View.extend({
var opts = { at: index, noIncrement: 1 }; var opts = { at: index, noIncrement: 1 };
if (!dropContent) { if (!dropContent) {
modelTemp = targetCollection.add({}, opts); modelTemp = targetCollection.add({}, { ...opts, avoidStore: 1 });
if (model) { if (model) {
modelToDrop = model.collection.remove(model); modelToDrop = model.collection.remove(model);
@ -986,7 +988,7 @@ module.exports = Backbone.View.extend({
created = targetCollection.add(modelToDrop, opts); created = targetCollection.add(modelToDrop, opts);
if (!dropContent) { if (!dropContent) {
targetCollection.remove(modelTemp); targetCollection.remove(modelTemp, { avoidStore: 1 });
} else { } else {
this.dropContent = null; this.dropContent = null;
} }

52
src/utils/extender.js

@ -1,3 +1,5 @@
import { isObject } from 'underscore';
module.exports = ({ $, Backbone }) => { module.exports = ({ $, Backbone }) => {
if (Backbone) { if (Backbone) {
const ViewProt = Backbone.View.prototype; const ViewProt = Backbone.View.prototype;
@ -60,46 +62,6 @@ module.exports = ({ $, Backbone }) => {
? [namespaceArray[0], namespaceArray.slice(1)] ? [namespaceArray[0], namespaceArray.slice(1)]
: [null, namespaceArray]; : [null, namespaceArray];
}; };
/*
const CashEvent = function(node, eventName, namespaces, delegate, originalCallback, runOnce) {
const eventCache = getData(node,'_cashEvents') || setData(node, '_cashEvents', {});
const remove = function(c, namespace){
if ( c && originalCallback !== c ) { return; }
if ( namespace && this.namespaces.indexOf(namespace) < 0 ) { return; }
node.removeEventListener(eventName, callback);
};
const callback = function(e) {
var t = this;
if (delegate) {
t = e.target;
while (t && !matches(t, delegate)) {
if (t === this) {
return (t = false);
}
t = t.parentNode;
}
}
if (t) {
originalCallback.call(t, e, e.data);
if ( runOnce ) { remove(); }
}
};
this.remove = remove;
this.namespaces = namespaces;
node.addEventListener(eventName, callback);
eventCache[eventName] = eventCache[eventName] || [];
eventCache[eventName].push(this);
return this;
}
*/
const on = $.prototype.on; const on = $.prototype.on;
const off = $.prototype.off; const off = $.prototype.off;
@ -226,7 +188,15 @@ module.exports = ({ $, Backbone }) => {
}); });
fn.unbind = function(ev, h) { fn.unbind = function(ev, h) {
return this.off(ev, h); if (isObject(ev)) {
for (let name in ev) {
ev.hasOwnProperty(name) && this.off(name, ev[name]);
}
return this;
} else {
return this.off(ev, h);
}
}; };
fn.click = function(h) { fn.click = function(h) {

6
test/specs/css_composer/e2e/CssComposer.js

@ -140,6 +140,8 @@ module.exports = {
protected: false protected: false
} }
], ],
atRuleType: '',
singleAtRule: 0,
important: 0, important: 0,
selectorsAdd: '', selectorsAdd: '',
state: '', state: '',
@ -178,6 +180,8 @@ module.exports = {
rule1Out = JSON.parse(JSON.stringify(rule1Out)); rule1Out = JSON.parse(JSON.stringify(rule1Out));
rule2Out = JSON.parse(JSON.stringify(rule2Out)); rule2Out = JSON.parse(JSON.stringify(rule2Out));
var rule1Result = { var rule1Result = {
atRuleType: '',
singleAtRule: 0,
important: 0, important: 0,
mediaText: '', mediaText: '',
selectors: [], selectors: [],
@ -190,6 +194,8 @@ module.exports = {
} }
}; };
var rule2Result = { var rule2Result = {
atRuleType: '',
singleAtRule: 0,
important: 0, important: 0,
mediaText: '', mediaText: '',
selectors: [], selectors: [],

27
test/specs/css_composer/model/CssModels.js

@ -74,11 +74,38 @@ module.exports = {
it('toCSS wraps correctly inside media rule', () => { it('toCSS wraps correctly inside media rule', () => {
const media = '(max-width: 768px)'; const media = '(max-width: 768px)';
obj.set('atRuleType', 'media');
obj.set('mediaText', media); obj.set('mediaText', media);
obj.get('selectors').add({ name: 'test1' }); obj.get('selectors').add({ name: 'test1' });
obj.setStyle({ color: 'red' }); obj.setStyle({ color: 'red' });
expect(obj.toCSS()).toEqual(`@media ${media}{.test1{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', () => { describe('CssRules', () => {

41
test/specs/parser/model/ParserCss.js

@ -133,6 +133,7 @@ module.exports = {
var str = var str =
'@media only screen and (max-width: 992px){ .test1.test2:hover{ color:red }}'; '@media only screen and (max-width: 992px){ .test1.test2:hover{ color:red }}';
var result = { var result = {
atRuleType: 'media',
selectors: ['test1', 'test2'], selectors: ['test1', 'test2'],
style: { color: 'red' }, style: { color: 'red' },
state: 'hover', state: 'hover',
@ -145,6 +146,7 @@ module.exports = {
it('Parse rule inside media query', () => { it('Parse rule inside media query', () => {
var str = '@media (max-width: 992px){ .test1.test2:hover{ color:red }}'; var str = '@media (max-width: 992px){ .test1.test2:hover{ color:red }}';
var result = { var result = {
atRuleType: 'media',
selectors: ['test1', 'test2'], selectors: ['test1', 'test2'],
style: { color: 'red' }, style: { color: 'red' },
state: 'hover', state: 'hover',
@ -167,11 +169,13 @@ module.exports = {
selectors: ['test1', 'test2'], selectors: ['test1', 'test2'],
style: { color: 'red' }, style: { color: 'red' },
state: 'hover', state: 'hover',
atRuleType: 'media',
mediaText: '(max-width: 992px)' mediaText: '(max-width: 992px)'
}, },
{ {
selectors: ['test2'], selectors: ['test2'],
style: { color: 'blue' }, style: { color: 'blue' },
atRuleType: 'media',
mediaText: '(max-width: 992px)' mediaText: '(max-width: 992px)'
} }
]; ];
@ -234,6 +238,43 @@ module.exports = {
}; };
expect(obj.parse(str)).toEqual(result); 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);
});
}); });
} }
}; };

9
test/specs/selector_manager/e2e/ClassManager.js

@ -35,7 +35,7 @@ module.exports = {
fixtures = document.body.firstChild; fixtures = document.body.firstChild;
gjs = grapesjs.init({ gjs = grapesjs.init({
stylePrefix: '', stylePrefix: '',
storageManager: { autoload: 0, type: 'none' }, storageManager: { autoload: 0, type: 0 },
assetManager: { assetManager: {
storageType: 'none' storageType: 'none'
}, },
@ -45,17 +45,14 @@ module.exports = {
describe('Interaction with Components', () => { describe('Interaction with Components', () => {
beforeEach(function() { beforeEach(function() {
components = gjs.editor components = gjs.getComponents();
.get('DomComponents')
.getWrapper()
.get('components');
tagEl = instClassTagViewer(gjs, fixtures); tagEl = instClassTagViewer(gjs, fixtures);
}); });
it('Assign correctly new class to component', function() { it('Assign correctly new class to component', function() {
var model = components.add({}); var model = components.add({});
expect(model.get('classes').length).toEqual(0); expect(model.get('classes').length).toEqual(0);
gjs.editor.set('selectedComponent', model); gjs.select(model);
tagEl.addNewTag('test'); tagEl.addNewTag('test');
expect(model.get('classes').length).toEqual(1); expect(model.get('classes').length).toEqual(1);
expect( expect(

Loading…
Cancel
Save