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();