From 5cd63088500f6dc76f39e060fd3a3ef3988a791e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 2 Nov 2015 16:05:40 -0500 Subject: [PATCH] API for adding buttons to the new composer --- .../discourse/components/d-editor.js.es6 | 158 +++++++++++++----- .../initializers/enable-emoji.js.es6 | 11 ++ .../templates/components/d-editor.hbs | 22 +-- .../components/d-editor-test.js.es6 | 42 +++-- 4 files changed, 160 insertions(+), 73 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 879cb9c6434..43b07533ac4 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -12,6 +12,96 @@ function getHead(head, prev) { } } +const _createCallbacks = []; + +function Toolbar() { + this.groups = [ + {group: 'fontStyles', buttons: []}, + {group: 'insertions', buttons: []}, + {group: 'extras', buttons: [], lastGroup: true} + ]; + + this.addButton({ + id: 'bold', + group: 'fontStyles', + perform: e => e.applySurround('**', '**', 'bold_text') + }); + + this.addButton({ + id: 'italic', + group: 'fontStyles', + perform: e => e.applySurround('*', '*', 'italic_text') + }); + + this.addButton({group: 'insertions', id: 'link', action: 'showLinkModal'}); + + this.addButton({ + id: 'quote', + group: 'insertions', + icon: 'quote-right', + perform: e => e.applySurround('> ', '', 'code_text') + }); + + this.addButton({ + id: 'code', + group: 'insertions', + perform(e) { + if (e.selected.value.indexOf("\n") !== -1) { + e.applySurround(' ', '', 'code_text'); + } else { + e.applySurround('`', '`', 'code_text'); + } + }, + }); + + this.addButton({ + id: 'bullet', + group: 'extras', + icon: 'list-ul', + perform: e => e.applyList('* ', 'list_item') + }); + + this.addButton({ + id: 'list', + group: 'extras', + icon: 'list-ol', + perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item') + }); + + this.addButton({ + id: 'heading', + group: 'extras', + icon: 'font', + perform: e => e.applyList('## ', 'heading_text') + }); + + this.addButton({ + id: 'rule', + group: 'extras', + icon: 'minus', + perform: e => e.addText("\n\n----------\n") + }); +}; + +Toolbar.prototype.addButton = function(button) { + const g = this.groups.findProperty('group', button.group); + if (!g) { + throw `Couldn't find toolbar group ${button.group}`; + } + + g.buttons.push({ + id: button.id, + className: button.className || button.id, + icon: button.icon || button.id, + action: button.action || 'toolbarButton', + perform: button.perform || Ember.k + }); +}; + +export function onToolbarCreate(func) { + _createCallbacks.push(func); +}; + export default Ember.Component.extend({ classNames: ['d-editor'], ready: false, @@ -25,6 +115,13 @@ export default Ember.Component.extend({ loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true)); }, + @property + toolbar() { + const toolbar = new Toolbar(); + _createCallbacks.forEach(cb => cb(toolbar)); + return toolbar; + }, + @property('ready', 'value') preview(ready, value) { if (!ready) { return; } @@ -51,7 +148,7 @@ export default Ember.Component.extend({ showSelector({ appendTo: self.$(), container, - onSelect: title => self._addText(`${title}:`) + onSelect: title => self._addText(this._getSelected(), `${title}:`) }); return ""; } @@ -112,8 +209,7 @@ export default Ember.Component.extend({ }); }, - _applySurround(head, tail, exampleKey) { - const sel = this._getSelected(); + _applySurround(sel, head, tail, exampleKey) { const pre = sel.pre; const post = sel.post; @@ -162,10 +258,9 @@ export default Ember.Component.extend({ } }, - _applyList(head, exampleKey) { - const sel = this._getSelected(); + _applyList(sel, head, exampleKey) { if (sel.value.indexOf("\n") !== -1) { - this._applySurround(head, '', exampleKey); + this._applySurround(sel, head, '', exampleKey); } else { const [hval, hlen] = getHead(head); @@ -185,20 +280,22 @@ export default Ember.Component.extend({ } }, - _addText(text, sel) { - sel = sel || this._getSelected(); + _addText(sel, text) { const insert = `${sel.pre}${text}`; this.set('value', `${insert}${sel.post}`); this._selectText(insert.length, 0); }, actions: { - bold() { - this._applySurround('**', '**', 'bold_text'); - }, + toolbarButton(button) { - italic() { - this._applySurround('*', '*', 'italic_text'); + const selected = this._getSelected(); + button.perform({ + selected, + applySurround: (head, tail, exampleKey) => this._applySurround(selected, head, tail, exampleKey), + applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey), + addText: text => this._addText(selected, text) + }); }, showLinkModal() { @@ -214,48 +311,19 @@ export default Ember.Component.extend({ if (m && m.length === 2) { const description = m[1]; const remaining = link.replace(m[0], ''); - this._addText(`[${description}](${remaining})`, this._lastSel); + this._addText(this._lastSel, `[${description}](${remaining})`); } else { - this._addText(`[${link}](${link})`, this._lastSel); + this._addText(this._lastSel, `[${link}](${link})`); } this.set('link', ''); }, - code() { - const sel = this._getSelected(); - if (sel.value.indexOf("\n") !== -1) { - this._applySurround(' ', '', 'code_text'); - } else { - this._applySurround('`', '`', 'code_text'); - } - }, - - quote() { - this._applySurround('> ', "", 'code_text'); - }, - - bullet() { - this._applyList('* ', 'list_item'); - }, - - list() { - this._applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item'); - }, - - heading() { - this._applyList('## ', 'heading_text'); - }, - - rule() { - this._addText("\n\n----------\n"); - }, - emoji() { showSelector({ appendTo: this.$(), container: this.container, - onSelect: title => this._addText(`:${title}:`) + onSelect: title => this._addText(this._getSelected(), `:${title}:`) }); } } diff --git a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 index 3d2bd23b5fd..be1b47b1219 100644 --- a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 +++ b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 @@ -1,4 +1,5 @@ import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; +import { onToolbarCreate } from 'discourse/components/d-editor'; export default { name: 'enable-emoji', @@ -6,6 +7,16 @@ export default { initialize(container) { const siteSettings = container.lookup('site-settings:main'); if (siteSettings.enable_emoji) { + + onToolbarCreate(toolbar => { + toolbar.addButton({ + id: 'emoji', + group: 'extras', + icon: 'smile-o', + action: 'emoji' + }); + }); + window.PagedownCustom.appendButtons.push({ id: 'wmd-emoji-button', description: I18n.t("composer.emoji"), diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 8921294cbe2..61e68ae4511 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -8,20 +8,14 @@
- {{d-button action="bold" icon="bold" class="bold"}} - {{d-button action="italic" icon="italic" class="italic"}} -
- {{d-button action="showLinkModal" icon="link" class="link"}} - {{d-button action="quote" icon="quote-right" class="quote"}} - {{d-button action="code" icon="code" class="code"}} -
- {{d-button action="bullet" icon="list-ul" class="bullet"}} - {{d-button action="list" icon="list-ol" class="list"}} - {{d-button action="heading" icon="font" class="heading"}} - {{d-button action="rule" icon="minus" class="rule"}} - {{#if siteSettings.enable_emoji}} - {{d-button action="emoji" icon="smile-o" class="emoji"}} - {{/if}} + {{#each toolbar.groups as |group|}} + {{#each group.buttons as |b|}} + {{d-button action=b.action actionParam=b icon=b.icon class=b.className title=t.title}} + {{/each}} + {{#unless group.lastGroup}} +
+ {{/unless}} + {{/each}}
{{textarea value=value class="d-editor-input"}} diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6 index f27607499c1..572d7342693 100644 --- a/test/javascripts/components/d-editor-test.js.es6 +++ b/test/javascripts/components/d-editor-test.js.es6 @@ -1,4 +1,5 @@ import componentTest from 'helpers/component-test'; +import { onToolbarCreate } from 'discourse/components/d-editor'; moduleForComponent('d-editor', {integration: true}); @@ -441,21 +442,34 @@ testCase(`rule with a selection`, function(assert, textarea) { }); }); -testCase(`emoji`, function(assert) { - assert.equal($('.emoji-modal').length, 0); +componentTest('emoji', { + template: '{{d-editor value=value}}', + setup() { + // Test adding a custom button + onToolbarCreate(toolbar => { + toolbar.addButton({ + id: 'emoji', + group: 'extras', + icon: 'smile-o', + action: 'emoji' + }); + }); + this.set('value', 'hello world.'); + }, + test(assert) { + assert.equal($('.emoji-modal').length, 0); - click('button.emoji'); - andThen(() => { - assert.equal($('.emoji-modal').length, 1); - }); + click('button.emoji'); + andThen(() => { + assert.equal($('.emoji-modal').length, 1); + }); - click('a[data-group-id=0]'); - click('a[title=grinning]'); + click('a[data-group-id=0]'); + click('a[title=grinning]'); - andThen(() => { - assert.ok($('.emoji-modal').length === 0); - assert.equal(this.get('value'), 'hello world.:grinning:'); - }); + andThen(() => { + assert.ok($('.emoji-modal').length === 0); + assert.equal(this.get('value'), 'hello world.:grinning:'); + }); + } }); - -