').append(
22 | ``
25 | );
26 | this.$cn.append(this.$header);
27 | this.$cn2.append(
28 | `
${em.t('traitManager.label')}
`
29 | );
30 | this.$cn2.append(tmView.render().el);
31 | var panels = editor.Panels;
32 |
33 | if (!panels.getPanel('views-container'))
34 | panelC = panels.addPanel({ id: 'views-container' });
35 | else panelC = panels.getPanel('views-container');
36 |
37 | panelC
38 | .set('appendContent', this.$cn.get(0))
39 | .trigger('change:appendContent');
40 |
41 | this.target = editor.getModel();
42 | this.listenTo(this.target, 'component:toggled', this.toggleTm);
43 | }
44 |
45 | this.toggleTm();
46 | },
47 |
48 | /**
49 | * Toggle Trait Manager visibility
50 | * @private
51 | */
52 | toggleTm() {
53 | const sender = this.sender;
54 | if (sender && sender.get && !sender.get('active')) return;
55 |
56 | if (this.target.getSelectedAll().length === 1) {
57 | this.$cn2.show();
58 | this.$header.hide();
59 | } else {
60 | this.$cn2.hide();
61 | this.$header.show();
62 | }
63 | },
64 |
65 | stop() {
66 | this.$cn2 && this.$cn2.hide();
67 | this.$header && this.$header.hide();
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/test/specs/panels/view/PanelView.js:
--------------------------------------------------------------------------------
1 | import PanelView from 'panels/view/PanelView';
2 | import Panel from 'panels/model/Panel';
3 |
4 | describe('PanelView', () => {
5 | var fixtures;
6 | var model;
7 | var view;
8 |
9 | beforeEach(() => {
10 | model = new Panel();
11 | view = new PanelView({
12 | model
13 | });
14 | document.body.innerHTML = '
';
15 | fixtures = document.body.querySelector('#fixtures');
16 | fixtures.appendChild(view.render().el);
17 | });
18 |
19 | afterEach(() => {
20 | view.remove();
21 | });
22 |
23 | test('Panel empty', () => {
24 | fixtures.firstChild.className = '';
25 | expect(fixtures.innerHTML).toEqual('
');
26 | });
27 |
28 | test('Append content', () => {
29 | model.set('appendContent', 'test');
30 | model.set('appendContent', 'test2');
31 | expect(view.$el.html()).toEqual('testtest2');
32 | });
33 |
34 | test('Update content', () => {
35 | model.set('content', 'test');
36 | model.set('content', 'test2');
37 | expect(view.$el.html()).toEqual('test2');
38 | });
39 |
40 | test('Hide panel', () => {
41 | expect(view.$el.hasClass('hidden')).toBeFalsy();
42 | model.set('visible', false);
43 | expect(view.$el.hasClass('hidden')).toBeTruthy();
44 | });
45 |
46 | test('Show panel', () => {
47 | model.set('visible', false);
48 | expect(view.$el.hasClass('hidden')).toBeTruthy();
49 | model.set('visible', true);
50 | expect(view.$el.hasClass('hidden')).toBeFalsy();
51 | });
52 |
53 | describe('Init with options', () => {
54 | beforeEach(() => {
55 | model = new Panel({
56 | buttons: [{}]
57 | });
58 | view = new PanelView({
59 | model
60 | });
61 | document.body.innerHTML = '
';
62 | fixtures = document.body.querySelector('#fixtures');
63 | fixtures.appendChild(view.render().el);
64 | });
65 |
66 | afterEach(() => {
67 | view.remove();
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/src/panels/model/Buttons.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 | import Button from './Button';
3 |
4 | export default Backbone.Collection.extend({
5 | model: Button,
6 |
7 | /**
8 | * Deactivate all buttons, except one passed
9 | * @param {Object} except Model to ignore
10 | * @param {Boolean} r Recursive flag
11 | *
12 | * @return void
13 | * */
14 | deactivateAllExceptOne(except, r) {
15 | this.forEach((model, index) => {
16 | if (model !== except) {
17 | model.set('active', false);
18 | if (r && model.get('buttons').length)
19 | model.get('buttons').deactivateAllExceptOne(except, r);
20 | }
21 | });
22 | },
23 |
24 | /**
25 | * Deactivate all buttons
26 | * @param {String} ctx Context string
27 | *
28 | * @return void
29 | * */
30 | deactivateAll(ctx, sender) {
31 | const context = ctx || '';
32 | this.forEach(model => {
33 | if (model.get('context') == context && model !== sender) {
34 | model.set('active', false, { silent: 1 });
35 | model.trigger('updateActive', { fromCollection: 1 });
36 | }
37 | });
38 | },
39 |
40 | /**
41 | * Disables all buttons
42 | * @param {String} ctx Context string
43 | *
44 | * @return void
45 | * */
46 | disableAllButtons(ctx) {
47 | var context = ctx || '';
48 | this.forEach((model, index) => {
49 | if (model.get('context') == context) {
50 | model.set('disable', true);
51 | }
52 | });
53 | },
54 |
55 | /**
56 | * Disables all buttons, except one passed
57 | * @param {Object} except Model to ignore
58 | * @param {Boolean} r Recursive flag
59 | *
60 | * @return void
61 | * */
62 | disableAllButtonsExceptOne(except, r) {
63 | this.forEach((model, index) => {
64 | if (model !== except) {
65 | model.set('disable', true);
66 | if (r && model.get('buttons').length)
67 | model.get('buttons').disableAllButtonsExceptOne(except, r);
68 | }
69 | });
70 | }
71 | });
72 |
--------------------------------------------------------------------------------
/src/style_manager/view/PropertiesView.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 | import { appendAtIndex } from 'utils/dom';
3 |
4 | export default Backbone.View.extend({
5 | initialize(o) {
6 | this.config = o.config || {};
7 | this.pfx = this.config.stylePrefix || '';
8 | this.target = o.target || {};
9 | this.propTarget = o.propTarget || {};
10 | this.onChange = o.onChange;
11 | this.onInputRender = o.onInputRender || {};
12 | this.customValue = o.customValue || {};
13 | this.properties = [];
14 | const coll = this.collection;
15 | this.listenTo(coll, 'add', this.addTo);
16 | this.listenTo(coll, 'reset', this.render);
17 | },
18 |
19 | addTo(model, coll, opts) {
20 | this.add(model, null, opts);
21 | },
22 |
23 | add(model, frag, opts = {}) {
24 | const appendTo = frag || this.el;
25 | const view = new model.typeView({
26 | model,
27 | name: model.get('name'),
28 | id: this.pfx + model.get('property'),
29 | target: this.target,
30 | propTarget: this.propTarget,
31 | onChange: this.onChange,
32 | onInputRender: this.onInputRender,
33 | config: this.config
34 | });
35 |
36 | if (model.get('type') != 'composite') {
37 | view.customValue = this.customValue;
38 | }
39 |
40 | view.render();
41 | const rendered = view.el;
42 | this.properties.push(view);
43 | view.updateVisibility();
44 |
45 | appendAtIndex(appendTo, rendered, opts.at);
46 | },
47 |
48 | render() {
49 | const { $el } = this;
50 | this.clearItems();
51 | const fragment = document.createDocumentFragment();
52 | this.collection.each(model => this.add(model, fragment));
53 | $el.empty();
54 | $el.append(fragment);
55 | $el.attr('class', `${this.pfx}properties`);
56 | return this;
57 | },
58 |
59 | remove() {
60 | Backbone.View.prototype.remove.apply(this, arguments);
61 | this.clearItems();
62 | },
63 |
64 | clearItems() {
65 | this.properties.forEach(item => item.remove());
66 | this.properties = [];
67 | }
68 | });
69 |
--------------------------------------------------------------------------------
/test/specs/block_manager/index.js:
--------------------------------------------------------------------------------
1 | import BlockManager from 'block_manager';
2 | import BlocksView from './view/BlocksView';
3 |
4 | describe('BlockManager', () => {
5 | describe('Main', () => {
6 | var obj;
7 | var idTest;
8 | var optsTest;
9 |
10 | beforeEach(() => {
11 | idTest = 'h1-block';
12 | optsTest = {
13 | label: 'Heading',
14 | content: '
Test
'
15 | };
16 | obj = new BlockManager().init();
17 | obj.render();
18 | });
19 |
20 | afterEach(() => {
21 | obj = null;
22 | });
23 |
24 | test('Object exists', () => {
25 | expect(obj).toBeTruthy();
26 | });
27 |
28 | test('No blocks inside', () => {
29 | expect(obj.getAll().length).toEqual(0);
30 | });
31 |
32 | test('No categories inside', () => {
33 | expect(obj.getCategories().length).toEqual(0);
34 | });
35 |
36 | test('Add new block', () => {
37 | var model = obj.add(idTest, optsTest);
38 | expect(obj.getAll().length).toEqual(1);
39 | });
40 |
41 | test('Added block has correct data', () => {
42 | var model = obj.add(idTest, optsTest);
43 | expect(model.get('label')).toEqual(optsTest.label);
44 | expect(model.get('content')).toEqual(optsTest.content);
45 | });
46 |
47 | test('Add block with attributes', () => {
48 | optsTest.attributes = { class: 'test' };
49 | var model = obj.add(idTest, optsTest);
50 | expect(model.get('attributes').class).toEqual('test');
51 | });
52 |
53 | test('The id of the block is unique', () => {
54 | var model = obj.add(idTest, optsTest);
55 | var model2 = obj.add(idTest, { other: 'test' });
56 | expect(model).toEqual(model2);
57 | });
58 |
59 | test('Get block by id', () => {
60 | var model = obj.add(idTest, optsTest);
61 | var model2 = obj.get(idTest);
62 | expect(model).toEqual(model2);
63 | });
64 |
65 | test('Render blocks', () => {
66 | obj.render();
67 | expect(obj.getContainer()).toBeTruthy();
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/test/specs/asset_manager/view/AssetImageView.js:
--------------------------------------------------------------------------------
1 | import AssetImageView from 'asset_manager/view/AssetImageView';
2 | import Assets from 'asset_manager/model/Assets';
3 |
4 | let obj;
5 |
6 | describe('AssetImageView', () => {
7 | beforeEach(() => {
8 | var coll = new Assets();
9 | var model = coll.add({ type: 'image', src: '/test' });
10 | obj = new AssetImageView({
11 | collection: new Assets(),
12 | config: {},
13 | model
14 | });
15 | document.body.innerHTML = '
';
16 | document.body.querySelector('#fixtures').appendChild(obj.render().el);
17 | });
18 |
19 | afterEach(() => {
20 | obj = null;
21 | document.body.innerHTML = '';
22 | });
23 |
24 | test('Object exists', () => {
25 | expect(AssetImageView).toBeTruthy();
26 | });
27 |
28 | describe('Asset should be rendered correctly', () => {
29 | test('Has preview box', () => {
30 | var $asset = obj.$el;
31 | expect($asset.find('.preview').length).toEqual(1);
32 | });
33 |
34 | test('Has meta box', () => {
35 | var $asset = obj.$el;
36 | expect($asset.find('.meta').length).toEqual(1);
37 | });
38 |
39 | test('Has close button', () => {
40 | var $asset = obj.$el;
41 | expect($asset.find('[data-toggle=asset-remove]').length).toEqual(1);
42 | });
43 | });
44 |
45 | test('Could be selected', () => {
46 | var spy = jest.spyOn(obj, 'updateTarget');
47 | obj.$el.trigger('click');
48 | expect(obj.$el.attr('class')).toContain('highlight');
49 | expect(spy).toHaveBeenCalled();
50 | });
51 |
52 | test('Could be chosen', () => {
53 | sinon.stub(obj, 'updateTarget');
54 | var spy = jest.spyOn(obj, 'updateTarget');
55 | obj.$el.trigger('dblclick');
56 | expect(spy).toHaveBeenCalled();
57 | //obj.updateTarget.calledOnce.should.equal(true);
58 | });
59 |
60 | test('Could be removed', () => {
61 | var spy = sinon.spy();
62 | obj.model.on('remove', spy);
63 | obj.onRemove({ stopImmediatePropagation() {} });
64 | expect(spy.called).toEqual(true);
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const pkg = require('./package.json');
3 | const webpack = require('webpack');
4 | const path = require('path');
5 | const fs = require('fs');
6 | const rootDir = path.resolve(__dirname);
7 | let plugins = [];
8 |
9 | module.exports = env => {
10 | const name = pkg.name;
11 | const isProd = env === 'prod';
12 | const output = {
13 | path: path.join(__dirname),
14 | filename: 'dist/grapes.min.js',
15 | library: name,
16 | libraryExport: 'default',
17 | libraryTarget: 'umd',
18 | };
19 |
20 | if (isProd) {
21 | plugins = [
22 | new webpack.optimize.ModuleConcatenationPlugin(),
23 | new webpack.BannerPlugin(`${name} - ${pkg.version}`),
24 | ];
25 | } else if (env === 'dev') {
26 | output.filename = 'dist/grapes.js';
27 | } else {
28 | const index = 'index.html';
29 | const indexDev = `_${index}`;
30 | const template = fs.existsSync(indexDev) ? indexDev : index;
31 | plugins.push(new HtmlWebpackPlugin({ template, inject: false }));
32 | }
33 |
34 | return {
35 | entry: './src',
36 | output: output,
37 | plugins: plugins,
38 | mode: isProd ? 'production' : 'development',
39 | devtool: isProd ? 'source-map' : (!env ? 'cheap-module-eval-source-map' : false),
40 | devServer: {
41 | headers: { 'Access-Control-Allow-Origin': '*' },
42 | disableHostCheck: true,
43 | },
44 | module: {
45 | rules: [{
46 | test: /\/index\.js$/,
47 | loader: 'string-replace-loader',
48 | query: {
49 | search: '<# VERSION #>',
50 | replace: pkg.version
51 | }
52 | }, {
53 | test: /\.js$/,
54 | loader: 'babel-loader',
55 | include: /src/,
56 | options: { cacheDirectory: true },
57 | }],
58 | },
59 | resolve: {
60 | modules: ['src', 'node_modules'],
61 | alias: {
62 | jquery: 'cash-dom',
63 | backbone: `${rootDir}/node_modules/backbone`,
64 | underscore: `${rootDir}/node_modules/underscore`,
65 | }
66 | }
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/src/trait_manager/index.js:
--------------------------------------------------------------------------------
1 | import { defaults, isElement } from 'underscore';
2 | import defaultOpts from './config/config';
3 | import TraitsView from './view/TraitsView';
4 |
5 | export default () => {
6 | let c = {};
7 | let TraitsViewer;
8 |
9 | return {
10 | TraitsView,
11 |
12 | /**
13 | * Name of the module
14 | * @type {String}
15 | * @private
16 | */
17 | name: 'TraitManager',
18 |
19 | /**
20 | * Get configuration object
21 | * @return {Object}
22 | * @private
23 | */
24 | getConfig() {
25 | return c;
26 | },
27 |
28 | /**
29 | * Initialize module. Automatically called with a new instance of the editor
30 | * @param {Object} config Configurations
31 | */
32 | init(config = {}) {
33 | c = config;
34 | defaults(c, defaultOpts);
35 | const ppfx = c.pStylePrefix;
36 | ppfx && (c.stylePrefix = `${ppfx}${c.stylePrefix}`);
37 | TraitsViewer = new TraitsView({
38 | collection: [],
39 | editor: c.em,
40 | config: c
41 | });
42 | return this;
43 | },
44 |
45 | postRender() {
46 | const elTo = this.getConfig().appendTo;
47 |
48 | if (elTo) {
49 | const el = isElement(elTo) ? elTo : document.querySelector(elTo);
50 | el.appendChild(this.render());
51 | }
52 | },
53 |
54 | /**
55 | *
56 | * Get Traits viewer
57 | * @private
58 | */
59 | getTraitsViewer() {
60 | return TraitsViewer;
61 | },
62 |
63 | /**
64 | * Add new trait type
65 | * @param {string} name Type name
66 | * @param {Object} methods Object representing the trait
67 | */
68 | addType(name, trait) {
69 | var itemView = TraitsViewer.itemView;
70 | TraitsViewer.itemsView[name] = itemView.extend(trait);
71 | },
72 |
73 | /**
74 | * Get trait type
75 | * @param {string} name Type name
76 | * @return {Object}
77 | */
78 | getType(name) {
79 | return TraitsViewer.itemsView[name];
80 | },
81 |
82 | render() {
83 | return TraitsViewer.render().el;
84 | }
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/src/dom_components/view/ToolbarButtonView.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 |
3 | export default Backbone.View.extend({
4 | events() {
5 | return (
6 | this.model.get('events') || {
7 | mousedown: 'handleClick'
8 | }
9 | );
10 | },
11 |
12 | attributes() {
13 | return this.model.get('attributes');
14 | },
15 |
16 | initialize(opts = {}) {
17 | const { config = {} } = opts;
18 | this.em = config.em;
19 | this.editor = config.editor;
20 | },
21 |
22 | handleClick(event) {
23 | event.preventDefault();
24 | event.stopPropagation();
25 |
26 | /*
27 | * Since the toolbar lives outside the canvas frame, the event's
28 | * generated on it have clientX and clientY relative to the page.
29 | *
30 | * This causes issues during events like dragging, where they depend
31 | * on the clientX and clientY.
32 | *
33 | * This makes sure the offsets are calculated.
34 | *
35 | * More information on
36 | * https://github.com/artf/grapesjs/issues/2372
37 | * https://github.com/artf/grapesjs/issues/2207
38 | */
39 |
40 | const { editor, em } = this;
41 | const { left, top } = editor.Canvas.getFrameEl().getBoundingClientRect();
42 |
43 | const calibrated = {
44 | ...event,
45 | clientX: event.clientX - left,
46 | clientY: event.clientY - top
47 | };
48 |
49 | em.trigger('toolbar:run:before');
50 | this.execCommand(calibrated);
51 | },
52 |
53 | execCommand(event) {
54 | const opts = { event };
55 | const command = this.model.get('command');
56 | const editor = this.editor;
57 |
58 | if (typeof command === 'function') {
59 | command(editor, null, opts);
60 | }
61 |
62 | if (typeof command === 'string') {
63 | editor.runCommand(command, opts);
64 | }
65 | },
66 |
67 | render() {
68 | const { editor, $el, model } = this;
69 | const id = model.get('id');
70 | const label = model.get('label');
71 | const pfx = editor.getConfig('stylePrefix');
72 | $el.addClass(`${pfx}toolbar-item`);
73 | id && $el.addClass(`${pfx}toolbar-item__${id}`);
74 | label && $el.append(label);
75 | return this;
76 | }
77 | });
78 |
--------------------------------------------------------------------------------
/src/panels/view/PanelsView.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 | import PanelView from './PanelView';
3 |
4 | export default Backbone.View.extend({
5 | initialize(o) {
6 | this.opt = o || {};
7 | this.config = this.opt.config || {};
8 | this.pfx = this.config.stylePrefix || '';
9 | const items = this.collection;
10 | this.listenTo(items, 'add', this.addTo);
11 | this.listenTo(items, 'reset', this.render);
12 | this.listenTo(items, 'remove', this.onRemove);
13 | this.className = this.pfx + 'panels';
14 | },
15 |
16 | onRemove(model) {
17 | const view = model.view;
18 | view && view.remove();
19 | },
20 |
21 | /**
22 | * Add to collection
23 | * @param Object Model
24 | *
25 | * @return Object
26 | * @private
27 | * */
28 | addTo(model) {
29 | this.addToCollection(model);
30 | },
31 |
32 | /**
33 | * Add new object to collection
34 | * @param Object Model
35 | * @param Object Fragment collection
36 | * @param integer Index of append
37 | *
38 | * @return Object Object created
39 | * @private
40 | * */
41 | addToCollection(model, fragmentEl) {
42 | const fragment = fragmentEl || null;
43 | const config = this.config;
44 | const el = model.get('el');
45 | const view = new PanelView({
46 | el,
47 | model,
48 | config
49 | });
50 | const rendered = view.render().el;
51 | const appendTo = model.get('appendTo');
52 |
53 | // Do nothing if the panel was requested to be another element
54 | if (el) {
55 | } else if (appendTo) {
56 | var appendEl = document.querySelector(appendTo);
57 | appendEl.appendChild(rendered);
58 | } else {
59 | if (fragment) {
60 | fragment.appendChild(rendered);
61 | } else {
62 | this.$el.append(rendered);
63 | }
64 | }
65 |
66 | view.initResize();
67 | return rendered;
68 | },
69 |
70 | render() {
71 | const $el = this.$el;
72 | const frag = document.createDocumentFragment();
73 | $el.empty();
74 | this.collection.each(model => this.addToCollection(model, frag));
75 | $el.append(frag);
76 | $el.attr('class', this.className);
77 | return this;
78 | }
79 | });
80 |
--------------------------------------------------------------------------------
/src/domain_abstract/ui/Input.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 |
3 | const $ = Backbone.$;
4 |
5 | export default Backbone.View.extend({
6 | events: {
7 | change: 'handleChange'
8 | },
9 |
10 | template() {
11 | return `
`;
12 | },
13 |
14 | inputClass() {
15 | return `${this.ppfx}field`;
16 | },
17 |
18 | holderClass() {
19 | return `${this.ppfx}input-holder`;
20 | },
21 |
22 | initialize(opts = {}) {
23 | const ppfx = opts.ppfx || '';
24 | this.opts = opts;
25 | this.ppfx = ppfx;
26 | this.em = opts.target || {};
27 | this.listenTo(this.model, 'change:value', this.handleModelChange);
28 | },
29 |
30 | /**
31 | * Fired when the element of the property is updated
32 | */
33 | elementUpdated() {
34 | this.model.trigger('el:change');
35 | },
36 |
37 | /**
38 | * Set value to the input element
39 | * @param {string} value
40 | */
41 | setValue(value) {
42 | const model = this.model;
43 | let val = value || model.get('defaults');
44 | const input = this.getInputEl();
45 | input && (input.value = val);
46 | },
47 |
48 | /**
49 | * Updates the view when the model is changed
50 | * */
51 | handleModelChange(model, value, opts) {
52 | this.setValue(value, opts);
53 | },
54 |
55 | /**
56 | * Handled when the view is changed
57 | */
58 | handleChange(e) {
59 | e.stopPropagation();
60 | const value = this.getInputEl().value;
61 | this.model.set({ value }, { fromInput: 1 });
62 | this.elementUpdated();
63 | },
64 |
65 | /**
66 | * Get the input element
67 | * @return {HTMLElement}
68 | */
69 | getInputEl() {
70 | if (!this.inputEl) {
71 | const { model } = this;
72 | const plh = model.get('placeholder') || model.get('defaults') || '';
73 | this.inputEl = $(`
`);
74 | }
75 |
76 | return this.inputEl.get(0);
77 | },
78 |
79 | render() {
80 | this.inputEl = null;
81 | const el = this.$el;
82 | el.addClass(this.inputClass());
83 | el.html(this.template());
84 | el.find(`.${this.holderClass()}`).append(this.getInputEl());
85 | return this;
86 | }
87 | });
88 |
--------------------------------------------------------------------------------
/test/specs/block_manager/view/BlocksView.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 | import BlocksView from 'block_manager/view/BlocksView';
3 | import Blocks from 'block_manager/model/Blocks';
4 |
5 | describe('BlocksView', () => {
6 | var $fixtures;
7 | var $fixture;
8 | var model;
9 | var view;
10 | var editorModel;
11 | var ppfx;
12 |
13 | beforeEach(() => {
14 | model = new Blocks([]);
15 | view = new BlocksView({ collection: model });
16 | document.body.innerHTML = '
';
17 | document.body.querySelector('#fixtures').appendChild(view.render().el);
18 | });
19 |
20 | afterEach(() => {
21 | view.collection.reset();
22 | });
23 |
24 | test('The container is not empty', () => {
25 | expect(view.el.outerHTML).toBeTruthy();
26 | });
27 |
28 | test('No children inside', () => {
29 | expect(view.getBlocksEl().children.length).toEqual(0);
30 | });
31 |
32 | test('Render children on add', () => {
33 | model.add({});
34 | expect(view.getBlocksEl().children.length).toEqual(1);
35 | model.add([{}, {}]);
36 | expect(view.getBlocksEl().children.length).toEqual(3);
37 | });
38 |
39 | test('Destroy children on remove', () => {
40 | model.add([{}, {}]);
41 | expect(view.getBlocksEl().children.length).toEqual(2);
42 | model.at(0).destroy();
43 | expect(view.getBlocksEl().children.length).toEqual(1);
44 | });
45 |
46 | describe('With configs', () => {
47 | beforeEach(() => {
48 | ppfx = 'pfx-t-';
49 | editorModel = new Backbone.Model();
50 | model = new Blocks([{ name: 'test1' }, { name: 'test2' }]);
51 | view = new BlocksView(
52 | {
53 | collection: model
54 | },
55 | {
56 | pStylePrefix: ppfx
57 | }
58 | );
59 | document.body.innerHTML = '
';
60 | document.body.querySelector('#fixtures').appendChild(view.render().el);
61 | });
62 |
63 | test('Render children', () => {
64 | expect(view.getBlocksEl().children.length).toEqual(2);
65 | });
66 |
67 | test('Render container', () => {
68 | expect(view.getBlocksEl().getAttribute('class')).toEqual(
69 | ppfx + 'blocks-c'
70 | );
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/CarbonAds.vue:
--------------------------------------------------------------------------------
1 |
52 |
53 |
80 |
--------------------------------------------------------------------------------
/test/specs/asset_manager/view/FileUploader.js:
--------------------------------------------------------------------------------
1 | import FileUploader from 'asset_manager/view/FileUploader';
2 |
3 | describe('File Uploader', () => {
4 | let obj;
5 |
6 | beforeEach(() => {
7 | obj = new FileUploader({ config: {} });
8 | document.body.innerHTML = '
';
9 | document.body.querySelector('#fixtures').appendChild(obj.render().el);
10 | });
11 |
12 | afterEach(() => {
13 | obj.remove();
14 | });
15 |
16 | test('Object exists', () => {
17 | expect(FileUploader).toBeTruthy();
18 | });
19 |
20 | test('Has correct prefix', () => {
21 | expect(obj.pfx).toBeFalsy();
22 | });
23 |
24 | describe('Should be rendered correctly', () => {
25 | test('Has title', () => {
26 | expect(obj.$el.find('#title').length).toEqual(1);
27 | });
28 |
29 | test('Title is empty', () => {
30 | expect(obj.$el.find('#title').html()).toEqual('');
31 | });
32 |
33 | test('Has file input', () => {
34 | expect(obj.$el.find('input[type=file]').length).toEqual(1);
35 | });
36 |
37 | test('File input is enabled', () => {
38 | expect(obj.$el.find('input[type=file]').prop('disabled')).toEqual(true);
39 | });
40 | });
41 |
42 | describe('Interprets configurations correctly', () => {
43 | test('Could be disabled', () => {
44 | var view = new FileUploader({
45 | config: {
46 | disableUpload: true,
47 | upload: 'something'
48 | }
49 | });
50 | view.render();
51 | expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(true);
52 | });
53 |
54 | test('Handles multiUpload false', () => {
55 | var view = new FileUploader({
56 | config: {
57 | multiUpload: false
58 | }
59 | });
60 | view.render();
61 | expect(view.$el.find('input[type=file]').prop('multiple')).toBeFalsy();
62 | });
63 |
64 | test('Handles embedAsBase64 parameter', () => {
65 | var view = new FileUploader({
66 | config: {
67 | embedAsBase64: true
68 | }
69 | });
70 | view.render();
71 | expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(false);
72 | expect(view.uploadFile).toEqual(FileUploader.embedAsBase64);
73 | });
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/selector_manager/model/Selector.js:
--------------------------------------------------------------------------------
1 | import Backbone from 'backbone';
2 |
3 | const TYPE_CLASS = 1;
4 | const TYPE_ID = 2;
5 |
6 | const Selector = Backbone.Model.extend(
7 | {
8 | idAttribute: 'name',
9 |
10 | defaults: {
11 | name: '',
12 |
13 | label: '',
14 |
15 | // Type of the selector
16 | type: TYPE_CLASS,
17 |
18 | // If not active it's not selectable by the style manager (uncheckboxed)
19 | active: true,
20 |
21 | // Can't be seen by the style manager, therefore even by the user
22 | // Will be rendered only in export code
23 | private: false,
24 |
25 | // If true, can't be removed from the attacched element
26 | protected: false
27 | },
28 |
29 | initialize(props, opts = {}) {
30 | const { config = {} } = opts;
31 | const name = this.get('name');
32 | const label = this.get('label');
33 |
34 | if (!name) {
35 | this.set('name', label);
36 | } else if (!label) {
37 | this.set('label', name);
38 | }
39 |
40 | const namePreEsc = this.get('name');
41 | const { escapeName } = config;
42 | const nameEsc = escapeName
43 | ? escapeName(namePreEsc)
44 | : Selector.escapeName(namePreEsc);
45 | this.set('name', nameEsc);
46 | },
47 |
48 | /**
49 | * Get full selector name
50 | * @return {string}
51 | */
52 | getFullName(opts = {}) {
53 | const { escape } = opts;
54 | const name = this.get('name');
55 | let init = '';
56 |
57 | switch (this.get('type')) {
58 | case TYPE_CLASS:
59 | init = '.';
60 | break;
61 | case TYPE_ID:
62 | init = '#';
63 | break;
64 | }
65 |
66 | return init + (escape ? escape(name) : name);
67 | }
68 | },
69 | {
70 | // All type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors
71 | // Here I define only what I need
72 | TYPE_CLASS,
73 |
74 | TYPE_ID,
75 |
76 | /**
77 | * Escape string
78 | * @param {string} name
79 | * @return {string}
80 | * @private
81 | */
82 | escapeName(name) {
83 | return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-');
84 | }
85 | }
86 | );
87 |
88 | export default Selector;
89 |
--------------------------------------------------------------------------------
/src/dom_components/config/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | stylePrefix: 'comp-',
3 |
4 | wrapperId: 'wrapper',
5 |
6 | wrapperName: 'Body',
7 |
8 | // Default wrapper configuration
9 | wrapper: {
10 | removable: false,
11 | copyable: false,
12 | draggable: false,
13 | components: [],
14 | traits: [],
15 | stylable: [
16 | 'background',
17 | 'background-color',
18 | 'background-image',
19 | 'background-repeat',
20 | 'background-attachment',
21 | 'background-position',
22 | 'background-size'
23 | ]
24 | },
25 |
26 | // Could be used for default components
27 | components: [],
28 |
29 | // If the component is draggable you can drag the component itself (not only from the toolbar)
30 | draggableComponents: 1,
31 |
32 | // Generally, if you don't edit the wrapper in the editor, like
33 | // custom attributes, you don't need the wrapper stored in your JSON
34 | // structure, but in case you need it you can use this option.
35 | // If you have `config.avoidInlineStyle` disabled the wrapper will be stored
36 | // as we need to store inlined style.
37 | storeWrapper: 0,
38 |
39 | /**
40 | * You can setup a custom component definiton processor before adding it into the editor.
41 | * It might be useful to transform custom objects (es. some framework specific JSX) to GrapesJS component one.
42 | * This custom function will be executed on ANY new added component to the editor so make smart checks/conditions
43 | * to avoid doing useless executions
44 | * By default, GrapesJS supports already elements generated from React JSX preset
45 | * @example
46 | * processor: (obj) => {
47 | * if (obj.$$typeof) { // eg. this is a React Element
48 | * const gjsComponent = {
49 | * type: obj.type,
50 | * components: obj.props.children,
51 | * ...
52 | * };
53 | * ...
54 | * return gjsComponent;
55 | * }
56 | * }
57 | */
58 | processor: 0,
59 |
60 | // List of void elements
61 | voidElements: [
62 | 'area',
63 | 'base',
64 | 'br',
65 | 'col',
66 | 'embed',
67 | 'hr',
68 | 'img',
69 | 'input',
70 | 'keygen',
71 | 'link',
72 | 'menuitem',
73 | 'meta',
74 | 'param',
75 | 'source',
76 | 'track',
77 | 'wbr'
78 | ]
79 | };
80 |
--------------------------------------------------------------------------------
/docs/api/device_manager.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## DeviceManager
4 |
5 | You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
6 |
7 | ```js
8 | const editor = grapesjs.init({
9 | deviceManager: {
10 | // options
11 | }
12 | })
13 | ```
14 |
15 | Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
16 |
17 | ```js
18 | const deviceManager = editor.DeviceManager;
19 | ```
20 |
21 | - [add][2]
22 | - [get][3]
23 | - [getAll][4]
24 |
25 | ## add
26 |
27 | Add new device to the collection. URLs are supposed to be unique
28 |
29 | ### Parameters
30 |
31 | - `id` **[String][5]** Device id
32 | - `width` **[String][5]** Width of the device
33 | - `opts` **[Object][6]?** Custom options (optional, default `{}`)
34 |
35 | ### Examples
36 |
37 | ```javascript
38 | deviceManager.add('tablet', '900px');
39 | deviceManager.add('tablet2', '900px', {
40 | height: '300px',
41 | // At first, GrapesJS tries to localize the name by device id.
42 | // In case is not found, the `name` property is used (or `id` if name is missing)
43 | name: 'Tablet 2',
44 | widthMedia: '810px', // the width that will be used for the CSS media
45 | });
46 | ```
47 |
48 | Returns **Device** Added device
49 |
50 | ## get
51 |
52 | Return device by name
53 |
54 | ### Parameters
55 |
56 | - `name` **[string][5]** Name of the device
57 |
58 | ### Examples
59 |
60 | ```javascript
61 | var device = deviceManager.get('Tablet');
62 | console.log(JSON.stringify(device));
63 | // {name: 'Tablet', width: '900px'}
64 | ```
65 |
66 | ## getAll
67 |
68 | Return all devices
69 |
70 | ### Examples
71 |
72 | ```javascript
73 | var devices = deviceManager.getAll();
74 | console.log(JSON.stringify(devices));
75 | // [{name: 'Desktop', width: ''}, ...]
76 | ```
77 |
78 | Returns **Collection**
79 |
80 | [1]: https://github.com/artf/grapesjs/blob/master/src/device_manager/config/config.js
81 |
82 | [2]: #add
83 |
84 | [3]: #get
85 |
86 | [4]: #getAll
87 |
88 | [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
89 |
90 | [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
91 |
--------------------------------------------------------------------------------
/src/asset_manager/view/AssetImageView.js:
--------------------------------------------------------------------------------
1 | import { isFunction } from 'underscore';
2 | import AssetView from './AssetView';
3 |
4 | export default AssetView.extend({
5 | events: {
6 | 'click [data-toggle=asset-remove]': 'onRemove',
7 | click: 'onClick',
8 | dblclick: 'onDblClick'
9 | },
10 |
11 | getPreview() {
12 | const pfx = this.pfx;
13 | const src = this.model.get('src');
14 | return `
15 |
16 |
17 | `;
18 | },
19 |
20 | getInfo() {
21 | const pfx = this.pfx;
22 | const model = this.model;
23 | let name = model.get('name');
24 | let width = model.get('width');
25 | let height = model.get('height');
26 | let unit = model.get('unitDim');
27 | let dim = width && height ? `${width}x${height}${unit}` : '';
28 | name = name || model.getFilename();
29 | return `
30 |
${name}
31 |
${dim}
32 | `;
33 | },
34 |
35 | init(o) {
36 | const pfx = this.pfx;
37 | this.className += ` ${pfx}asset-image`;
38 | },
39 |
40 | /**
41 | * Triggered when the asset is clicked
42 | * @private
43 | * */
44 | onClick() {
45 | var onClick = this.config.onClick;
46 | var model = this.model;
47 | this.collection.trigger('deselectAll');
48 | this.$el.addClass(this.pfx + 'highlight');
49 |
50 | if (isFunction(onClick)) {
51 | onClick(model);
52 | } else {
53 | this.updateTarget(this.collection.target);
54 | }
55 | },
56 |
57 | /**
58 | * Triggered when the asset is double clicked
59 | * @private
60 | * */
61 | onDblClick() {
62 | const { em, model } = this;
63 | const onDblClick = this.config.onDblClick;
64 |
65 | if (isFunction(onDblClick)) {
66 | onDblClick(model);
67 | } else {
68 | this.updateTarget(this.collection.target);
69 | em && em.get('Modal').close();
70 | }
71 |
72 | var onSelect = this.collection.onSelect;
73 | isFunction(onSelect) && onSelect(model);
74 | },
75 |
76 | /**
77 | * Remove asset from collection
78 | * @private
79 | * */
80 | onRemove(e) {
81 | e.stopImmediatePropagation();
82 | this.model.collection.remove(this.model);
83 | }
84 | });
85 |
--------------------------------------------------------------------------------
/src/commands/view/CanvasMove.js:
--------------------------------------------------------------------------------
1 | import { bindAll } from 'underscore';
2 | import { on, off, getKeyChar } from 'utils/mixins';
3 | import Dragger from 'utils/Dragger';
4 |
5 | export default {
6 | run(ed) {
7 | bindAll(this, 'onKeyUp', 'enableDragger', 'disableDragger');
8 | this.editor = ed;
9 | this.canvasModel = this.canvas.getCanvasView().model;
10 | this.toggleMove(1);
11 | },
12 | stop(ed) {
13 | this.toggleMove();
14 | this.disableDragger();
15 | },
16 |
17 | onKeyUp(ev) {
18 | if (getKeyChar(ev) === ' ') {
19 | this.editor.stopCommand(this.id);
20 | }
21 | },
22 |
23 | enableDragger(ev) {
24 | this.toggleDragger(1, ev);
25 | },
26 |
27 | disableDragger(ev) {
28 | this.toggleDragger(0, ev);
29 | },
30 |
31 | toggleDragger(enable, ev) {
32 | const { canvasModel, em } = this;
33 | let { dragger } = this;
34 | const methodCls = enable ? 'add' : 'remove';
35 | this.getCanvas().classList[methodCls](`${this.ppfx}is__grabbing`);
36 |
37 | if (!dragger) {
38 | dragger = new Dragger({
39 | getPosition() {
40 | return {
41 | x: canvasModel.get('x'),
42 | y: canvasModel.get('y')
43 | };
44 | },
45 | setPosition({ x, y }) {
46 | canvasModel.set({ x, y });
47 | },
48 | onStart(ev, dragger) {
49 | em.trigger('canvas:move:start', dragger);
50 | },
51 | onDrag(ev, dragger) {
52 | em.trigger('canvas:move', dragger);
53 | },
54 | onEnd(ev, dragger) {
55 | em.trigger('canvas:move:end', dragger);
56 | }
57 | });
58 | this.dragger = dragger;
59 | }
60 |
61 | enable ? dragger.start(ev) : dragger.stop();
62 | },
63 |
64 | toggleMove(enable) {
65 | const { ppfx } = this;
66 | const methodCls = enable ? 'add' : 'remove';
67 | const methodEv = enable ? 'on' : 'off';
68 | const methodsEv = { on, off };
69 | const canvas = this.getCanvas();
70 | const classes = [`${ppfx}is__grab`];
71 | !enable && classes.push(`${ppfx}is__grabbing`);
72 | classes.forEach(cls => canvas.classList[methodCls](cls));
73 | methodsEv[methodEv](document, 'keyup', this.onKeyUp);
74 | methodsEv[methodEv](canvas, 'mousedown', this.enableDragger);
75 | methodsEv[methodEv](document, 'mouseup', this.disableDragger);
76 | }
77 | };
78 |
--------------------------------------------------------------------------------