Browse Source

Merge dev

pull/4223/head
Artur Arseniev 4 years ago
parent
commit
481a054995
  1. 4
      dist/grapes.min.js
  2. 2
      dist/grapes.min.js.map
  3. 1
      docs/api/storage_manager.md
  4. 10349
      index.d.ts
  5. 3
      src/commands/view/SelectComponent.js
  6. 2
      src/css_composer/model/CssRule.js
  7. 4
      src/css_composer/model/CssRules.js
  8. 14
      src/rich_text_editor/model/RichTextEditor.js
  9. 17
      src/style_manager/index.js
  10. 74
      test/specs/code_manager/model/CodeModels.js
  11. 4
      test/specs/dom_components/index.js
  12. 21
      test/specs/grapesjs/headless.js
  13. 95
      test/specs/grapesjs/index.js

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

1
docs/api/storage_manager.md

@ -44,7 +44,6 @@ storageManager.add(...);
* [setAutosave][4]
* [getStepsBeforeSave][5]
* [setStepsBeforeSave][6]
* [setStepsBeforeSave][6]
* [getStorages][7]
* [getCurrent][8]
* [getCurrentStorage][9]

10349
index.d.ts

File diff suppressed because it is too large

3
src/commands/view/SelectComponent.js

@ -89,7 +89,8 @@ export default {
.getFrames()
.forEach(frame => {
const { view } = frame;
view && trigger(view.getWindow(), view.getBody());
const win = view?.getWindow();
win && trigger(win, view.getBody());
});
},

2
src/css_composer/model/CssRule.js

@ -36,6 +36,8 @@ export default class CssRule extends Model.extend(Styleable) {
singleAtRule: false,
important: false,
group: '',
// If true, won't be stored in JSON or showed in CSS
shallow: false,
_undo: true,
};
}

4
src/css_composer/model/CssRules.js

@ -19,7 +19,7 @@ export default Collection.extend({
toJSON(opts) {
const result = Collection.prototype.toJSON.call(this, opts);
return result.filter(i => i.style);
return result.filter(rule => rule.style && !rule.shallow);
},
onAdd(model, c, o) {
@ -38,5 +38,5 @@ export default Collection.extend({
}
opt.em = this.editor;
return Collection.prototype.add.apply(this, [models, opt]);
}
},
});

14
src/rich_text_editor/model/RichTextEditor.js

