Browse Source

Merge branch 'new-rte' into dev

pull/415/head
Artur Arseniev 8 years ago
parent
commit
375435a138
  1. 2
      dist/css/grapes.min.css
  2. 4
      dist/grapes.min.js
  3. 2
      src/canvas/index.js
  4. 8
      src/canvas/view/CanvasView.js
  5. 334
      src/demo.js
  6. 1
      src/dom_components/index.js
  7. 100
      src/dom_components/view/ComponentTextView.js
  8. 142
      src/dom_components/view/ComponentView.js
  9. 61
      src/dom_components/view/ComponentsView.js
  10. 4
      src/editor/index.js
  11. 31
      src/navigator/view/ItemView.js
  12. 22
      src/navigator/view/ItemsView.js
  13. 35
      src/rich_text_editor/config/config.js
  14. 326
      src/rich_text_editor/index.js
  15. 21
      src/rich_text_editor/model/CommandButton.js
  16. 6
      src/rich_text_editor/model/CommandButtons.js
  17. 239
      src/rich_text_editor/model/RichTextEditor.js
  18. 42
      src/rich_text_editor/view/CommandButtonSelectView.js
  19. 16
      src/rich_text_editor/view/CommandButtonView.js
  20. 76
      src/rich_text_editor/view/CommandButtonsView.js
  21. 234
      src/rich_text_editor/view/TextEditorView.js
  22. 41
      src/styles/scss/_gjs_rte.scss
  23. 61
      src/styles/scss/main.scss
  24. 6
      src/utils/extender.js
  25. 2
      test/specs/dom_components/view/ComponentV.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
src/canvas/index.js

