diff --git a/src/js/commands/format-block.js b/src/js/commands/format-block.js
index 658d58205..cfc2cfb3a 100644
--- a/src/js/commands/format-block.js
+++ b/src/js/commands/format-block.js
@@ -1,8 +1,21 @@
import TextFormatCommand from './text-format';
+import {
+ any
+} from '../utils/array-utils';
class FormatBlockCommand extends TextFormatCommand {
- constructor(options={}) {
+ constructor(editor, options={}) {
super(options);
+ this.editor = editor;
+ }
+
+ isActive() {
+ const editor = this.editor;
+ const activeSections = editor.activeSections;
+
+ return any(activeSections, section => {
+ return any(this.mappedTags, t => section.tagName === t);
+ });
}
exec() {
@@ -15,8 +28,6 @@ class FormatBlockCommand extends TextFormatCommand {
});
editor.rerender();
- editor.trigger('update'); // FIXME -- should be handled by editor
-
editor.selectSections(activeSections);
}
@@ -29,8 +40,6 @@ class FormatBlockCommand extends TextFormatCommand {
});
editor.rerender();
- editor.trigger('update'); // FIXME -- should be handled by editor
-
editor.selectSections(activeSections);
}
}
diff --git a/src/js/commands/heading.js b/src/js/commands/heading.js
index 78fa479b8..26e6736bc 100644
--- a/src/js/commands/heading.js
+++ b/src/js/commands/heading.js
@@ -1,12 +1,12 @@
import FormatBlockCommand from './format-block';
export default class HeadingCommand extends FormatBlockCommand {
- constructor() {
+ constructor(editor) {
const options = {
name: 'heading',
tag: 'h2',
button: '1'
};
- super(options);
+ super(editor, options);
}
}
diff --git a/src/js/commands/quote.js b/src/js/commands/quote.js
index 2fa01af92..0fe1c8469 100644
--- a/src/js/commands/quote.js
+++ b/src/js/commands/quote.js
@@ -1,13 +1,11 @@
import FormatBlockCommand from './format-block';
-import { inherit } from 'content-kit-utils';
-function QuoteCommand() {
- FormatBlockCommand.call(this, {
- name: 'quote',
- tag: 'blockquote',
- button: ''
- });
+export default class QuoteCommand extends FormatBlockCommand {
+ constructor(editor) {
+ super(editor, {
+ name: 'quote',
+ tag: 'blockquote',
+ button: ''
+ });
+ }
}
-inherit(QuoteCommand, FormatBlockCommand);
-
-export default QuoteCommand;
diff --git a/src/js/commands/subheading.js b/src/js/commands/subheading.js
index 849030a5f..21f36ce34 100644
--- a/src/js/commands/subheading.js
+++ b/src/js/commands/subheading.js
@@ -1,13 +1,11 @@
import FormatBlockCommand from './format-block';
-import { inherit } from 'content-kit-utils';
-function SubheadingCommand() {
- FormatBlockCommand.call(this, {
- name: 'subheading',
- tag: 'h3',
- button: '2'
- });
+export default class SubheadingCommand extends FormatBlockCommand {
+ constructor(editor) {
+ super(editor, {
+ name: 'subheading',
+ tag: 'h3',
+ button: '2'
+ });
+ }
}
-inherit(SubheadingCommand, FormatBlockCommand);
-
-export default SubheadingCommand;
diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js
index 560478c6f..802e6bc91 100644
--- a/src/js/editor/editor.js
+++ b/src/js/editor/editor.js
@@ -2,6 +2,7 @@ import TextFormatToolbar from '../views/text-format-toolbar';
import Tooltip from '../views/tooltip';
import EmbedIntent from '../views/embed-intent';
+import ReversibleToolbarButton from '../views/reversible-toolbar-button';
import BoldCommand from '../commands/bold';
import ItalicCommand from '../commands/italic';
import LinkCommand from '../commands/link';
@@ -56,10 +57,7 @@ const defaults = {
textFormatCommands: [
new BoldCommand(),
new ItalicCommand(),
- new LinkCommand(),
- new QuoteCommand(),
- new HeadingCommand(),
- new SubheadingCommand()
+ new LinkCommand()
],
embedCommands: [
new ImageCommand({ serviceUrl: '/upload' }),
@@ -177,6 +175,23 @@ function initEmbedCommands(editor) {
}
}
+function makeButtons(editor) {
+ const headingCommand = new HeadingCommand(editor);
+ const headingButton = new ReversibleToolbarButton(headingCommand, editor);
+
+ const subheadingCommand = new SubheadingCommand(editor);
+ const subheadingButton = new ReversibleToolbarButton(subheadingCommand, editor);
+
+ const quoteCommand = new QuoteCommand(editor);
+ const quoteButton = new ReversibleToolbarButton(quoteCommand, editor);
+
+ return [
+ headingButton,
+ subheadingButton,
+ quoteButton
+ ];
+}
+
/**
* @class Editor
* An individual Editor
@@ -225,7 +240,10 @@ class Editor {
this.addView(new TextFormatToolbar({
editor: this,
rootElement: element,
+ // FIXME -- eventually all the commands should migrate to being buttons
+ // that can be added
commands: this.textFormatCommands,
+ buttons: makeButtons(this),
sticky: this.stickyToolbar
}));
@@ -430,6 +448,7 @@ class Editor {
selectSections(sections) {
this.cursor.selectSections(sections);
+ this.hasSelection();
}
getActiveSections() {
diff --git a/src/js/models/cursor.js b/src/js/models/cursor.js
index df1544222..0debefd27 100644
--- a/src/js/models/cursor.js
+++ b/src/js/models/cursor.js
@@ -70,7 +70,9 @@ export default class Cursor {
const { rangeCount } = selection;
const range = rangeCount > 0 && selection.getRangeAt(0);
- if (!range) { throw new Error('Unable to get activeSections because no range'); }
+ if (!range) {
+ return [];
+ }
const { startContainer, endContainer } = range;
const isSectionElement = (element) => {
diff --git a/src/js/views/reversible-toolbar-button.js b/src/js/views/reversible-toolbar-button.js
new file mode 100644
index 000000000..e4c4f7898
--- /dev/null
+++ b/src/js/views/reversible-toolbar-button.js
@@ -0,0 +1,62 @@
+import mixin from '../utils/mixin';
+import EventListenerMixin from '../utils/event-listener';
+
+const ELEMENT_TYPE = 'button';
+const BUTTON_CLASS_NAME = 'ck-toolbar-btn';
+
+class ReversibleToolbarButton {
+ constructor(command, editor) {
+ this.command = command;
+ this.editor = editor;
+ this.element = this.createElement();
+ this.active = false;
+
+ this.addEventListener(this.element, 'mouseup', e => this.handleClick(e));
+ this.editor.on('selection', () => this.updateActiveState());
+ this.editor.on('selectionUpdated', () => this.updateActiveState());
+ this.editor.on('selectionEnded', () => this.updateActiveState());
+ }
+
+ // These are here to match the API of the ToolbarButton class
+ setInactive() {}
+ setActive() {}
+
+ handleClick(e) {
+ e.stopPropagation();
+
+ if (this.active) {
+ this.command.unexec();
+ } else {
+ this.command.exec();
+ }
+ }
+
+ updateActiveState() {
+ this.active = this.command.isActive();
+ }
+
+ createElement() {
+ const element = document.createElement(ELEMENT_TYPE);
+ element.className = BUTTON_CLASS_NAME;
+ element.innerHTML = this.command.button;
+ element.title = this.command.name;
+ return element;
+ }
+
+ set active(val) {
+ this._active = val;
+ if (this._active) {
+ this.element.className = BUTTON_CLASS_NAME + ' active';
+ } else {
+ this.element.className = BUTTON_CLASS_NAME;
+ }
+ }
+
+ get active() {
+ return this._active;
+ }
+}
+
+mixin(ReversibleToolbarButton, EventListenerMixin);
+
+export default ReversibleToolbarButton;
diff --git a/src/js/views/toolbar.js b/src/js/views/toolbar.js
index 9fc9cdaf6..8e8e3ddff 100644
--- a/src/js/views/toolbar.js
+++ b/src/js/views/toolbar.js
@@ -34,9 +34,6 @@ class Toolbar extends View {
options.classNames = ['ck-toolbar'];
super(options);
- let commands = options.commands;
- let commandCount = commands && commands.length;
-
this.setDirection(options.direction || ToolbarDirection.TOP);
this.editor = options.editor || null;
this.embedIntent = options.embedIntent || null;
@@ -50,9 +47,8 @@ class Toolbar extends View {
this.contentElement.appendChild(this.buttonContainerElement);
this.element.appendChild(this.contentElement);
- for(let i = 0; i < commandCount; i++) {
- this.addCommand(commands[i]);
- }
+ (options.buttons || []).forEach(b => this.addButton(b));
+ (options.commands || []).forEach(c => this.addCommand(c));
// Closes prompt if displayed when changing selection
this.addEventListener(document, 'mouseup', () => {
@@ -73,6 +69,10 @@ class Toolbar extends View {
command.editor = this.editor;
command.embedIntent = this.embedIntent;
let button = new ToolbarButton({command: command, toolbar: this});
+ this.addButton(button);
+ }
+
+ addButton(button) {
this.buttons.push(button);
this.buttonContainerElement.appendChild(button.element);
}
diff --git a/tests/acceptance/editor-commands-test.js b/tests/acceptance/editor-commands-test.js
index dbf219086..96c6df2a4 100644
--- a/tests/acceptance/editor-commands-test.js
+++ b/tests/acceptance/editor-commands-test.js
@@ -131,7 +131,8 @@ test('highlighting heading text activates toolbar button', (assert) => {
Helpers.dom.triggerEvent(document, 'mouseup');
setTimeout(() => {
- assertActiveToolbarButton(assert, 'heading');
+ assertActiveToolbarButton(assert, 'heading',
+ 'heading button is active when text is selected');
done();
});
@@ -159,6 +160,32 @@ test('when heading text is highlighted, clicking heading button turns it to plai
});
});
+test('clicking multiple heading buttons keeps the correct ones active', (assert) => {
+ const done = assert.async();
+
+ setTimeout(() => {
+ // click subheading, makes its button active, changes the display
+ clickToolbarButton(assert, 'subheading');
+ assert.hasElement('#editor h3:contains(THIS IS A TEST)');
+ assertActiveToolbarButton(assert, 'subheading');
+ assertInactiveToolbarButton(assert, 'heading');
+
+ // click heading, makes its button active and no others, changes display
+ clickToolbarButton(assert, 'heading');
+ assert.hasElement('#editor h2:contains(THIS IS A TEST)');
+ assertActiveToolbarButton(assert, 'heading');
+ assertInactiveToolbarButton(assert, 'subheading');
+
+ // click heading again, removes headline from display, no active buttons
+ clickToolbarButton(assert, 'heading');
+ assert.hasElement('#editor p:contains(THIS IS A TEST)');
+ assertInactiveToolbarButton(assert, 'heading');
+ assertInactiveToolbarButton(assert, 'subheading');
+
+ done();
+ });
+});
+
test('highlight text, click "subheading" button turns text into h3 header', (assert) => {
const done = assert.async();