@ -92,6 +92,7 @@ export default class RichTextEditor {
this.setEl(el);
this.updateActiveActions = this.updateActiveActions.bind(this);
this.__onKeydown = this.__onKeydown.bind(this);
this.__onPaste = this.__onPaste.bind(this);
const acts = (settings.actions || []).map(action => {
let result = action;
@ -201,6 +202,7 @@ export default class RichTextEditor {
el.contentEditable = !!enable;
method(el, 'mouseup keyup', this.updateActiveActions);
method(doc, 'keydown', this.__onKeydown);
method(doc, 'paste', this.__onPaste);
this.enabled = enable;
if (enable) {
@ -237,6 +239,18 @@ export default class RichTextEditor {
}
}
__onPaste(ev) {
const clipboardData = ev.clipboardData || window.clipboardData;
const text = clipboardData.getData('text');
const textHtml = clipboardData.getData('text/html');
// Replace \n with <br> in case of plain text
if (text && !textHtml) {
ev.preventDefault();
const html = text.replace(/(?:\r\n|\r|\n)/g, '<br/>');
this.doc.execCommand('insertHTML', false, html);
}
}
/**
* Sync actions with the current RTE
*/

17
src/style_manager/index.js

@ -63,7 +63,7 @@
* @module StyleManager
*/
import { isElement, isUndefined, isArray, isString, debounce } from 'underscore';
import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
import { isComponent } from 'utils/mixins';
import Module from 'common/module';
import { Model } from 'common';
@ -127,6 +127,7 @@ export default () => {
* @private
*/
init(config = {}) {
bindAll(this, '__clearStateTarget');
this.__initConfig(defaults, config);
const c = this.config;
const { em } = c;
@ -145,6 +146,8 @@ export default () => {
const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
const upAll = debounce(() => this.__upSel());
model.listenTo(em, ev, upAll);
// Clear state target on any component selection change, without debounce (#4208)
model.listenTo(em, 'component:toggled', this.__clearStateTarget);
// Triggers only for properties (avoid selection refresh)
const upProps = debounce(() => {
@ -176,6 +179,16 @@ export default () => {
this.em.trigger(event, ...data);
},
__clearStateTarget() {
const { em } = this;
const stateTarget = this.__getStateTarget();
stateTarget &&
em?.skip(() => {
em.get('CssComposer').remove(stateTarget);
this.model.set({ stateTarget: null });
});
},
onLoad() {
// Use silent as sectors' view will be created and rendered on StyleManager.render
sectors.add(this.config.sectors, { silent: true });
@ -367,7 +380,7 @@ export default () => {
if (state && lastTarget?.getState?.()) {
const style = lastTarget.getStyle();
if (!stateTarget) {
stateTarget = cssc.getAll().add({ selectors: 'gjs-selected', style, important: true });
stateTarget = cssc.getAll().add({ selectors: 'gjs-selected', style, shallow: true, important: true });
} else {
stateTarget.setStyle(style);
}

74
test/specs/code_manager/model/CodeModels.js

@ -20,7 +20,7 @@ describe('HtmlGenerator', () => {
{},
{
em,
componentTypes: dcomp.componentTypes
componentTypes: dcomp.componentTypes,
}
);
});
@ -30,41 +30,37 @@ describe('HtmlGenerator', () => {
});
test('Build correctly one component', () => {
expect(obj.build(comp)).toEqual('');
expect(obj.build(comp)).toEqual('<div></div>');
});
test('Build correctly empty component inside', () => {
var m1 = comp.get('components').add({});
expect(obj.build(comp)).toEqual('<div></div>');
comp.get('components').add({});
expect(obj.build(comp)).toEqual('<div><div></div></div>');
});
test('Build correctly not empty component inside', () => {
var m1 = comp.get('components').add({
const m1 = comp.get('components').add({
tagName: 'article',
attributes: {
'data-test1': 'value1',
'data-test2': 'value2'
}
'data-test2': 'value2',
},
});
expect(obj.build(comp)).toEqual(
'<article data-test1="value1" data-test2="value2"></article>'
);
expect(obj.build(m1)).toEqual('<article data-test1="value1" data-test2="value2"></article>');
});
test('Build correctly component with classes', () => {
var m1 = comp.get('components').add({
const m1 = comp.get('components').add({
tagName: 'article',
attributes: {
'data-test1': 'value1',
'data-test2': 'value2'
}
'data-test2': 'value2',
},
});
['class1', 'class2'].forEach(item => {
m1.get('classes').add({ name: item });
});
expect(obj.build(comp)).toEqual(
'<article data-test1="value1" data-test2="value2" class="class1 class2"></article>'
);
expect(obj.build(m1)).toEqual('<article data-test1="value1" data-test2="value2" class="class1 class2"></article>');
});
});
@ -82,7 +78,7 @@ describe('CssGenerator', () => {
{},
{
em,
componentTypes: dcomp.componentTypes
componentTypes: dcomp.componentTypes,
}
);
});
@ -105,12 +101,10 @@ describe('CssGenerator', () => {
tagName: 'article',
style: {
prop1: 'value1',
prop2: 'value2'
}
prop2: 'value2',
},
});
expect(obj.build(comp)).toEqual(
'#' + m1.getId() + '{prop1:value1;prop2:value2;}'
);
expect(obj.build(comp)).toEqual('#' + m1.getId() + '{prop1:value1;prop2:value2;}');
});
test('Build correctly component with class styled', () => {
@ -121,9 +115,7 @@ describe('CssGenerator', () => {
var rule = cssc.add(cls1);
rule.set('style', { prop1: 'value1', prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual(
'.class1{prop1:value1;prop2:value2;}'
);
expect(obj.build(comp, { cssc })).toEqual('.class1{prop1:value1;prop2:value2;}');
});
test('Build correctly component styled with class and state', () => {
@ -135,9 +127,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1', prop2: 'value2' });
rule.set('state', 'hover');
expect(obj.build(comp, { cssc })).toEqual(
'.class1:hover{prop1:value1;prop2:value2;}'
);
expect(obj.build(comp, { cssc })).toEqual('.class1:hover{prop1:value1;prop2:value2;}');
});
test('Build correctly with more classes', () => {
@ -149,9 +139,7 @@ describe('CssGenerator', () => {
var rule = cssc.add([cls1, cls2]);
rule.set('style', { prop1: 'value1', prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual(
'.class1.class2{prop1:value1;prop2:value2;}'
);
expect(obj.build(comp, { cssc })).toEqual('.class1.class2{prop1:value1;prop2:value2;}');
});
test('Build rules with mixed classes', () => {
@ -175,9 +163,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1', prop2: 'value2' });
rule.set('selectorsAdd', '.class1 .class2, div > .class4');
expect(obj.build(comp, { cssc })).toEqual(
'.class1 .class2, div > .class4{prop1:value1;prop2:value2;}'
);
expect(obj.build(comp, { cssc })).toEqual('.class1 .class2, div > .class4{prop1:value1;prop2:value2;}');
});
test('Build correctly with class styled out', () => {
@ -191,9 +177,7 @@ describe('CssGenerator', () => {
var rule2 = cssc.add(cls2);
rule2.set('style', { prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual(
'.class1.class2{prop1:value1;}.class2{prop2:value2;}'
);
expect(obj.build(comp, { cssc })).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}');
});
test('Rule with media query', () => {
@ -206,9 +190,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1' });
rule.set('mediaText', '(max-width: 999px)');
expect(obj.build(comp, { cssc })).toEqual(
'@media (max-width: 999px){.class1.class2{prop1:value1;}}'
);
expect(obj.build(comp, { cssc })).toEqual('@media (max-width: 999px){.class1.class2{prop1:value1;}}');
});
test('Rules mixed with media queries', () => {
@ -263,7 +245,7 @@ describe('CssGenerator', () => {
{},
{
em,
componentTypes: dcomp.componentTypes
componentTypes: dcomp.componentTypes,
}
);
comp.setStyle({ color: 'red' });
@ -299,7 +281,7 @@ describe('CssGenerator', () => {
[
['@media (max-width: 999px)', 999],
['@media (min-width: 123%)', 123],
['@media (min-width: 1040rem)', 1040]
['@media (min-width: 1040rem)', 1040],
].forEach(item => {
expect(obj.getQueryLength(item[0])).toBe(item[1]);
});
@ -312,14 +294,14 @@ describe('CssGenerator', () => {
'@font-face': 2,
'@media (max-width: 768px)': 3,
'@media (max-width: 1020ch)': 4,
'@media (max-width: 10%)': 5
'@media (max-width: 10%)': 5,
})
).toEqual([
{ key: '@font-face', value: 2 },
{ key: '@media (max-width: 1020ch)', value: 4 },
{ key: '@media (max-width: 768px)', value: 3 },
{ key: '@media (max-width: 480px)', value: 1 },
{ key: '@media (max-width: 10%)', value: 5 }
{ key: '@media (max-width: 10%)', value: 5 },
]);
});
@ -330,14 +312,14 @@ describe('CssGenerator', () => {
'@font-face': 2,
'@media (min-width: 768px)': 3,
'@media (min-width: 1020ch)': 4,
'@media (min-width: 10%)': 5
'@media (min-width: 10%)': 5,
})
).toEqual([
{ key: '@font-face', value: 2 },
{ key: '@media (min-width: 10%)', value: 5 },
{ key: '@media (min-width: 480px)', value: 1 },
{ key: '@media (min-width: 768px)', value: 3 },
{ key: '@media (min-width: 1020ch)', value: 4 }
{ key: '@media (min-width: 1020ch)', value: 4 },
]);
});
});

4
test/specs/dom_components/index.js

@ -152,12 +152,12 @@ describe('DOM Components', () => {
obj = em.get('DomComponents');
const cc = em.get('CssComposer');
const id = 'idtest';
const comp = obj.addComponent(`
const component = obj.addComponent(`
<div id="${id}" style="color:red; padding: 50px 100px">Text</div>
<style>
#${id} { background-color: red }
</style>`);
expect(em.getHtml()).toEqual(`<div id="${id}">Text</div>`);
expect(em.getHtml({ component })).toEqual(`<div id="${id}">Text</div>`);
expect(obj.getComponents().length).toEqual(1);
obj.getComponents().first().addStyle({ margin: '10px' });
expect(cc.getAll().length).toEqual(1);

21
test/specs/grapesjs/headless.js

@ -13,12 +13,13 @@ describe('GrapesJS Headless', () => {
let editor;
const cmpObj = {
attributes: { class: 'cls', test: 'value' },
components: { type: 'textnode', content: 'Test' }
components: { type: 'textnode', content: 'Test' },
};
const cmpStr = '<div class="cls" test="value">Test</div>';
const fullHtml = `<body>${cmpStr}</body>`;
const styleObj = {
selectors: [{ name: 'cls' }],
style: { color: 'red' }
style: { color: 'red' },
};
const styleStr = '.cls{color:red;}';
@ -37,7 +38,7 @@ describe('GrapesJS Headless', () => {
expect(comp.toHTML()).toBe(cmpStr);
expect(editor.Selectors.getAll().length).toBe(1); // 1 selector is created
expect(editor.Css.getAll().length).toBe(0); // No CSS
expect(editor.getHtml()).toBe(cmpStr);
expect(editor.getHtml()).toBe(fullHtml);
expect(editor.getCss()).toBe(''); // same as default
});
@ -55,10 +56,10 @@ describe('GrapesJS Headless', () => {
copyable: false,
selectable: true,
content: 'Hello!',
_innertext: false
}
]
}
_innertext: false,
},
],
},
]);
expect(res.length).toBe(1);
const resHtml = '<h1>Hello!</h1>';
@ -66,7 +67,7 @@ describe('GrapesJS Headless', () => {
expect(comp.toHTML()).toBe(resHtml);
expect(editor.Selectors.getAll().length).toBe(0);
expect(editor.Css.getAll().length).toBe(0);
expect(editor.getHtml()).toBe(resHtml);
expect(editor.getHtml()).toBe(`<body>${resHtml}</body>`);
expect(editor.getCss()).toBe('');
});
@ -81,10 +82,10 @@ describe('GrapesJS Headless', () => {
test('Load data', () => {
editor.loadData({
components: [cmpObj],
styles: [styleObj]
styles: [styleObj],
});
expect(editor.Selectors.getAll().length).toBe(1);
expect(editor.getHtml()).toBe(cmpStr);
expect(editor.getHtml()).toBe(fullHtml);
expect(editor.getCss()).toBe(styleStr);
});
});

95
test/specs/grapesjs/index.js

@ -18,7 +18,7 @@ describe('GrapesJS', () => {
},
load(keys, clb) {
return clb(storage);
}
},
};
beforeAll(() => {
@ -27,16 +27,17 @@ describe('GrapesJS', () => {
beforeEach(() => {
storage = {};
htmlString = '<div class="test1"></div><div class="test2"></div>';
const initHtml = '<div class="test1"></div><div class="test2"></div>';
htmlString = `<body>${initHtml}</body>`;
cssString = '.test2{color:red}.test3{color:blue}';
documentEl = '<style>' + cssString + '</style>' + htmlString;
documentEl = '<style>' + cssString + '</style>' + initHtml;
config = {
container: '#' + editorName,
storageManager: {
autoload: 0,
autosave: 0,
type: 0
}
type: 0,
},
};
obj = grapesjs;
document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`;
@ -67,19 +68,19 @@ describe('GrapesJS', () => {
container: document.createElement('div'),
storageManager: {
autoload: 0,
type: 'none'
}
type: 'none',
},
};
var editor = obj.init(configAlt);
expect(editor).toBeTruthy();
});
test('New editor is empty', () => {
var editor = obj.init(config);
var html = editor.getHtml();
//var css = editor.getCss();
var protCss = editor.getConfig().protectedCss;
expect(html ? html : '').toBeFalsy();
const editor = obj.init(config);
const html = editor.getHtml();
//const css = editor.getCss();
const protCss = editor.getConfig().protectedCss;
expect(html).toBe('<body></body>');
//expect((css ? css : '')).toEqual(protCss);
expect(editor.getComponents().length).toEqual(0);
expect(editor.getStyle().length).toEqual(0);
@ -93,9 +94,7 @@ describe('GrapesJS', () => {
const body = editor.Canvas.getBody();
expect(body.outerHTML).toContain(config.baseCss);
expect(body.outerHTML.replace(/\s+/g, ` `)).not.toContain(
`body { margin: 0;`
);
expect(body.outerHTML.replace(/\s+/g, ` `)).not.toContain(`body { margin: 0;`);
});
test('Editor canvas baseCSS defaults to sensible values if not defined', () => {
@ -104,9 +103,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config);
expect(
window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ` `)
).toContain(`body { margin: 0;`);
expect(window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ` `)).toContain(`body { margin: 0;`);
});
test('Init editor with html', () => {
@ -114,13 +111,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config);
var comps = editor.DomComponents.getComponents();
expect(comps.length).toEqual(2);
expect(
comps
.at(0)
.get('classes')
.at(0)
.get('name')
).toEqual('test1');
expect(comps.at(0).get('classes').at(0).get('name')).toEqual('test1');
});
test('Init editor with css', () => {
@ -128,13 +119,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config);
var rules = editor.CssComposer.getAll();
expect(rules.length).toEqual(2);
expect(
rules
.at(0)
.get('selectors')
.at(0)
.get('name')
).toEqual('test2');
expect(rules.at(0).get('selectors').at(0).get('name')).toEqual('test2');
});
test('Init editor from element', () => {
@ -193,13 +178,7 @@ describe('GrapesJS', () => {
editor.setStyle(cssString);
var styles = editor.getStyle();
expect(styles.length).toEqual(2);
expect(
styles
.at(1)
.get('selectors')
.at(0)
.get('name')
).toEqual('test3');
expect(styles.at(1).get('selectors').at(0).get('name')).toEqual('test3');
});
test('Set style as as array of objects', () => {
@ -207,20 +186,12 @@ describe('GrapesJS', () => {
editor.setStyle([{ selectors: ['test4'] }, { selectors: ['test5'] }]);
var styles = editor.getStyle();
expect(styles.length).toEqual(2);
expect(
styles
.at(1)
.get('selectors')
.at(0)
.get('name')
).toEqual('test5');
expect(styles.at(1).get('selectors').at(0).get('name')).toEqual('test5');
});
test.skip('Adds new storage as plugin and store data there', done => {
const pluginName = storageId + '-p2';
obj.plugins.add(pluginName, e =>
e.StorageManager.add(storageId, storageMock)
);
obj.plugins.add(pluginName, e => e.StorageManager.add(storageId, storageMock));
config.storageManager.type = storageId;
config.plugins = [pluginName];
const editor = obj.init(config);
@ -239,26 +210,24 @@ describe('GrapesJS', () => {
const style = [
{
selectors: [{ name: 'sclass1' }],
style: { color: 'green' }
style: { color: 'green' },
},
{
selectors: [{ name: 'test2' }],
style: styleResult
style: styleResult,
},
{
selectors: [{ name: 'test3' }],
style: { color: 'black', display: 'block' }
}
style: { color: 'black', display: 'block' },
},
];
storage = {
css: '* { box-sizing: border-box; } body {margin: 0;}',
styles: JSON.stringify(style)
styles: JSON.stringify(style),
};
const pluginName = storageId + '-p';
obj.plugins.add(pluginName, e =>
e.StorageManager.add(storageId, storageMock)
);
obj.plugins.add(pluginName, e => e.StorageManager.add(storageId, storageMock));
config.fromElement = 1;
config.storageManager.type = storageId;
config.plugins = [pluginName];
@ -326,7 +295,7 @@ describe('GrapesJS', () => {
editor.Commands.add('test-command', {
run(ed, caller, opts) {
ed.testVal = ed.getHtml() + opts.val;
}
},
});
editor.runCommand('test-command', { val: 5 });
expect(editor.testVal).toEqual(htmlString + '5');
@ -339,7 +308,7 @@ describe('GrapesJS', () => {
editor.Commands.add('test-command', {
stop(ed, caller, opts) {
ed.testVal = ed.getHtml() + opts.val;
}
},
});
editor.stopCommand('test-command', { val: 5, force: 1 });
expect(editor.testVal).toEqual(htmlString + '5');
@ -356,7 +325,7 @@ describe('GrapesJS', () => {
editor.on(`abort:${id}`, () => (result.abort = 1));
editor.Commands.add(id, {
run() {},
stop() {}
stop() {},
});
editor.runCommand(id);
editor.stopCommand(id);
@ -367,7 +336,7 @@ describe('GrapesJS', () => {
runBefore: 1,
stop: 1,
stopBefore: 1,
abort: 1
abort: 1,
});
});
@ -375,7 +344,7 @@ describe('GrapesJS', () => {
config.deviceManager = {};
config.deviceManager.devices = [
{ name: '1', width: '2' },
{ name: '3', width: '4' }
{ name: '3', width: '4' },
];
var editor = obj.init(config);
expect(editor.DeviceManager.getAll().length).toEqual(2);
@ -516,7 +485,7 @@ describe('GrapesJS', () => {
const toSpy = {
selected() {},
deselected() {},
toggled() {}
toggled() {},
};
const selected = jest.spyOn(toSpy, 'selected');
const deselected = jest.spyOn(toSpy, 'deselected');

Loading…
Cancel
Save