Browse Source

Added state to RTE actions

pull/2429/head
adriangroch 7 years ago
parent
commit
f7f68a66c5
  1. 627
      src/rich_text_editor/index.js
  2. 73
      src/rich_text_editor/model/RichTextEditor.js
  3. 9
      src/styles/scss/_gjs_rte.scss

627
src/rich_text_editor/index.js

@ -5,302 +5,331 @@
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/rich_text_editor/config/config.js)
* ```js
* const editor = grapesjs.init({
* richTextEditor: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const rte = editor.RichTextEditor;
* ```
*
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [remove](#remove)
* * [getToolbarEl](#gettoolbarel)
*
* @module RichTextEditor
*/
import RichTextEditor from './model/RichTextEditor';
import { on, off } from 'utils/mixins';
import defaults from './config/config';
export default () => {
let config = {};
let toolbar, actions, lastEl, globalRte;
const hideToolbar = () => {
const style = toolbar.style;
const size = '-1000px';
style.top = size;
style.left = size;
style.display = 'none';
};
return {
customRte: null,
/**
* Name of the module
* @type {String}
* @private
*/
name: 'RichTextEditor',
getConfig() {
return config;
},
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} opts Options
* @private
*/
init(opts = {}) {
config = {
...defaults,
...opts
};
const ppfx = config.pStylePrefix;
if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix;
}
this.pfx = config.stylePrefix;
actions = config.actions || [];
toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
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';
hideToolbar();
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 action to the built-in RTE toolbar
* @param {string} name Action name
* @param {Object} action Action options
* @example
* rte.add('bold', {
* icon: '<b>B</b>',
* attributes: {title: 'Bold'},
* result: rte => rte.exec('bold')
* });
* rte.add('link', {
* icon: document.getElementById('t'),
* attributes: {title: 'Link',}
* // Example on it's easy to wrap a selected content
* result: rte => rte.insertHTML(`<a href="#">${rte.selection()}</a>`)
* });
* // An example with fontSize
* rte.add('fontSize', {
* icon: `<select class="gjs-field">
* <option>1</option>
* <option>4</option>
* <option>7</option>
* </select>`,
* // Bind the 'result' on 'change' listener
* event: 'change',
* result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value),
* // Callback on any input change (mousedown, keydown, etc..)
* update: (rte, action) => {
* const value = rte.doc.queryCommandValue(action.name);
* if (value != 'false') { // value is a string
* action.btn.firstChild.value = value;
* }
* }
* })
*/
add(name, action = {}) {
action.name = name;
globalRte.addAction(action, { sync: 1 });
},
/**
* Get the action by its name
* @param {string} name Action name
* @return {Object}
* @example
* const action = rte.get('bold');
* // {name: 'bold', ...}
*/
get(name) {
let result;
globalRte.getActions().forEach(action => {
if (action.name == name) {
result = action;
}
});
return result;
},
/**
* Get all actions
* @return {Array}
*/
getAll() {
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;
},
/**
* Triggered when the offset of the editor is changed
* @private
*/
updatePosition() {
const un = 'px';
const canvas = config.em.get('Canvas');
const pos = canvas.getTargetToElementDim(toolbar, lastEl, {
event: 'rteToolbarPosUpdate'
});
if (pos) {
if (config.adjustToolbar) {
const frameOffset = canvas.getCanvasView().getFrameOffset();
// Move the toolbar down when the top canvas edge is reached
if (
pos.top <= pos.canvasTop &&
!(pos.elementHeight + pos.targetHeight >= frameOffset.height)
) {
pos.top = pos.elementTop + pos.elementHeight;
}
}
const toolbarStyle = toolbar.style;
toolbarStyle.top = pos.top + un;
toolbarStyle.left = pos.left + un;
}
},
/**
* Enable rich text editor on the element
* @param {View} view Component view
* @param {Object} rte The instance of already defined RTE
* @private
* */
enable(view, rte) {
lastEl = view.el;
const em = config.em;
const el = view.getChildrenContainer();
const customRte = this.customRte;
toolbar.style.display = '';
rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable();
if (em) {
setTimeout(this.updatePosition.bind(this), 0);
const event = 'change:canvasOffset canvasScroll';
em.off(event, this.updatePosition, this);
em.on(event, this.updatePosition, this);
em.trigger('rte:enable', view, rte);
}
return rte;
},
/**
* Unbind rich text editor from the element
* @param {View} view
* @param {Object} rte The instance of already defined RTE
* @private
* */
disable(view, rte) {
const em = config.em;
const customRte = this.customRte;
var el = view.getChildrenContainer();
if (customRte) {
customRte.disable(el, rte);
} else {
rte && rte.disable();
}
hideToolbar();
em && em.trigger('rte:disable', view, rte);
}
};
};
* richTextEditor: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const rte = editor.RichTextEditor;
* ```
*
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [remove](#remove)
* * [getToolbarEl](#gettoolbarel)
*
* @module RichTextEditor
*/
import RichTextEditor from './model/RichTextEditor';
import { on, off } from 'utils/mixins';
import defaults from './config/config';
export default () => {
let config = {};
let toolbar, actions, lastEl, globalRte;
const hideToolbar = () => {
const style = toolbar.style;
const size = '-1000px';
style.top = size;
style.left = size;
style.display = 'none';
};
return {
customRte: null,
/**
* Name of the module
* @type {String}
* @private
*/
name: 'RichTextEditor',
getConfig() {
return config;
},
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} opts Options
* @private
*/
init(opts = {}) {
config = {
...defaults,
...opts
};
const ppfx = config.pStylePrefix;
if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix;
}
this.pfx = config.stylePrefix;
actions = config.actions || [];
toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
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';
hideToolbar();
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`,
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`
};
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 action to the built-in RTE toolbar
* @param {string} name Action name
* @param {Object} action Action options
* @example
* rte.add('bold', {
* icon: '<b>B</b>',
* attributes: {title: 'Bold'},
* result: rte => rte.exec('bold')
* });
* rte.add('link', {
* icon: document.getElementById('t'),
* attributes: {title: 'Link',}
* // Example on it's easy to wrap a selected content
* result: rte => rte.insertHTML(`<a href="#">${rte.selection()}</a>`)
* });
* // An example with fontSize
* rte.add('fontSize', {
* icon: `<select class="gjs-field">
* <option>1</option>
* <option>4</option>
* <option>7</option>
* </select>`,
* // Bind the 'result' on 'change' listener
* event: 'change',
* result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value),
* // Callback on any input change (mousedown, keydown, etc..)
* update: (rte, action) => {
* const value = rte.doc.queryCommandValue(action.name);
* if (value != 'false') { // value is a string
* action.btn.firstChild.value = value;
* }
* }
* })
* // An example with state
* const isValidAnchor = (rte) => {
* // a utility function to help determine if the selected is a valid anchor node
* const anchor = rte.selection().anchorNode;
* const parentNode = anchor && anchor.parentNode;
* const nextSibling = anchor && anchor.nextSibling;
* return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A')
* }
* rte.add('toggleAnchor', {
* icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
* state: (rte, doc) => {
* if (rte && rte.selection()) {
* // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active
* return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
* } else {
* return btnState.INACTIVE;
* }
* },
* result: (rte, action) => {
* if (isValidAnchor(rte)) {
* rte.exec('unlink');
* } else {
* rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
* }
* }
* })
*/
add(name, action = {}) {
action.name = name;
globalRte.addAction(action, { sync: 1 });
},
/**
* Get the action by its name
* @param {string} name Action name
* @return {Object}
* @example
* const action = rte.get('bold');
* // {name: 'bold', ...}
*/
get(name) {
let result;
globalRte.getActions().forEach(action => {
if (action.name == name) {
result = action;
}
});
return result;
},
/**
* Get all actions
* @return {Array}
*/
getAll() {
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;
},
/**
* Triggered when the offset of the editor is changed
* @private
*/
updatePosition() {
const un = 'px';
const canvas = config.em.get('Canvas');
const pos = canvas.getTargetToElementDim(toolbar, lastEl, {
event: 'rteToolbarPosUpdate'
});
if (pos) {
if (config.adjustToolbar) {
const frameOffset = canvas.getCanvasView().getFrameOffset();
// Move the toolbar down when the top canvas edge is reached
if (
pos.top <= pos.canvasTop &&
!(pos.elementHeight + pos.targetHeight >= frameOffset.height)
) {
pos.top = pos.elementTop + pos.elementHeight;
}
}
const toolbarStyle = toolbar.style;
toolbarStyle.top = pos.top + un;
toolbarStyle.left = pos.left + un;
}
},
/**
* Enable rich text editor on the element
* @param {View} view Component view
* @param {Object} rte The instance of already defined RTE
* @private
* */
enable(view, rte) {
lastEl = view.el;
const em = config.em;
const el = view.getChildrenContainer();
const customRte = this.customRte;
toolbar.style.display = '';
rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable();
if (em) {
setTimeout(this.updatePosition.bind(this), 0);
const event = 'change:canvasOffset canvasScroll';
em.off(event, this.updatePosition, this);
em.on(event, this.updatePosition, this);
em.trigger('rte:enable', view, rte);
}
return rte;
},
/**
* Unbind rich text editor from the element
* @param {View} view
* @param {Object} rte The instance of already defined RTE
* @private
* */
disable(view, rte) {
const em = config.em;
const customRte = this.customRte;
var el = view.getChildrenContainer();
if (customRte) {
customRte.disable(el, rte);
} else {
rte && rte.disable();
}
hideToolbar();
em && em.trigger('rte:disable', view, rte);
}
};
};

73
src/rich_text_editor/model/RichTextEditor.js

@ -5,6 +5,20 @@ import { on, off } from 'utils/mixins';
const RTE_KEY = '_rte';
const btnState = {
ACTIVE: 1,
INACTIVE: 0,
DISABLED: -1
};
const isValidAnchor = rte => {
const anchor = rte.selection().anchorNode;
const parentNode = anchor && anchor.parentNode;
const nextSibling = anchor && anchor.nextSibling;
return (
(parentNode && parentNode.nodeName == 'A') ||
(nextSibling && nextSibling.nodeName == 'A')
);
};
const defActions = {
bold: {
name: 'bold',
@ -37,10 +51,15 @@ const defActions = {
style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link'
},
state: (rte, doc) => {
if (rte && rte.selection()) {
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
},
result: rte => {
const anchor = rte.selection().anchorNode;
const nextSibling = anchor && anchor.nextSibling;
if (nextSibling && nextSibling.nodeName == 'A') {
if (isValidAnchor(rte)) {
rte.exec('unlink');
} else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
@ -78,7 +97,9 @@ export default class RichTextEditor {
...{
actionbar: 'actionbar',
button: 'action',
active: 'active'
active: 'active',
disabled: 'disabled',
inactive: 'inactive'
},
...settings.classes
};
@ -114,16 +135,36 @@ export default class RichTextEditor {
this.getActions().forEach(action => {
const btn = action.btn;
const update = action.update;
const active = this.classes.active;
const { active, inactive, disabled } = { ...this.classes };
const state = action.state;
const name = action.name;
const doc = this.doc;
btn.className = btn.className.replace(active, '').trim();
// doc.queryCommandValue(name) != 'false'
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
btn.className = btn.className.replace(inactive, '').trim();
btn.className = btn.className.replace(disabled, '').trim();
console.log(this);
// if there is a state function, which depicts the state,
// i.e. `active`, `disabled`, then call it
if (state) {
switch (state(this, doc)) {
case btnState.ACTIVE:
btn.className += ` ${active}`;
break;
case btnState.INACTIVE:
btn.className += ` ${inactive}`;
break;
case btnState.DISABLED:
btn.className += ` ${disabled}`;
break;
}
} else {
// otherwise default to checking if the name command is supported & enabled
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
}
}
update && update(this, action);
});
}
@ -156,11 +197,13 @@ export default class RichTextEditor {
*/
syncActions() {
this.getActions().forEach(action => {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
this.updateActiveActions();
};
if (!action.state || (action.state && action.state() >= 0)) {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
this.updateActiveActions();
};
}
});
}

9
src/styles/scss/_gjs_rte.scss

@ -33,6 +33,13 @@
}
&active {
background-color: $mainDkColor;
background-color: $mainLhColor;
}
&disabled {
color: $mainLhColor;
cursor: not-allowed;
&:hover {
background-color: unset;
}
}
}

Loading…
Cancel
Save