Skip to content

Commit f880537

Browse files
authored
Merge pull request #37 from typecode/issue-23
#23 (+ #24, #35, #36) - Toolbar configuration + refinements + bugs
2 parents 5f68dcf + 6b08d2f commit f880537

25 files changed

+491
-117
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ import Typester from 'typester-editor'
3232
new Typester({ el: document.querySelector('[contenteditable]') }) // Where document.querySelector(...) is a single DOM element.
3333
```
3434

35+
### Configuration
36+
You can configure the formatters available for a specific typester instance in two ways:
37+
38+
1. When you instatiate a Typester instance via the custom configs option:
39+
40+
```
41+
new Typester({
42+
el: document.querySelector('[contenteditable]'),
43+
configs: {
44+
toolbar: {
45+
buttons: ['bold', 'italic', 'h1', 'h2', 'orderedlist', 'unorderedlist', 'quote', 'link']
46+
}
47+
}
48+
});
49+
```
50+
51+
2. By using a data attribute on the editable container
52+
```
53+
<div contenteditable='true' data-toolbar-buttons='["bold", "italic", "h1", "h2", "orderedlist", "unorderedlist", "quote", "link"]'></div>
54+
```
55+
56+
The options available for the toolbar buttons are:
57+
- Inline formatters: `bold`, `italic`
58+
- Headers: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
59+
- Lists: `orderedlist`, `unorderedlist`
60+
- Blockquotes: `quote`
61+
- Links: `link`
62+
3563
### License
3664
Typester is released under the MIT license.
3765

src/scripts/config/toolbar.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ const Toolbar = {
8181
},
8282
content: '<b>B</b>',
8383
disabledIn: ['H1', 'H2', 'BLOCKQUOTE'],
84-
activeIn: ['B']
84+
activeIn: ['B'],
85+
toggles: true
8586
},
8687

8788
italic: {
@@ -92,7 +93,8 @@ const Toolbar = {
9293
validTags: ['I']
9394
},
9495
content: '<i>I</i>',
95-
activeIn: ['I']
96+
activeIn: ['I'],
97+
toggles: true
9698
},
9799

98100
underline: {
@@ -102,7 +104,7 @@ const Toolbar = {
102104

103105
strikethrough: {
104106
formatter: 'text:strikethrough',
105-
content: '<s>A</s>'
107+
content: '<s>Abc</s>'
106108
},
107109

108110
superscript: {

src/scripts/containers/AppContainer.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import FormatterContainer from '../containers/FormatterContainer';
3030
import CanvasContainer from '../containers/CanvasContainer';
3131
import ContentEditable from '../modules/ContentEditable';
3232
import Selection from '../modules/Selection';
33+
import Config from '../modules/Config';
3334

3435
let uiContainer, formatterContainer, canvasContainer;
3536

@@ -53,6 +54,9 @@ const AppContainer = Container({
5354
},
5455
{
5556
class: Selection
57+
},
58+
{
59+
class: Config
5660
}
5761
],
5862

@@ -75,10 +79,6 @@ const AppContainer = Container({
7579
* @protected
7680
*/
7781
setup: function () {
78-
const { mediator } = this;
79-
formatterContainer = formatterContainer || new FormatterContainer({ mediator });
80-
uiContainer = uiContainer || new UIContainer({ mediator });
81-
canvasContainer = canvasContainer || new CanvasContainer({ mediator });
8282
},
8383

8484
/**
@@ -88,6 +88,10 @@ const AppContainer = Container({
8888
*/
8989
init () {
9090
// Current nothing to init for this container. Method left here for ref.
91+
const { mediator } = this;
92+
formatterContainer = formatterContainer || new FormatterContainer({ mediator });
93+
uiContainer = uiContainer || new UIContainer({ mediator });
94+
canvasContainer = canvasContainer || new CanvasContainer({ mediator });
9195
},
9296

9397
/**

src/scripts/containers/FormatterContainer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import BlockFormatter from '../modules/BlockFormatter';
2424
import TextFormatter from '../modules/TextFormatter';
2525
import ListFormatter from '../modules/ListFormatter';
2626
import LinkFormatter from '../modules/LinkFormatter';
27+
import Commands from '../modules/Commands';
2728
import Paste from '../modules/Paste';
2829

2930
/**
@@ -42,6 +43,9 @@ const FormatterContainer = Container({
4243
* @enum {Array<{class:Module}>} modules
4344
*/
4445
modules: [
46+
{
47+
class: Commands
48+
},
4549
{
4650
class: BaseFormatter
4751
},

src/scripts/core/Container.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,13 @@ const Container = function Container(containerObj) {
149149

150150
containerUtils.initModules(containerModules, {
151151
dom: opts.dom,
152+
configs: opts.configs,
152153
mediator
153154
});
154155

155156
containerUtils.initChildContainers(containerChildContainers, {
156157
dom: opts.dom,
158+
configs: opts.configs,
157159
mediator
158160
});
159161

src/scripts/core/Module.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ const Module = function (moduleObj) {
5353
handlers: moduleHandlers,
5454
dom: moduleDom,
5555
methods: moduleMethods,
56-
requiredProps: moduleRequiredProps
56+
requiredProps: moduleRequiredProps,
57+
acceptsConfigs: moduleAcceptsConfigs
5758
} = moduleObj;
5859

5960
if (!moduleName) {
@@ -125,6 +126,7 @@ const Module = function (moduleObj) {
125126

126127
mergeDom (defaultDom, dom={}) {
127128
let mergedDom = {};
129+
128130
Object.keys(defaultDom).forEach((domKey) => {
129131
mergedDom[domKey] = defaultDom[domKey];
130132
});
@@ -139,6 +141,7 @@ const Module = function (moduleObj) {
139141

140142
getDom (dom) {
141143
const rootEl = dom.el || document.body;
144+
142145
Object.keys(dom).forEach((domKey) => {
143146
let selector, domEl;
144147

@@ -191,6 +194,7 @@ const Module = function (moduleObj) {
191194
const moduleProto = {
192195
moduleConstructor: function (opts) {
193196
moduleProto.prepModule(opts);
197+
moduleProto.bindConfigs(opts);
194198
moduleProto.buildModule(opts);
195199
moduleProto.setupModule(opts);
196200
moduleProto.renderModule(opts);
@@ -212,6 +216,20 @@ const Module = function (moduleObj) {
212216
opts.context = context;
213217
},
214218

219+
bindConfigs (opts) {
220+
if (!moduleAcceptsConfigs) { return; }
221+
222+
const { context } = opts;
223+
const optsConfigs = opts.configs || {};
224+
let moduleConfigs = {};
225+
226+
moduleAcceptsConfigs.forEach((configKey) => {
227+
moduleConfigs[configKey] = optsConfigs[configKey] || {};
228+
});
229+
230+
context.extendWith({ configs: moduleConfigs });
231+
},
232+
215233
buildModule (opts) {
216234
const { context } = opts;
217235
const boundMethods = moduleUtils.bindMethods(moduleMethods, context);

src/scripts/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import AppContainer from './containers/AppContainer';
1212
* @param {object} opts={} - instance options
1313
* @param {object} opts.dom - The dom components used by Typester
1414
* @param {element} opts.dom.el - The dom element to be the canvas for Typester
15+
* @param {object} opts.config - Additional instanced config
1516
* @return {appContainer} AppContainer instance
1617
*
1718
* @example
@@ -22,7 +23,10 @@ import AppContainer from './containers/AppContainer';
2223
* });
2324
*/
2425
const Typester = function (opts={}) {
25-
return new AppContainer({ dom: {el: opts.el }});
26+
return new AppContainer({
27+
dom: {el: opts.el },
28+
configs: opts.configs
29+
});
2630
};
2731

2832
export default Typester;

src/scripts/modules/BaseFormatter.js

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,10 @@
2323
* mediator.exec('format:clean', elem); // Clean the HTML inside elem
2424
*/
2525
import Module from '../core/Module';
26-
import commands from '../utils/commands';
2726
import DOM from '../utils/DOM';
2827
import zeroWidthSpace from '../utils/zeroWidthSpace';
2928

30-
import toolbarConfig from '../config/toolbar';
31-
32-
let validTags = toolbarConfig.getValidTags();
33-
let blockTags = toolbarConfig.getBlockTags();
34-
let listTags = toolbarConfig.getListTags();
29+
let validTags, blockTags, listTags;
3530

3631
const BaseFormatter = Module({
3732
name: 'BaseFormatter',
@@ -49,7 +44,12 @@ const BaseFormatter = Module({
4944
}
5045
},
5146
methods: {
52-
init () {},
47+
init () {
48+
const { mediator } = this;
49+
validTags = mediator.get('config:toolbar:validTags');
50+
blockTags = mediator.get('config:toolbar:blockTags');
51+
listTags = mediator.get('config:toolbar:listTags');
52+
},
5353

5454
/**
5555
* @func exportToCanvas
@@ -64,10 +64,7 @@ const BaseFormatter = Module({
6464
this.injectHooks(rootElement);
6565

6666
const rangeCoordinates = mediator.get('selection:range:coordinates');
67-
const clonedNodes = this.cloneNodes(rootElement);
68-
clonedNodes.forEach((node) => {
69-
DOM.trimNodeText(node);
70-
});
67+
const clonedNodes = DOM.cloneNodes(rootElement, { trim: true });
7168

7269
mediator.exec('canvas:content', clonedNodes);
7370
mediator.exec('canvas:select:by:coordinates', rangeCoordinates);
@@ -109,7 +106,7 @@ const BaseFormatter = Module({
109106
formatDefault () {
110107
const { mediator } = this;
111108
const rootElem = mediator.get('selection:rootelement');
112-
commands.defaultBlockFormat();
109+
mediator.exec('commands:format:default');
113110
this.removeStyledSpans(rootElem);
114111
},
115112

@@ -141,14 +138,6 @@ const BaseFormatter = Module({
141138
/**
142139
* PRIVATE METHODS:
143140
*/
144-
cloneNodes (rootElement) {
145-
let clonedNodes = [];
146-
rootElement.childNodes.forEach((node) => {
147-
clonedNodes.push(node.cloneNode(true));
148-
});
149-
return clonedNodes;
150-
},
151-
152141
injectHooks (rootElement) {
153142
while (!/\w+/.test(rootElement.firstChild.textContent)) {
154143
DOM.removeNode(rootElement.firstChild);
@@ -165,18 +154,21 @@ const BaseFormatter = Module({
165154
formatEmptyNewLine () {
166155
const { mediator } = this;
167156
const anchorNode = mediator.get('selection:anchornode');
168-
const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, toolbarConfig.preventNewlineDefault);
157+
const preventNewlineDefault = mediator.get('config:toolbar:preventNewlineDefault');
158+
const canDefaultNewline = !(anchorNode.innerText && anchorNode.innerText.trim().length) && !DOM.isIn(anchorNode, preventNewlineDefault);
169159
const anchorIsContentEditable = anchorNode.hasAttribute && anchorNode.hasAttribute('contenteditable');
170160

171161
if (canDefaultNewline || anchorIsContentEditable) {
172162
this.formatDefault();
173163
}
174164
},
175165

176-
formateBlockquoteNewLine () {
166+
formatBlockquoteNewLine () {
177167
const { mediator } = this;
178168

179-
commands.exec('outdent');
169+
mediator.exec('commands:exec', {
170+
command: 'outdent'
171+
});
180172
this.formatDefault();
181173

182174
const currentRangeClone = mediator.get('selection:range').cloneRange();
@@ -207,7 +199,7 @@ const BaseFormatter = Module({
207199
const isContentEditable = startContainer.nodeType === Node.ELEMENT_NODE && startContainer.hasAttribute('contenteditable');
208200

209201
if (containerIsBlockquote) {
210-
this.formateBlockquoteNewLine();
202+
this.formatBlockquoteNewLine();
211203
} else if (containerIsEmpty || isContentEditable) {
212204
this.formatEmptyNewLine();
213205
}
@@ -235,9 +227,10 @@ const BaseFormatter = Module({
235227
const isLastChild = brNode === brNode.parentNode.lastChild;
236228
const isDoubleBreak = brNode.nextSibling && brNode.nextSibling.nodeName === 'BR';
237229
const isInBlock = DOM.isIn(brNode, blockTags, rootElem);
230+
const isOrphan = brNode.parentNode === rootElem;
238231

239-
if (isLastChild) {
240-
brNodesToRemove.push(isLastChild);
232+
if (isLastChild || isOrphan) {
233+
brNodesToRemove.push(brNode);
241234
return;
242235
}
243236

@@ -291,8 +284,9 @@ const BaseFormatter = Module({
291284
let isInvalid = validTags.indexOf(currentNode.nodeName) < 0;
292285
let isBrNode = currentNode.nodeName === 'BR'; // BR nodes are handled elsewhere
293286
let isTypesterElem = currentNode.className && /typester/.test(currentNode.className);
287+
let isElement = currentNode.nodeType !== Node.TEXT_NODE;
294288

295-
if (isInvalid && !isBrNode && !isTypesterElem) {
289+
if (isInvalid && !isBrNode && !isTypesterElem && isElement) {
296290
invalidElements.unshift(currentNode);
297291
}
298292
}
@@ -312,6 +306,7 @@ const BaseFormatter = Module({
312306

313307
defaultOrphanedTextNodes (rootElem) {
314308
const { childNodes } = rootElem;
309+
315310
for (let i = 0; i < childNodes.length; i++) {
316311
let childNode = childNodes[i];
317312
if (childNode.nodeType === Node.TEXT_NODE && /\w+/.test(childNode.textContent)) {

src/scripts/modules/BlockFormatter.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
*/
2626

2727
import Module from '../core/Module';
28-
import commands from '../utils/commands';
2928
import DOM from '../utils/DOM';
3029

3130
/**
@@ -62,11 +61,19 @@ const BlockFormatter = Module({
6261

6362
if (opts.toggle) {
6463
if (opts.style === 'BLOCKQUOTE') {
65-
commands.exec('outdent', null, canvasDoc);
64+
mediator.exec('commands:exec', {
65+
command: 'outdent',
66+
contextDocument: canvasDoc
67+
});
6668
}
67-
commands.defaultBlockFormat(canvasDoc);
69+
mediator.exec('commands:format:default', {
70+
contextDocument: canvasDoc
71+
});
6872
} else {
69-
commands.formatBlock(opts.style, canvasDoc);
73+
mediator.exec('commands:format:block', {
74+
style: opts.style,
75+
contextDocument: canvasDoc
76+
});
7077
}
7178
},
7279

0 commit comments

Comments
 (0)