@ -106,7 +106,7 @@ module.exports = () => {
},
/**
* Returns element containing canvas tools
* Returns element containing all canvas tools
* @return {HTMLElement}
*/
getToolsEl() {

8
src/canvas/view/CanvasView.js

@ -362,7 +362,6 @@ module.exports = Backbone.View.extend({
</div>
`);
const el = this.el;
const rte = this.em.get('rte');
const toolsEl = el.querySelector(`#${ppfx}tools`);
this.hlEl = el.querySelector(`.${ppfx}highlighter`);
this.badgeEl = el.querySelector(`.${ppfx}badge`);
@ -372,13 +371,6 @@ module.exports = Backbone.View.extend({
this.resizerEl = el.querySelector(`.${ppfx}resizer`);
this.offsetEl = el.querySelector(`.${ppfx}offset-v`);
this.fixedOffsetEl = el.querySelector(`.${ppfx}offset-fixed-v`);
if (rte) {
const rteEl = rte.render();
rteEl.style.pointerEvents = 'all';
toolsEl.appendChild(rteEl);
}
this.toolsEl = toolsEl;
this.el.className = this.className;
return this;

334
src/demo.js

@ -0,0 +1,334 @@
require(['config/require-config'], function() {
require(['grapesjs/main'],function (grapesjs){
var editor = grapesjs.init(
{
noticeOnUnload: 0,
container : '#gjs',
height: '100%',
fromElement: true,
/*
components: [{
type: 'text',
style:{
width:'100px',
height:'100px',
margin: '50px auto',
},
traits: ['title'],
components: [{
type: 'textnode',
content: 'text node row',
},{
type: 'textnode',
content: ', another text node',
},{
type: 'link',
content: 'someLink',
},{
type: 'textnode',
content: " More text node --- ",
}],
}],*/
storageManager:{
autoload: 0,
storeComponents: 1,
storeStyles: 1,
},
commands: {
defaults : [{
id: 'open-github',
run: function(editor, sender){
sender.set('active',false);
window.open('https://github.com/artf/grapesjs','_blank');
}
},{
id: 'undo',
run: function(editor, sender){
sender.set('active',false);
editor.UndoManager.undo(true);
}
},{
id: 'redo',
run: function(editor, sender){
sender.set('active',false);
editor.UndoManager.redo(true);
}
},{
id: 'clean-all',
run: function(editor, sender){
sender.set('active',false);
if(confirm('Are you sure to clean the canvas?')){
var comps = editor.DomComponents.clear();
}
}
}],
},
assetManager: {
storageType : '',
storeOnChange : true,
storeAfterUpload : true,
assets : [
{ type: 'image', src : 'http://placehold.it/350x250/78c5d6/fff/image1.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/459ba8/fff/image2.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/79c267/fff/image3.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/c5d647/fff/image4.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/f28c33/fff/image5.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/e868a2/fff/image6.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/cc4360/fff/image7.jpg', height:350, width:250},
{ type: 'image', src : './img/work-desk.jpg', date: '2015-02-01',height:1080, width:1728},
{ type: 'image', src : './img/phone-app.png', date: '2015-02-01',height:650, width:320},
{ type: 'image', src : './img/bg-gr-v.png', date: '2015-02-01',height:1, width:1728},
]
},
styleManager : {
sectors: [{
name: 'General',
open: false,
buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom'],
},{
name: 'Dimension',
open: false,
buildProps: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'],
},{
name: 'Typography',
open: false,
buildProps: ['font-family', 'font-size', 'font-weight', 'letter-spacing', 'color', 'line-height', 'text-align', 'text-shadow'],
properties: [{
property: 'text-align',
list : [
{value: 'left', className: 'fa fa-align-left'},
{value: 'center', className: 'fa fa-align-center' },
{value: 'right', className: 'fa fa-align-right'},
{value: 'justify', className: 'fa fa-align-justify'}
],
}]
},{
name: 'Decorations',
open: false,
buildProps: ['border-radius-c', 'background-color', 'border-radius', 'border', 'box-shadow', 'background'],
},{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform'],
},{
name: 'Dimension',
open: false,
buildProps: ['margin'],
properties:[{
name: 'Marginnnn',
property: 'margin',
type: 'composite',
properties:[{
name: 'Top',
property: 'margin-top',
},{
name: 'Right',
property: 'margin-right',
},{
name: 'Bottom',
property: 'margin-bottom',
},{
name: 'Left',
property: 'margin-left',
},],
}/*{
name : 'Center blocksss',
property : 'margins',
type : 'select',
defaults : '0',
list : [{
value : '0',
name : 'Normal',
},{
value : '0 auto',
name : 'Center',
}],
}*/],
},{
name: 'Flex',
open: false,
properties: [{
name : 'Flex Container',
property : 'display',
type : 'select',
defaults : 'block',
list : [{
value : 'block',
name : 'Disable',
},{
value : 'flex',
name : 'Enable',
}],
},{
name: 'Flex Parent',
property: 'label-parent-flex',
},{
name : 'Direction',
property : 'flex-direction',
type : 'radio',
defaults : 'row',
list : [{
value : 'row',
name : 'Row',
className : 'icons-flex icon-dir-row',
title : 'Row',
},{
value : 'row-reverse',
name : 'Row reverse',
className : 'icons-flex icon-dir-row-rev',
title : 'Row reverse',
},{
value : 'column',
name : 'Column',
title : 'Column',
className : 'icons-flex icon-dir-col',
},{
value : 'column-reverse',
name : 'Column reverse',
title : 'Column reverse',
className : 'icons-flex icon-dir-col-rev',
}],
},{
name : 'Wrap',
property : 'flex-wrap',
type : 'radio',
defaults : 'nowrap',
list : [{
value : 'nowrap',
title : 'Single line',
},{
value : 'wrap',
title : 'Multiple lines',
},{
value : 'wrap-reverse',
title : 'Multiple lines reverse',
}],
},{
name : 'Justify',
property : 'justify-content',
type : 'radio',
defaults : 'flex-start',
list : [{
value : 'flex-start',
className : 'icons-flex icon-just-start',
title : 'Start',
},{
value : 'flex-end',
title : 'End',
className : 'icons-flex icon-just-end',
},{
value : 'space-between',
title : 'Space between',
className : 'icons-flex icon-just-sp-bet',
},{
value : 'space-around',
title : 'Space around',
className : 'icons-flex icon-just-sp-ar',
},{
value : 'center',
title : 'Center',
className : 'icons-flex icon-just-sp-cent',
}],
},{
name : 'Align',
property : 'align-items',
type : 'radio',
defaults : 'center',
list : [{
value : 'flex-start',
title : 'Start',
className : 'icons-flex icon-al-start',
},{
value : 'flex-end',
title : 'End',
className : 'icons-flex icon-al-end',
},{
value : 'stretch',
title : 'Stretch',
className : 'icons-flex icon-al-str',
},{
value : 'center',
title : 'Center',
className : 'icons-flex icon-al-center',
}],
},{
name: 'Flex Children',
property: 'label-parent-flex',
},{
name: 'Order',
property: 'order',
type: 'integer',
defaults : 0,
min: 0
},{
name : 'Flex',
property : 'flex',
type : 'composite',
properties : [{
name: 'Grow',
property: 'flex-grow',
type: 'integer',
defaults : 0,
min: 0
},{
name: 'Shrink',
property: 'flex-shrink',
type: 'integer',
defaults : 0,
min: 0
},{
name: 'Basis',
property: 'flex-basis',
type: 'integer',
units: ['px','%',''],
unit: '',
defaults : 'auto',
}],
},{
name : 'Align',
property : 'align-self',
type : 'radio',
defaults : 'auto',
list : [{
value : 'auto',
name : 'Auto',
},{
value : 'flex-start',
title : 'Start',
className : 'icons-flex icon-al-start',
},{
value : 'flex-end',
title : 'End',
className : 'icons-flex icon-al-end',
},{
value : 'stretch',
title : 'Stretch',
className : 'icons-flex icon-al-str',
},{
value : 'center',
title : 'Center',
className : 'icons-flex icon-al-center',
}],
}]
}
],
},
}
);
window.editor = editor;
});
});

1
src/dom_components/index.js

@ -162,7 +162,6 @@ module.exports = () => {
// Load dependencies
if (em) {
c.rte = em.get('rte') || '';
c.modal = em.get('Modal') || '';
c.am = em.get('AssetManager') || '';
em.get('Parser').compTypes = componentTypes;

100
src/dom_components/view/ComponentTextView.js

@ -6,63 +6,91 @@ module.exports = ComponentView.extend({
events: {
'dblclick': 'enableEditing',
'change': 'parseRender',
},
initialize(o) {
ComponentView.prototype.initialize.apply(this, arguments);
_.bindAll(this,'disableEditing');
this.disableEditing = this.disableEditing.bind(this);
const model = this.model;
const em = this.em;
this.listenTo(model, 'focus active', this.enableEditing);
this.listenTo(model, 'change:content', this.updateContent);
this.rte = this.config.rte || '';
this.activeRte = null;
this.em = this.config.em;
this.rte = em && em.get('RichTextEditor');
},
/**
* Enable the component to be editable
* @param {Event} e
* Enable element content editing
* @private
* */
enableEditing(e) {
var editable = this.model.get('editable');
if(this.rte && editable) {
enableEditing() {
if (this.rteEnabled) {
return;
}
const editable = this.model.get('editable');
const rte = this.rte;
if (rte && editable) {
try {
this.activeRte = this.rte.attach(this, this.activeRte);
this.rte.focus(this, this.activeRte);
this.activeRte = rte.enable(this, this.activeRte);
} catch (err) {
console.error(err);
}
}
this.rteEnabled = 1;
this.toggleEvents(1);
},
/**
* Disable this component to be editable
* @param {Event}
* Disable element content editing
* @private
* */
disableEditing(e) {
var model = this.model;
var editable = model.get('editable');
disableEditing() {
const model = this.model;
const editable = model.get('editable');
const rte = this.rte;
if(this.rte && editable) {
if (rte && editable) {
try {
this.rte.detach(this, this.activeRte);
rte.disable(this, this.activeRte);
} catch (err) {
console.error(err);
}
var el = this.getChildrenContainer();
// Avoid double content by removing its children components
model.get('components').reset();
model.set('content', el.innerHTML);
}
if(!this.rte.customRte && editable) {
this.parseRender();
const content = this.getChildrenContainer().innerHTML;
const comps = model.get('components');
comps.length && comps.reset();
// If there is a custom RTE the content is just baked staticly
// inside 'content'
if (rte.customRte) {
// Avoid double content by removing its children components
// and force to trigger change
model.set('content', '')
.set('content', content);
} else {
const clean = model => {
model.set({
editable: 0,
highlightable: 0,
removable: 0,
draggable: 0,
copyable: 0,
toolbar: '',
})
model.get('components').each(model => clean(model));
}
// Avoid re-render on reset with silent option
model.set('content', '');
comps.add(content);
comps.each(model => clean(model));
comps.trigger('resetNavigator');
}
}
this.rteEnabled = 0;
this.toggleEvents();
},
@ -75,26 +103,6 @@ module.exports = ComponentView.extend({
e.stopPropagation();
},
/**
* Parse content and re-render it
* @private
*/
parseRender() {
var el = this.getChildrenContainer();
var comps = this.model.get('components');
var opts = {silent: true};
// Avoid re-render on reset with silent option
comps.reset(null, opts);
comps.add(el.innerHTML, opts);
this.model.set('content', '');
this.render();
// As the reset was in silent mode I need to notify
// the navigator about the change
comps.trigger('resetNavigator');
},
/**
* Enable/Disable events
* @param {Boolean} enable

142
src/dom_components/view/ComponentView.js

@ -1,5 +1,4 @@
const ComponentsView = require('./ComponentsView');
const $ = Backbone.$;
module.exports = Backbone.View.extend({
@ -12,42 +11,38 @@ module.exports = Backbone.View.extend({
},
initialize(opt) {
var model = this.model;
const model = this.model;
this.opts = opt || {};
this.config = this.opts.config || {};
this.em = this.config.em || '';
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.components = model.get('components');
this.attr = model.get("attributes");
this.attr = model.get('attributes');
this.classe = this.attr.class || [];
const $el = this.$el;
const classes = model.get('classes');
this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model, 'change:style', this.updateStyle);
this.listenTo(model, 'change:attributes', this.updateAttributes);
this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:state', this.updateState);
this.listenTo(model, 'change:script', this.render);
this.listenTo(model, 'change', this.handleChange);
this.listenTo(model.get('classes'), 'add remove change', this.updateClasses);
const $el = this.$el;
const el = this.el;
const em = this.em;
this.listenTo(classes, 'add remove change', this.updateClasses);
$el.data('model', model);
$el.data('collection', this.components);
$el.data('collection', model.get('components'));
model.view = this;
if (em) {
em.data(el, 'model', model);
em.data(el, 'collection', model.get('components'));
}
if(model.get('classes').length)
this.importClasses();
classes.length && this.importClasses();
this.init();
},
remove() {
Backbone.View.prototype.remove.apply(this);
const children = this.childrenView;
children && children.stopListening();
},
/**
* Initialize callback
*/
@ -136,6 +131,45 @@ module.exports = Backbone.View.extend({
}
},
/**
* Update highlight attribute
* @private
* */
updateHighlight() {
const hl = this.model.get('highlightable');
this.setAttribute('data-highlightable', hl ? 1 : '');
},
/**
* Update style attribute
* @private
* */
updateStyle() {
this.setAttribute('style', this.getStyleString());
},
/**
* Update classe attribute
* @private
* */
updateClasses() {
const str = this.model.get('classes').pluck('name').join(' ');
this.setAttribute('class', str);
// Regenerate status class
this.updateStatus();
},
/**
* Update single attribute
* @param {[type]} name [description]
* @param {[type]} value [description]
*/
setAttribute(name, value) {
const el = this.$el;
value ? el.attr(name, value) : el.removeAttr(name);
},
/**
* Get classes from attributes.
* This method is called before initialize
@ -157,36 +191,19 @@ module.exports = Backbone.View.extend({
* @private
* */
updateAttributes() {
var model = this.model;
var attributes = {},
attr = model.get("attributes");
for(var key in attr) {
if (key && attr.hasOwnProperty(key)) {
attributes[key] = attr[key];
}
}
// Update src
if(model.get('src'))
attributes.src = model.get('src');
if(model.get('highlightable'))
attributes['data-highlightable'] = 1;
const model = this.model;
const attrs = {}
const attr = model.get('attributes');
const src = model.get('src');
var styleStr = this.getStyleString();
if(styleStr)
attributes.style = styleStr;
this.$el.attr(attributes);
},
for (let key in attr) {
attrs[key] = attr[key];
}
/**
* Update style attribute
* @private
* */
updateStyle() {
this.$el.attr('style', this.getStyleString());
src && (attrs.src = src);
this.$el.attr(attrs);
this.updateHighlight();
this.updateStyle();
},
/**
@ -213,27 +230,6 @@ module.exports = Backbone.View.extend({
return style;
},
/**
* Update classe attribute
* @private
* */
updateClasses() {
var str = '';
this.model.get('classes').each(model => {
str += model.get('name') + ' ';
});
str = str.trim();
if(str)
this.$el.attr('class', str);
else
this.$el.removeAttr('class');
// Regenerate status class
this.updateStatus();
},
/**
* Reply to event call
* @param object Event that generated the request
@ -309,15 +305,16 @@ module.exports = Backbone.View.extend({
* @private
*/
renderChildren() {
var view = new ComponentsView({
const container = this.getChildrenContainer();
const view = new ComponentsView({
collection: this.model.get('components'),
config: this.config,
componentTypes: this.opts.componentTypes,
});
var container = this.getChildrenContainer();
var childNodes = view.render($(container)).el.childNodes;
childNodes = Array.prototype.slice.call(childNodes);
view.render(container);
this.childrenView = view;
const childNodes = Array.prototype.slice.call(view.el.childNodes);
for (var i = 0, len = childNodes.length ; i < len; i++) {
container.appendChild(childNodes.shift());
@ -348,7 +345,6 @@ module.exports = Backbone.View.extend({
render() {
this.renderAttributes();
var model = this.model;
this.updateContent();
this.renderChildren();
this.updateScript();

61
src/dom_components/view/ComponentsView.js

@ -1,12 +1,13 @@
var Backbone = require('backbone');
import { isUndefined } from 'underscore'
module.exports = Backbone.View.extend({
initialize(o) {
this.opts = o || {};
this.config = o.config || {};
this.listenTo( this.collection, 'add', this.addTo );
this.listenTo( this.collection, 'reset', this.render );
const coll = this.collection;
this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.resetChildren);
},
/**
@ -17,7 +18,7 @@ module.exports = Backbone.View.extend({
* @private
* */
addTo(model) {
var i = this.collection.indexOf(model);
var i = this.collection.indexOf(model);
this.addToCollection(model, null, i);
var em = this.config.em;
@ -42,7 +43,6 @@ module.exports = Backbone.View.extend({
this.compView = require('./ComponentView');
var fragment = fragmentEl || null,
viewObject = this.compView;
//console.log('Add to collection', model, 'Index',i);
var dt = this.opts.componentTypes;
@ -66,44 +66,47 @@ module.exports = Backbone.View.extend({
if(view.model.get('type') == 'textnode')
rendered = document.createTextNode(view.model.get('content'));
if(fragment){
if (fragment) {
fragment.appendChild(rendered);
}else{
var p = this.$parent;
var pc = p.children;
if(typeof index != 'undefined'){
var method = 'before';
} else {
const parent = this.parentEl;
const children = parent.childNodes;
if (!isUndefined(index)) {
const lastIndex = children.length == index;
// If the added model is the last of collection
// need to change the logic of append
if(pc && p.children().length == index){
if (lastIndex) {
index--;
method = 'after';
}
// In case the added is new in the collection index will be -1
if(index < 0) {
p.append(rendered);
}else {
if(pc) {
p.children().eq(index)[method](rendered);
}
if (lastIndex || !children.length) {
parent.appendChild(rendered);
} else {
parent.insertBefore(rendered, children[index]);
}
}else{
p.append(rendered);
} else {
parent.appendChild(rendered);
}
}
return rendered;
},
render($p) {
var fragment = document.createDocumentFragment();
this.$parent = $p || this.$el;
this.$el.empty();
this.collection.each(function(model){
this.addToCollection(model, fragment);
},this);
this.$el.append(fragment);
resetChildren() {
this.parentEl.innerHTML = '';
this.collection.each(model => this.addToCollection(model));
},
render(parent) {
const el = this.el;
const frag = document.createDocumentFragment();
this.parentEl = parent || this.el;
this.collection.each(model => this.addToCollection(model, frag));
el.innerHTML = '';
el.appendChild(frag);
return this;
}

4
src/editor/index.js

@ -54,6 +54,8 @@
* * `storage:end` - After the storage request is ended
* * `storage:error` - On any error on storage request, passes the error as an argument
* * `selector:add` - Triggers when a new selector/class is created
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
* * `canvasScroll` - Triggered when the canvas is scrolled
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
@ -207,7 +209,7 @@ module.exports = config => {
* @property {RichTextEditor}
* @private
*/
RichTextEditor: em.get('rte'),
RichTextEditor: em.get('RichTextEditor'),
/**
* @property {Utils}

31
src/navigator/view/ItemView.js

@ -4,16 +4,25 @@ var ItemsView;
module.exports = Backbone.View.extend({
events: {
'mousedown [data-toggle-move]': 'startSort',
'click [data-toggle-visible]': 'toggleVisibility',
'click [data-toggle-select]': 'handleSelect',
'click [data-toggle-open]': 'toggleOpening',
'click [data-toggle-edit]': 'handleEdit',
'focusout input': 'handleEditEnd',
},
template: _.template(`
<% if (hidable) { %>
<i id="<%= prefix %>btn-eye" class="btn fa fa-eye <%= (visible ? '' : 'fa-eye-slash') %>"></i>
<i id="<%= prefix %>btn-eye" class="btn fa fa-eye <%= (visible ? '' : 'fa-eye-slash') %>" data-toggle-visible></i>
<% } %>
<div class="<%= prefix %>title-c">
<div class="<%= prefix %>title <%= addClass %>" style="padding-left: <%= 42 + level * 10 %>px">
<div class="<%= prefix %>title <%= addClass %>" style="padding-left: <%= 42 + level * 10 %>px" data-toggle-select>
<div class="<%= prefix %>title-inn">
<i class="fa fa-pencil <%= editBtnCls %>"></i>
<i id="<%= prefix %>caret" class="fa fa-chevron-right <%= caretCls %>"></i>
<i class="fa fa-pencil <%= editBtnCls %>" data-toggle-edit></i>
<i id="<%= prefix %>caret" class="fa fa-chevron-right <%= caretCls %>" data-toggle-open></i>
<%= icon %>
<input class="<%= ppfx %>no-app <%= inputNameCls %>" value="<%= title %>" readonly>
</div>
@ -22,7 +31,7 @@ module.exports = Backbone.View.extend({
<div id="<%= prefix %>counter"><%= (count ? count : '') %></div>
<div id="<%= prefix %>move">
<div id="<%= prefix %>move" data-toggle-move>
<i class="fa fa-arrows"></i>
</div>
@ -50,20 +59,8 @@ module.exports = Backbone.View.extend({
this.inputNameCls = `${ppfx}nav-comp-name`;
this.caretCls = `${ppfx}nav-item-caret`;
this.titleCls = `${pfx}title`;
this.events = {};
this.events[`click #${pfx}btn-eye`] = 'toggleVisibility';
this.events['click .' + this.caretCls] = 'toggleOpening';
this.events['click .' + this.titleCls] = 'handleSelect';
this.events['click .' + this.editBtnCls] = 'handleEdit';
this.events['blur .' + this.inputNameCls] = 'handleEditEnd';
this.$el.data('model', model);
this.$el.data('collection', components);
if(o.config.sortable)
this.events['mousedown #'+pfx+'move'] = 'startSort';
this.delegateEvents();
},
/**

22
src/navigator/view/ItemsView.js

@ -14,7 +14,7 @@ module.exports = Backbone.View.extend({
this.parent = o.parent;
this.listenTo(this.collection, 'add', this.addTo);
this.listenTo(this.collection, 'reset resetNavigator', this.render);
this.className = this.pfx + 'items';
this.className = this.pfx + 'items';
if (config.sortable && !this.opt.sorter) {
var pfx = this.pfx;
@ -65,6 +65,10 @@ module.exports = Backbone.View.extend({
var fragment = fragmentEl || null;
var viewObject = ItemView;
if(!this.isCountable(model, this.config.hideTextnode)) {
return;
}
var view = new viewObject({
level,
model,
@ -115,17 +119,11 @@ module.exports = Backbone.View.extend({
},
render() {
var fragment = document.createDocumentFragment();
this.$el.empty();
this.collection.each(function(model) {
if(!this.isCountable(model, this.config.hideTextnode))
return;
this.addToCollection(model, fragment);
}, this);
this.$el.append(fragment);
this.$el.attr('class', _.result(this, 'className'));
const frag = document.createDocumentFragment();
this.el.innerHTML = '';
this.collection.each(model => this.addToCollection(model, frag));
this.el.appendChild(frag);
this.$el.attr('class', this.className);
return this;
}
});

35
src/rich_text_editor/config/config.js

@ -1,40 +1,11 @@
module.exports = {
stylePrefix : 'rte-',
toolbarId : 'toolbar',
// If true, moves the toolbar below the element when the top canvas
// edge is reached
adjustToolbar: 1,
// Default toolbar commands
commands : [{
command: 'bold',
title: 'Bold',
class: 'fa fa-bold',
},{
command: 'italic',
title: 'Italic',
class: 'fa fa-italic',
},{
command: 'underline',
title: 'Underline',
class: 'fa fa-underline',
},{
command: 'strikethrough',
title: 'Strikethrough',
class: 'fa fa-strikethrough',
group: 'format'
},{
command: 'insertHTML',
title: 'Link',
class: 'fa fa-link',
args: '<a class="link" href="">${content}</a>',
}/*,{
command: 'fontSize',
options: [
{name: 'Huge', value: '7'},
{name: 'Normal', value: '5'},
{value: '1'}
]
}*/],
// Default RTE actions
actions: ['bold', 'italic', 'underline', 'strikethrough', 'link'],
};

326
src/rich_text_editor/index.js

@ -3,10 +3,11 @@
* * [get](#get)
* * [getAll](#getall)
* * [remove](#remove)
* * [getToolbarEl](#gettoolbarel)
*
* This module allows to customize the toolbar of the Rich Text Editor and use commands from the HTML Editing APIs.
* For more info about HTML Editing APIs check here:
* https://developer.mozilla.org/it/docs/Web/API/Document/execCommand
* https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
*
* It's highly recommended to keep this toolbar as small as possible, especially from styling commands (eg. 'fontSize')
* and leave this task to the Style Manager.
@ -16,22 +17,15 @@
* ```js
* var rte = editor.RichTextEditor;
* ```
* Complete list of commands
* https://developer.mozilla.org/it/docs/Web/API/Document/execCommand
* http://www.quirksmode.org/dom/execCommand.html
* @module RichTextEditor
*/
const $ = Backbone.$;
import RichTextEditor from './model/RichTextEditor';
import {on, off} from 'utils/mixins'
module.exports = () => {
var c = {},
defaults = require('./config/config'),
rte = require('./view/TextEditorView'),
CommandButtons = require('./model/CommandButtons'),
CommandButtonsView = require('./view/CommandButtonsView');
const $ = require('backbone').$;
var tlbPfx, toolbar, commands;
var mainSelf;
let config = {};
const defaults = require('./config/config');
let toolbar, actions, lastEl, globalRte;
return {
@ -42,77 +36,166 @@ module.exports = () => {
* @type {String}
* @private
*/
name: 'rte',
name: 'RichTextEditor',
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @param {Object} opts Options
* @private
*/
init(config) {
mainSelf = this;
c = config || {};
for (var name in defaults) {
if (!(name in c))
c[name] = defaults[name];
init(opts = {}) {
config = opts;
for (let name in defaults) {
if (!(name in config)) {
config[name] = defaults[name];
}
}
var ppfx = c.pStylePrefix;
if(ppfx)
c.stylePrefix = ppfx + c.stylePrefix;
const ppfx = config.pStylePrefix;
tlbPfx = c.stylePrefix;
commands = new CommandButtons(c.commands);
toolbar = new CommandButtonsView({
collection: commands,
config: c,
});
if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix;
}
this.pfx = config.stylePrefix;
actions = config.actions || [];
toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar`;
globalRte = this.initRte(document.createElement('div'));
//Avoid closing on toolbar clicking
on(toolbar, 'mousedown', e => e.stopPropagation());
return this;
},
/**
* Post render callback
* @param {View} ev
* @private
*/
postRender(ev) {
const canvas = ev.model.get('Canvas');
toolbar.style.pointerEvents = 'all';
canvas.getToolsEl().appendChild(toolbar);
},
/**
* Init the built-in RTE
* @param {HTMLElement} el
* @return {RichTextEditor}
* @private
*/
initRte(el) {
const pfx = this.pfx;
const actionbarContainer = toolbar;
const actionbar = this.actionbar;
const actions = this.actions || config.actions;
const classes = {
actionbar: `${pfx}actionbar`,
button: `${pfx}action`,
active: `${pfx}active`,
};
const rte = new RichTextEditor({
el,
classes,
actions,
actionbar,
actionbarContainer,
});
globalRte && globalRte.setEl(el);
if (rte.actionbar) {
this.actionbar = rte.actionbar;
}
if (rte.actions) {
this.actions = rte.actions;
}
return rte;
},
/**
* Add a new command to the toolbar
* @param {string} command Command name
* @param {Object} opts Command options
* @return {Model} Added command
* Add a new action to the built-in RTE toolbar
* @param {string} name Action name
* @param {Object} action Action options
* @example
* var cm = rte.add('bold', {
* title: 'Make bold',
* class: 'fa fa-bold',
* rte.add('bold', {
* icon: '<b>B</b>',
* attributes: {title: 'Bold',}
* result: rte => rte.exec('bold')
* });
* // With arguments
* var cm = rte.add('fontSize', {
* title: 'Font size',
* options: [
* {name: 'Big', value: 5},
* {name: 'Normal', value: 3},
* {name: 'Small', value: 1}
* ]
* rte.add('link', {
* icon: document.getElementById('t'),
* // Bind the 'result' on 'onclick' listener
* event: 'click',
* attributes: {title: 'Link',}
* // Example on it's easy to wrap a selected content
* result: rte => rte.insertHTML(`<a href="#">${rte.selection()}</a>`)
* });
*/
add(command, opts) {
var obj = opts || {};
obj.command = command;
return commands.add(obj);
add(name, action = {}) {
action.name = name;
globalRte.addAction(action, {sync: 1});
},
/**
* Get the command by its name
* @param {string} command Command name
* @return {Model}
* Get the action by its name
* @param {string} name Action name
* @return {Object}
* @example
* var cm = rte.get('fontSize');
* const action = rte.get('bold');
* // {name: 'bold', ...}
*/
get(command) {
return commands.where({command})[0];
get(name) {
let result;
globalRte.getActions().forEach(action => {
if (action.name == name) {
result = action;
}
});
return result;
},
/**
* Returns the collection of commands
* @return {Collection}
* Get all actions
* @return {Array}
*/
getAll() {
return commands;
return globalRte.getActions();
},
/**
* Remove the action from the toolbar
* @param {string} name
* @return {Object} Removed action
* @example
* const action = rte.remove('bold');
* // {name: 'bold', ...}
*/
remove(name) {
const actions = this.getAll();
const action = this.get(name);
if (action) {
const btn = action.btn;
const index = actions.indexOf(action);
btn.parentNode.removeChild(btn);
actions.splice(index, 1);
}
return action;
},
/**
* Get the toolbar element
* @return {HTMLElement}
*/
getToolbarEl() {
return toolbar;
},
/**
@ -120,56 +203,47 @@ module.exports = () => {
* @private
*/
udpatePosition() {
var u = 'px';
var canvas = c.em.get('Canvas');
var pos = canvas.getTargetToElementDim(toolbar.el, this.lastEl, {
const un = 'px';
const canvas = config.em.get('Canvas');
const pos = canvas.getTargetToElementDim(toolbar, lastEl, {
event: 'rteToolbarPosUpdate',
});
if (c.adjustToolbar) {
if (config.adjustToolbar) {
// Move the toolbar down when the top canvas edge is reached
if (pos.top <= pos.canvasTop) {
pos.top = pos.elementTop + pos.elementHeight;
}
}
var toolbarStyle = toolbar.el.style;
toolbarStyle.top = pos.top + u;
toolbarStyle.left = pos.left + u;
const toolbarStyle = toolbar.style;
toolbarStyle.top = pos.top + un;
toolbarStyle.left = pos.left + un;
},
/**
* Bind rich text editor to the element
* @param {View} view
* Enable rich text editor on the element
* @param {View} view Component view
* @param {Object} rte The instance of already defined RTE
* @private
* */
attach(view, rte) {
// lastEl will be used to place the RTE toolbar
this.lastEl = view.el;
var el = view.getChildrenContainer();
var customRte = this.customRte;
enable(view, rte) {
lastEl = view.el;
const em = config.em;
const el = view.getChildrenContainer();
const customRte = this.customRte;
// If a custom RTE is defined
if (customRte) {
rte = customRte.enable(el, rte);
} else {
$(el).wysiwyg({}).focus();
}
this.show();
toolbar.style.display = '';
rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable();
if(c.em) {
if (em) {
setTimeout(this.udpatePosition.bind(this), 0);
c.em.off('change:canvasOffset', this.udpatePosition, this);
c.em.on('change:canvasOffset', this.udpatePosition, this);
// Update position on scrolling
c.em.off('canvasScroll', this.udpatePosition, this);
c.em.on('canvasScroll', this.udpatePosition, this);
const event = 'change:canvasOffset canvasScroll';
em.off(event, this.udpatePosition, this);
em.on(event, this.udpatePosition, this);
em.trigger('rte:enable', view, rte);
}
//Avoid closing edit mode clicking on toolbar
toolbar.$el.on('mousedown', this.disableProp);
return rte;
},
@ -179,78 +253,22 @@ module.exports = () => {
* @param {Object} rte The instance of already defined RTE
* @private
* */
detach(view, rte) {
var customRte = this.customRte;
disable(view, rte) {
const em = config.em;
const customRte = this.customRte;
const style = toolbar.style;
var el = view.getChildrenContainer();
if (customRte) {
view.model.set('content', el.innerHTML);
customRte.disable(el, rte);
} else {
$(el).wysiwyg('destroy');
}
this.hide();
toolbar.$el.off('mousedown', this.disableProp);
},
/**
* Unbind rich text editor from the element
* @param {View} view
* @param {Object} rte The instance of already defined RTE
* @private
* */
focus(view, rte) {
var customRte = this.customRte;
var el = view.getChildrenContainer();
if (customRte) {
if(customRte.focus)
customRte.focus(el, rte);
customRte.disable(el, rte);
} else {
this.attach(view);
rte.disable();
}
},
/**
* Show the toolbar
* @private
* */
show() {
var toolbarStyle = toolbar.el.style;
toolbarStyle.display = "block";
style.display = 'none';
style.top = 0;
style.left = 0;
em && em.trigger('rte:disable', view, rte);
},
/**
* Hide the toolbar
* @private
* */
hide() {
toolbar.el.style.display = "none";
},
/**
* Isolate the disable propagation method
* @private
* */
disableProp(e) {
e.stopPropagation();
},
/**
* Return toolbar element
* @return {HTMLElement}
* @private
*/
getToolbarEl() {
return toolbar.el;
},
/**
* Render toolbar
* @return {HTMLElement}
* @private
*/
render() {
return toolbar.render().el;
}
};
};

21
src/rich_text_editor/model/CommandButton.js

@ -1,21 +0,0 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
idAttribute: 'command',
defaults: {
command: '',
type: '',
title: '',
class: '',
options: [],
},
initialize() {
var opts = this.get('options');
if(opts.length)
this.set('type', 'select');
},
});

6
src/rich_text_editor/model/CommandButtons.js

@ -1,6 +0,0 @@
var Backbone = require('backbone');
var CommandButton = require('./CommandButton');
module.exports = Backbone.Collection.extend({
model: CommandButton,
});

239
src/rich_text_editor/model/RichTextEditor.js

@ -0,0 +1,239 @@
// The initial version of this RTE was borrowed from https://github.com/jaredreich/pell
// and adapted to the GrapesJS's need
import {on, off} from 'utils/mixins'
const RTE_KEY = '_rte';
const defActions = {
bold: {
name: 'bold',
icon: '<b>B</b>',
attributes: {title: 'Bold'},
result: (rte) => rte.exec('bold')
},
italic: {
name: 'italic',
icon: '<i>I</i>',
attributes: {title: 'Italic'},
result: (rte) => rte.exec('italic')
},
underline: {
name: 'underline',
icon: '<u>U</u>',
attributes: {title: 'Underline'},
result: (rte) => rte.exec('underline')
},
strikethrough: {
name: 'strikethrough',
icon: '<strike>S</strike>',
attributes: {title: 'Strike-through'},
result: (rte) => rte.exec('strikeThrough')
},
link: {
icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
name: 'link',
attributes: {
style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link',
},
result: (rte) => rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`)
}
}
export default class RichTextEditor {
constructor(settings = {}) {
const el = settings.el;
if (el[RTE_KEY]) {
return el[RTE_KEY];
}
el[RTE_KEY] = this;
this.setEl(el);
this.updateActiveActions = this.updateActiveActions.bind(this);
const settAct = settings.actions || [];
settAct.forEach((action, i) => {
if (typeof action === 'string') {
action = defActions[action];
} else if (defActions[action.name]) {
action = {...defActions[action.name], ...action};
}
settAct[i] = action;
});
const actions = settAct.length ? settAct :
Object.keys(defActions).map(action => defActions[action])
settings.classes = { ...{
actionbar: 'actionbar',
button: 'action',
active: 'active',
}, ...settings.classes};
const classes = settings.classes;
let actionbar = settings.actionbar;
this.actionbar = actionbar;
this.settings = settings;
this.classes = classes;
this.actions = actions;
if (!actionbar) {
const actionbarCont = settings.actionbarContainer;
actionbar = document.createElement('div');
actionbar.className = classes.actionbar;
actionbarCont.appendChild(actionbar);
this.actionbar = actionbar;
actions.forEach(action => this.addAction(action))
}
settings.styleWithCSS && this.exec('styleWithCSS');
this.syncActions();
return this;
}
setEl(el) {
this.el = el;
this.doc = el.ownerDocument;
}
updateActiveActions() {
this.getActions().forEach(action => {
const btn = action.btn;
const active = this.classes.active;
btn.className = btn.className.replace(active, '').trim();
if (this.doc.queryCommandState(action.name)) {
btn.className += ` ${active}`;
}
})
}
enable() {
if (this.enabled) {
return this;
}
this.actionbarEl().style.display = '';
this.el.contentEditable = true;
on(this.el, 'mouseup keyup', this.updateActiveActions)
this.syncActions();
this.updateActiveActions();
this.el.focus();
this.enabled = 1;
return this;
}
disable() {
this.actionbarEl().style.display = 'none';
this.el.contentEditable = false;
off(this.el, 'mouseup keyup', this.updateActiveActions);
this.enabled = 0;
return this;
}
/**
* Sync actions with the current RTE
*/
syncActions() {
this.getActions().forEach(action => {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this);
this.updateActiveActions();
};
})
}
/**
* Add new action to the actionbar
* @param {Object} action
* @param {Object} [opts={}]
*/
addAction(action, opts = {}) {
const sync = opts.sync;
const btn = document.createElement('span');
const icon = action.icon;
const attr = action.attributes || {};
btn.className = this.classes.button;
action.btn = btn;
for (let key in attr) {
btn.setAttribute(key, attr[key]);
}
if (typeof icon == 'string') {
btn.innerHTML = icon;
} else {
btn.appendChild(icon);
}
this.actionbarEl().appendChild(btn);
if (sync) {
this.actions.push(action);
this.syncActions();
}
}
/**
* Get the array of current actions
* @return {Array}
*/
getActions() {
return this.actions;
}
/**
* Returns the Selection instance
* @return {Selection}
*/
selection() {
return this.doc.getSelection()
}
/**
* Execute the command
* @param {string} command Command name
* @param {any} [value=null Command's arguments
*/
exec(command, value = null) {
this.doc.execCommand(command, false, value);
}
/**
* Get the actionbar element
* @return {HTMLElement}
*/
actionbarEl() {
return this.actionbar;
}
/**
* Set custom HTML to the selection, useful as the default 'insertHTML' command
* doesn't work in the same way on all browsers
* @param {string} value HTML string
*/
insertHTML(value) {
let lastNode;
const doc = this.doc;
const sel = doc.getSelection();
if (sel && sel.rangeCount) {
const node = doc.createElement('div');
const range = sel.getRangeAt(0);
range.deleteContents();
node.innerHTML = value;
Array.prototype.slice.call(node.childNodes).forEach(nd => {
range.insertNode(nd);
lastNode = nd;
})
sel.removeAllRanges();
sel.addRange(range);
this.el.focus();
}
}
}

42
src/rich_text_editor/view/CommandButtonSelectView.js

@ -1,42 +0,0 @@
const CommandButtonView = require('./CommandButtonView');
const $ = Backbone.$;
module.exports = CommandButtonView.extend({
initialize(o, config) {
CommandButtonView.prototype.initialize.apply(this, arguments);
},
getInput() {
var m = this.model;
if(!this.input){
var cmd = m.get('command');
var input = '<select data-edit="' + cmd +'">';
var opts = m.get('options');
var label = m.get('title') || m.get('command');
input += '<option>' + label + '</option>';
for(var i = 0, len = opts.length; i < len; i++){
var opt = opts[i];
var value = opt.value;
var name = opt.name || value;
input += '<option value="' + value + '">' + name + '</option>';
}
input += '</select>';
this.input = $(input);
}
return this.input;
},
getInputCont() {
var input = this.getInput();
var pfx = this.ppfx;
var cont = $('<div class="'+pfx+'field '+pfx+'select"><div class="'+pfx+'sel-arrow"><div class="'+pfx+'d-s-arrow"></div></div></div>');
return cont.append(input);
},
render(...args) {
CommandButtonView.prototype.render.apply(this, args);
this.$el.html(this.getInputCont());
return this;
}
});

16
src/rich_text_editor/view/CommandButtonView.js

@ -1,16 +0,0 @@
var Backbone = require('backbone');
module.exports = Backbone.View.extend({
tagName: 'a',
initialize(o, config) {
this.config = config || {};
this.ppfx = this.config.pStylePrefix || '';
this.className = this.config.stylePrefix + 'btn ' + this.model.get('class');
},
render() {
this.$el.addClass(this.className);
return this;
}
});

76
src/rich_text_editor/view/CommandButtonsView.js

@ -1,76 +0,0 @@
var Backbone = require('backbone');
var CommandButtonView = require('./CommandButtonView');
var CommandButtonSelectView = require('./CommandButtonSelectView');
module.exports = Backbone.View.extend({
attributes : {
'data-role': 'editor-toolbar',
},
initialize(o) {
this.config = o.config || {};
var pfx = this.config.stylePrefix || '';
this.id = pfx + this.config.toolbarId;
this.listenTo(this.collection, 'add', this.addTo);
this.$el.data('helper', 1);
},
/**
* Add new model to the collection
* @param {Model} model
* @private
* */
addTo(model) {
this.add(model);
},
/**
* Render new model inside the view
* @param {Model} model
* @param {Object} fragment Fragment collection
* @private
* */
add(model, fragment) {
var frag = fragment || null;
var viewObj = CommandButtonView;
switch (model.get('type')) {
case 'select':
viewObj = CommandButtonSelectView;
break;
}
var args = model.get('args');
var attrs = {
'title': model.get('title'),
'data-edit': model.get('command'),
};
if(args)
attrs['data-args'] = args;
var view = new viewObj({
model,
attributes: attrs,
}, this.config);
var rendered = view.render().el;
if(frag)
frag.appendChild(rendered);
else
this.$el.append(rendered);
},
render() {
var frag = document.createDocumentFragment();
this.$el.empty();
this.collection.each(function(model){
this.add(model, frag);
}, this);
this.$el.append(frag);
this.$el.attr('id', this.id );
return this;
}
});

234
src/rich_text_editor/view/TextEditorView.js

@ -1,234 +0,0 @@
const Backbone = require('backbone');
const $ = Backbone.$;
var readFileIntoDataUrl = fileInfo => {
var loader = $.Deferred(),
fReader = new FileReader();
fReader.onload = e => {
loader.resolve(e.target.result);
};
fReader.onerror = loader.reject;
fReader.onprogress = loader.notify;
fReader.readAsDataURL(fileInfo);
return loader.promise();
};
$.fn.cleanHtml = function () {
var html = $(this).html();
return html && html.replace(/(<br>|\s|<div><br><\/div>|&nbsp;)*$/, '');
};
$.fn.wysiwyg = function (userOptions) {
var editor = this,
selectedRange,
options,
toolbarBtnSelector,
updateToolbar = () => {
var actCls = options.activeToolbarClass;
if (actCls) {
$(options.toolbarSelector).find(toolbarBtnSelector).each(function () {
var el = $(this);
var command = el.data(options.commandRole);
var doc = editor.get(0).ownerDocument;
if (doc.queryCommandState(command)) {
el.addClass(actCls);
} else {
el.removeClass(actCls);
}
});
}
},
execCommand = (commandWithArgs, valueArg) => {
var commandArr = commandWithArgs.split(' '),
command = commandArr.shift(),
args = commandArr.join(' ') + (valueArg || '');
//document.execCommand("insertHTML", false, "<span class='own-class'>"+ document.getSelection()+"</span>");
editor.get(0).ownerDocument.execCommand("styleWithCSS", false, true);
editor.get(0).ownerDocument.execCommand(command, 0, args);
updateToolbar();
editor.trigger('change');
},
/*
bindHotkeys = function (hotKeys) {
$.each(hotKeys, function (hotkey, command) {
editor.keydown(hotkey, function (e) {
if (editor.attr('contenteditable') && editor.is(':visible')) {
e.preventDefault();
e.stopPropagation();
execCommand(command);
}
}).keyup(hotkey, function (e) {
if (editor.attr('contenteditable') && editor.is(':visible')) {
e.preventDefault();
e.stopPropagation();
}
});
});
},
*/
getCurrentRange = () => {
var sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
return sel.getRangeAt(0);
}
},
saveSelection = () => {
selectedRange = getCurrentRange();
},
restoreSelection = () => {
var selection = window.getSelection();
if (selectedRange) {
try {
selection.removeAllRanges();
} catch (ex) {
document.body.createTextRange().select();
document.selection.empty();
}
selection.addRange(selectedRange);
}
},
insertFiles = files => {
editor.focus();
$.each(files, (idx, fileInfo) => {
if (/^image\//.test(fileInfo.type)) {
$.when(readFileIntoDataUrl(fileInfo)).done(dataUrl => {
execCommand('insertimage', dataUrl);
}).fail(e => {
options.fileUploadError("file-reader", e);
});
} else {
options.fileUploadError("unsupported-file-type", fileInfo.type);
}
});
},
markSelection = (input, color) => {
restoreSelection();
if (document.queryCommandSupported('hiliteColor')) {
document.execCommand('hiliteColor', 0, color || 'transparent');
}
saveSelection();
input.data(options.selectionMarker, color);
},
bindToolbar = (toolbar, options) => {
toolbar.find(toolbarBtnSelector).off('click').on('click',function () {
restoreSelection();
//editor.focus(); // cause defocus on selects
var doc = editor.get(0).ownerDocument;
var el = $(this);
var comm = el.data(options.commandRole);
var args = el.data('args');
if(args){
args = args.replace('${content}', doc.getSelection());
execCommand(comm, args);
}else{
doc.execCommand(comm);
}
saveSelection();
});
toolbar.find('[data-toggle=dropdown]').on('click', restoreSelection);
var dName = '[data-' + options.commandRole + ']';
toolbar.find('select'+dName).on('webkitspeechchange change', function(){
var newValue = this.value;
restoreSelection();
if (newValue) {
editor.focus();
execCommand($(this).data(options.commandRole), newValue);
}
saveSelection();
});
toolbar.find('input[type=text]'+dName,', select'+dName).on('webkitspeechchange change', function () {
var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */
this.value = '';
restoreSelection();
if (newValue) {
editor.focus();
execCommand($(this).data(options.commandRole), newValue);
}
saveSelection();
}).on('focus', function () {
var input = $(this);
if (!input.data(options.selectionMarker)) {
markSelection(input, options.selectionColor);
input.focus();
}
}).on('blur', function () {
var input = $(this);
if (input.data(options.selectionMarker)) {
markSelection(input, false);
}
});
toolbar.find('input[type=file][data-' + options.commandRole + ']').on('change', function () {
restoreSelection();
if (this.type === 'file' && this.files && this.files.length > 0) {
insertFiles(this.files);
}
saveSelection();
this.value = '';
});
},
initFileDrops = () => {
editor.on('dragenter dragover', false)
.on('drop', e => {
var dataTransfer = e.originalEvent.dataTransfer;
e.stopPropagation();
e.preventDefault();
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
insertFiles(dataTransfer.files);
}
});
};
/** Disable the editor
* @date 2015-03-19 */
if(typeof userOptions=='string' && userOptions=='destroy'){
editor.attr('contenteditable', false).off('mouseup keyup mouseout dragenter dragover');
$(window).off('touchend');
return this;
}
options = $.extend({}, $.fn.wysiwyg.defaults, userOptions);
var dName = '[data-' + options.commandRole + ']';
toolbarBtnSelector = 'a'+dName+',button'+dName+',input[type=button]'+dName+', select'+dName;
//bindHotkeys(options.hotKeys);
if (options.dragAndDropImages) {
initFileDrops();
}
bindToolbar($(options.toolbarSelector), options);
editor.attr('contenteditable', true).on('mouseup keyup mouseout', () => {
saveSelection();
updateToolbar();
});
$(window).on('touchend', e => {
var isInside = (editor.is(e.target) || editor.has(e.target).length > 0),
currentRange = getCurrentRange(),
clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset);
if (!clear || isInside) {
saveSelection();
updateToolbar();
}
});
return this;
};
$.fn.wysiwyg.defaults = {
/*
hotKeys: {
'ctrl+b meta+b': 'bold',
'ctrl+i meta+i': 'italic',
'ctrl+u meta+u': 'underline',
'ctrl+z meta+z': 'undo',
'ctrl+y meta+y meta+shift+z': 'redo',
'ctrl+l meta+l': 'justifyleft',
'ctrl+r meta+r': 'justifyright',
'ctrl+e meta+e': 'justifycenter',
'ctrl+j meta+j': 'justifyfull',
'shift+tab': 'outdent',
'tab': 'indent'
},
*/
toolbarSelector: '[data-role=editor-toolbar]',
commandRole: 'edit',
activeToolbarClass: 'btn-info',
selectionMarker: 'edit-focus-marker',
selectionColor: 'darkgrey',
dragAndDropImages: true,
fileUploadError(reason, detail) { console.log("File upload error", reason, detail); }
};
module.exports = $;

41
src/styles/scss/_gjs_rte.scss

@ -0,0 +1,41 @@
.#{$rte-prefix} {
&toolbar {
@extend .#{$app-prefix}bg-main;
@extend .#{$app-prefix}no-user-select;
border: 1px solid $mainDkColor;
position: absolute;
border-radius: 3px;
z-index: 10;
}
&actionbar {
display: flex;
}
&action {
@extend .#{$app-prefix}color-main;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
min-width: 25px;
border-right: 1px solid $mainDkColor;
text-align: center;
cursor: pointer;
outline: none;
&:last-child {
border-right: none;
}
&:hover {
background-color: $mainLhColor;
}
}
&active {
background-color: $mainDkColor;
}
}

61
src/styles/scss/main.scss

@ -236,41 +236,10 @@
@import "gjs_canvas";
/************* RTE ****************/
#commands.panel {
min-width: 35px;
height: 100%;
z-index:3;
}
#options.panel{ z-index:4; bottom: 0;}
#views.panel {
width: 16.5%;
font-weight: lighter;
color: $fontColor;
right:0; top:0;
z-index: 3;
height: 100%;
padding:0;
}
#views.panel .c{height:100%}
#commands.panel, #options.panel {width: 3.5%; left:0;}
#options .c { display: table; margin: 0 auto; }
/*********TEST**********/
body.dragging, body.dragging * { cursor: move !important;}
.dragged {
position: absolute;
@include opacity(0.50);
z-index: 2000;}
ol.example li.placeholder {position: relative;}
ol.example li.placeholder:before {position: absolute;}
/*********END-TEST**********/
/********* COMMANDS **********/
.no-dots, .ui-resizable-handle{ border: none !important; margin:0 !important; outline: none !important; }
/********* COMMANDS **********/
.#{$com-prefix}dashed *{
outline: 1px dashed #888;
outline-offset: -2px;
@ -769,33 +738,7 @@ ol.example li.placeholder:before {position: absolute;}
/*************RTE****************/
##{$rte-prefix}toolbar {
@extend .#{$app-prefix}bg-main;
display: none;
border: 1px solid $mainDkColor;
position: absolute;
border-radius: 3px;
z-index: 10;
.#{$rte-prefix}btn {
@extend .#{$app-prefix}color-main;
display: inline-block;
padding: 5px;
min-width: 25px;
border-right: 1px solid $mainDkColor;
text-align: center;
cursor: pointer;
outline: none;
&:last-child{ border-right:none; }
&.btn-info{ background-color: $mainDkColor;}
&:hover { background-color: $mainLhColor;}
}
select{
height: 16px;
}
}
@import 'gjs_rte';
/********* Spectrum **********/

6
src/utils/extender.js

@ -213,6 +213,12 @@ module.exports = ({$, Backbone}) => {
return this;
}
fn.remove = function () {
return this.each(node => {
return node.parentNode && node.parentNode.removeChild(node);
});
},
// For spectrum compatibility
fn.bind = function(ev, h) {
return this.on(ev, h);

2
test/specs/dom_components/view/ComponentV.js

@ -77,7 +77,7 @@ module.exports = {
it('Clean style', () => {
model.set('style', { color: 'red'});
model.set('style', {});
expect(view.el.getAttribute('style')).toEqual('');
expect(view.el.getAttribute('style')).toEqual(null);
});
it('Get style string', () => {

Loading…
Cancel
Save