From 9ac57eeb982d7969c4de0d282233c31d879a2092 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 20 Nov 2019 23:08:04 +1100 Subject: [PATCH] FEATURE: Add real Discourse composer --- assets/javascripts/wizard-custom-lib.js | 1 + assets/javascripts/wizard-custom.js | 96 ++- .../javascripts/wizard-raw-templates.js.erb | 34 +- .../components/wizard-composer-editor.js.es6 | 78 +++ .../wizard/components/wizard-editor.js.es6 | 659 ------------------ .../components/wizard-field-composer.js.es6 | 33 +- .../javascripts/wizard/custom-wizard.js.es6 | 36 +- .../wizard/helpers/plugin-outlet.js.es6 | 5 + .../wizard/initializers/custom.js.es6 | 9 + .../components/wizard-composer-editor.hbs | 19 + .../components/wizard-field-composer.hbs | 14 +- .../stylesheets/wizard/wizard_composer.scss | 13 + 12 files changed, 299 insertions(+), 698 deletions(-) create mode 100644 assets/javascripts/wizard/components/wizard-composer-editor.js.es6 delete mode 100644 assets/javascripts/wizard/components/wizard-editor.js.es6 create mode 100644 assets/javascripts/wizard/helpers/plugin-outlet.js.es6 create mode 100644 assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs diff --git a/assets/javascripts/wizard-custom-lib.js b/assets/javascripts/wizard-custom-lib.js index 1752f4e8..ee775332 100644 --- a/assets/javascripts/wizard-custom-lib.js +++ b/assets/javascripts/wizard-custom-lib.js @@ -3,6 +3,7 @@ window.Wizard = {}; Wizard.SiteSettings = {}; Wizard.RAW_TEMPLATES = {}; Discourse.__widget_helpers = {}; +Discourse.RAW_TEMPLATES = {}; Discourse.SiteSettings = Wizard.SiteSettings; Discourse.Model = Ember.Object.extend(); Discourse.Site = Ember.Object.extend(); \ No newline at end of file diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index ccc339d4..ce454717 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -12,24 +12,25 @@ //= require discourse/lib/logout //= require discourse/lib/render-tag //= require discourse/lib/notification-levels -//= require discourse/lib/computed +//= require discourse/lib/computed +//= require discourse/lib/user-search +//= require discourse/lib/debounce +//= require discourse/lib/text +//= require discourse/lib/formatter +//= require discourse/lib/quote +//= require discourse/lib/link-mentions +//= require discourse/lib/link-category-hashtags +//= require discourse/lib/category-hashtags +//= require discourse/lib/link-tag-hashtag +//= require discourse/lib/tag-hashtags +//= require discourse/lib/raw-templates +//= require discourse/lib/uploads +//= require discourse/lib/category-tag-search +//= require discourse/lib/intercept-click +//= require discourse/lib/show-modal +//= require discourse/lib/key-value-store -//= require markdown-it-bundle -//= require pretty-text/engines/discourse-markdown-it -//= require pretty-text/engines/discourse-markdown/helpers -//= require pretty-text/pretty-text -//= require ember-addons/fmt -//= require preload-store - -//= require ./wizard/custom-wizard -//= require_tree ./wizard/components -//= require_tree ./wizard/controllers -//= require_tree ./wizard/helpers -//= require_tree ./wizard/initializers -//= require_tree ./wizard/lib -//= require_tree ./wizard/models -//= require_tree ./wizard/routes -//= require_tree ./wizard/templates +//= require discourse/mixins/singleton //= require discourse/models/permission-type //= require discourse/models/archetype @@ -41,14 +42,67 @@ //= require discourse/models/trust-level //= require discourse/models/store //= require discourse/models/result-set +//= require discourse/models/user +//= require discourse/models/user-stream +//= require discourse/models/user-action +//= require discourse/models/user-action-group +//= require discourse/models/user-posts-stream +//= require discourse/models/badge +//= require discourse/models/badge-grouping +//= require discourse/models/user-badge +//= require discourse/models/topic +//= require discourse/models/action-summary +//= require discourse/models/user-action-stat +//= require discourse/models/user-drafts-stream +//= require discourse/models/user-draft +//= require discourse/models/composer +//= require discourse/models/draft +//= require discourse/models/group +//= require discourse/models/group-history + //= require discourse/helpers/category-link -//= require discourse/mixins/singleton +//= require discourse/helpers/user-avatar +//= require discourse/helpers/format-username + +//= require discourse/services/app-events +//= require discourse/services/emoji-store //= require discourse/components/user-selector -//= require discourse/helpers/user-avatar //= require discourse/components/conditional-loading-spinner -//= require discourse/templates/components/conditional-loading-spinner //= require discourse/components/d-button +//= require discourse/components/composer-editor +//= require discourse/components/d-editor +//= require discourse/components/popup-input-tip +//= require discourse/components/emoji-picker + +//= require discourse/templates/components/conditional-loading-spinner //= require discourse/templates/components/d-button +//= require discourse/templates/components/d-editor +//= require discourse/templates/components/emoji-picker +//= require discourse/templates/category-tag-autocomplete +//= require discourse/templates/emoji-selector-autocomplete + +//= require discourse/pre-initializers/sniff-capabilities + +//= require ember-addons/decorator-alias +//= require ember-addons/macro-alias +//= require ember-addons/fmt +//= require polyfills + +//= require markdown-it-bundle +//= require preload-store //= require lodash.js -//= require mousetrap.js \ No newline at end of file +//= require mousetrap.js +//= require jquery.putcursoratend.js +//= require template_include.js +//= require caret_position.js + +//= require ./wizard/custom-wizard +//= require_tree ./wizard/components +//= require_tree ./wizard/controllers +//= require_tree ./wizard/helpers +//= require_tree ./wizard/initializers +//= require_tree ./wizard/lib +//= require_tree ./wizard/models +//= require_tree ./wizard/routes +//= require_tree ./wizard/templates diff --git a/assets/javascripts/wizard-raw-templates.js.erb b/assets/javascripts/wizard-raw-templates.js.erb index 8c6c37c6..3556dc45 100644 --- a/assets/javascripts/wizard-raw-templates.js.erb +++ b/assets/javascripts/wizard-raw-templates.js.erb @@ -2,19 +2,29 @@ result = '' Discourse.unofficial_plugins.each do |plugin| plugin_name = plugin.metadata.name - if require_plugin_assets = CustomWizard::Field.require_assets[plugin_name] + + if plugin_name == 'discourse-custom-wizard' || CustomWizard::Field.require_assets[plugin_name] + + files = [] + plugin.each_globbed_asset do |f, is_dir| - if f.include? "raw.hbs" - name = File.basename(f, ".raw.hbs") - compiled = Barber::Precompiler.new().compile(File.read(f)) - result << " - (function() { - if ('Wizard' in window) { - Wizard.RAW_TEMPLATES['javascripts/#{name}'] = requirejs('discourse-common/lib/raw-handlebars').template(#{compiled}); - } - })(); - " - end + files.push(f) if f.include? "raw.hbs" + end + + Dir.glob("#{Rails.root}/app/assets/javascripts/discourse/templates/*.raw.hbs").each do |f| + files.push(f) + end + + files.each do |f| + name = File.basename(f, ".raw.hbs") + compiled = Barber::Precompiler.new().compile(File.read(f)) + result << " + (function() { + if ('Wizard' in window) { + Discourse.RAW_TEMPLATES['javascripts/#{name}'] = requirejs('discourse-common/lib/raw-handlebars').template(#{compiled}); + } + })(); + " end end end diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 new file mode 100644 index 00000000..4ec87cb3 --- /dev/null +++ b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 @@ -0,0 +1,78 @@ +import ComposerEditor from 'discourse/components/composer-editor'; +import { default as computed, on } from 'ember-addons/ember-computed-decorators'; +import { findRawTemplate } from "discourse/lib/raw-templates"; +import { throttle } from "@ember/runloop"; +import { scheduleOnce } from "@ember/runloop"; +import { safariHacksDisabled } from "discourse/lib/utilities"; + +export default ComposerEditor.extend({ + classNameBindings: ['fieldClass'], + allowUpload: false, + showLink: false, + topic: null, + showToolbar: true, + focusTarget: "reply", + canWhisper: false, + lastValidatedAt: 'lastValidatedAt', + uploadIcon: "upload", + popupMenuOptions: [], + draftStatus: 'null', + + @on("didInsertElement") + _composerEditorInit() { + const $input = $(this.element.querySelector(".d-editor-input")); + const $preview = $(this.element.querySelector(".d-editor-preview-wrapper")); + + if (this.siteSettings.enable_mentions) { + $input.autocomplete({ + template: findRawTemplate("user-selector-autocomplete"), + dataSource: term => this.userSearchTerm.call(this, term), + key: "@", + transformComplete: v => v.username || v.name, + afterComplete() { + // ensures textarea scroll position is correct + scheduleOnce("afterRender", () => $input.blur().focus()); + } + }); + } + + if (this._enableAdvancedEditorPreviewSync()) { + this._initInputPreviewSync($input, $preview); + } else { + $input.on("scroll", () => + throttle(this, this._syncEditorAndPreviewScroll, $input, $preview, 20) + ); + } + + this._bindUploadTarget(); + }, + + _bindUploadTarget() { + }, + + _unbindUploadTarget() { + }, + + actions: { + extraButtons(toolbar) { + if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { + toolbar.addButton({ + id: "upload", + group: "insertions", + icon: this.uploadIcon, + title: "upload", + sendAction: this.showUploadModal + }); + } + + toolbar.addButton({ + id: "options", + group: "extras", + icon: "cog", + title: "composer.options", + sendAction: this.onExpandPopupMenuOptions.bind(this), + popupMenu: true + }); + } + } +}) \ No newline at end of file diff --git a/assets/javascripts/wizard/components/wizard-editor.js.es6 b/assets/javascripts/wizard/components/wizard-editor.js.es6 deleted file mode 100644 index cace45c5..00000000 --- a/assets/javascripts/wizard/components/wizard-editor.js.es6 +++ /dev/null @@ -1,659 +0,0 @@ -/* eslint no-undef: 0 */ -/*global Mousetrap:true */ -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import { cookAsync } from '../lib/text-lite'; -import { getRegister } from 'discourse-common/lib/get-owner'; -import { siteDir } from 'discourse/lib/text-direction'; -import { determinePostReplaceSelection, clipboardData } from '../lib/utilities-lite'; -import toMarkdown from 'discourse/lib/to-markdown'; - -// Our head can be a static string or a function that returns a string -// based on input (like for numbered lists). -function getHead(head, prev) { - if (typeof head === "string") { - return [head, head.length]; - } else { - return getHead(head(prev)); - } -} - -function getButtonLabel(labelKey, defaultLabel) { - // use the Font Awesome icon if the label matches the default - return I18n.t(labelKey) === defaultLabel ? null : labelKey; -} - -const OP = { - NONE: 0, - REMOVED: 1, - ADDED: 2 -}; - -const FOUR_SPACES_INDENT = '4-spaces-indent'; - -const _createCallbacks = []; - -const isInside = (text, regex) => { - const matches = text.match(regex); - return matches && (matches.length % 2); -}; - -class Toolbar { - - constructor() { - this.shortcuts = {}; - - this.groups = [ - {group: 'fontStyles', buttons: []}, - {group: 'insertions', buttons: []}, - {group: 'extras', buttons: []} - ]; - - this.addButton({ - trimLeading: true, - id: 'bold', - group: 'fontStyles', - icon: 'bold', - label: getButtonLabel('wizard_composer.bold_label', 'B'), - shortcut: 'B', - perform: e => e.applySurround('**', '**', 'bold_text') - }); - - this.addButton({ - trimLeading: true, - id: 'italic', - group: 'fontStyles', - icon: 'italic', - label: getButtonLabel('wizard_composer.italic_label', 'I'), - shortcut: 'I', - perform: e => e.applySurround('_', '_', 'italic_text') - }); - - this.addButton({ - id: 'quote', - group: 'insertions', - icon: 'quote-right', - shortcut: 'Shift+9', - perform: e => e.applyList( - '> ', - 'blockquote_text', - { applyEmptyLines: true, multiline: true } - ) - }); - - this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'}); - - this.addButton({ - id: 'bullet', - group: 'extras', - icon: 'list-ul', - shortcut: 'Shift+8', - title: 'wizard_composer.ulist_title', - perform: e => e.applyList('* ', 'list_item') - }); - - this.addButton({ - id: 'list', - group: 'extras', - icon: 'list-ol', - shortcut: 'Shift+7', - title: 'wizard_composer.olist_title', - perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item') - }); - - if (Wizard.SiteSettings.support_mixed_text_direction) { - this.addButton({ - id: 'toggle-direction', - group: 'extras', - icon: 'exchange', - shortcut: 'Shift+6', - title: 'wizard_composer.toggle_direction', - perform: e => e.toggleDirection(), - }); - } - - this.groups[this.groups.length-1].lastGroup = true; - } - - addButton(button) { - const g = this.groups.findBy('group', button.group); - if (!g) { - throw `Couldn't find toolbar group ${button.group}`; - } - - const createdButton = { - id: button.id, - className: button.className || button.id, - label: button.label, - icon: button.label ? null : button.icon || button.id, - action: button.action || 'toolbarButton', - perform: button.perform || function() { }, - trimLeading: button.trimLeading, - popupMenu: button.popupMenu || false - }; - - if (button.sendAction) { - createdButton.sendAction = button.sendAction; - } - - const title = I18n.t(button.title || `wizard_composer.${button.id}_title`); - if (button.shortcut) { - const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); - const mod = mac ? 'Meta' : 'Ctrl'; - var shortcutTitle = `${mod}+${button.shortcut}`; - - // Mac users are used to glyphs for shortcut keys - if (mac) { - shortcutTitle = shortcutTitle - .replace('Shift', "\u21E7") - .replace('Meta', "\u2318") - .replace('Alt', "\u2325") - .replace(/\+/g, ''); - } else { - shortcutTitle = shortcutTitle - .replace('Shift', I18n.t('shortcut_modifier_key.shift')) - .replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl')) - .replace('Alt', I18n.t('shortcut_modifier_key.alt')); - } - - createdButton.title = `${title} (${shortcutTitle})`; - - this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton; - } else { - createdButton.title = title; - } - - if (button.unshift) { - g.buttons.unshift(createdButton); - } else { - g.buttons.push(createdButton); - } - } -} - -export default Ember.Component.extend({ - classNames: ['d-editor'], - ready: false, - lastSel: null, - _mouseTrap: null, - showPreview: false, - - _readyNow() { - this.set('ready', true); - - if (this.get('autofocus')) { - this.$('textarea').focus(); - } - }, - - init() { - this._super(); - this.register = getRegister(this); - }, - - didInsertElement() { - this._super(); - - Ember.run.scheduleOnce('afterRender', this, this._readyNow); - - const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]); - const shortcuts = this.get('toolbar.shortcuts'); - - // for some reason I am having trouble bubbling this so hack it in - mouseTrap.bind(['ctrl+alt+f'], (event) =>{ - this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event}); - return true; - }); - - Object.keys(shortcuts).forEach(sc => { - const button = shortcuts[sc]; - mouseTrap.bind(sc, () => { - this.send(button.action, button); - return false; - }); - }); - - // disable clicking on links in the preview - this.$('.d-editor-preview').on('click.preview', e => { - if ($(e.target).is("a")) { - e.preventDefault(); - return false; - } - }); - - if (this.get('composerEvents')) { - this.appEvents.on('composer:insert-block', text => this._addBlock(this._getSelected(), text)); - this.appEvents.on('composer:insert-text', (text, options) => this._addText(this._getSelected(), text, options)); - this.appEvents.on('composer:replace-text', (oldVal, newVal) => this._replaceText(oldVal, newVal)); - } - this._mouseTrap = mouseTrap; - }, - - @on('willDestroyElement') - _shutDown() { - if (this.get('composerEvents')) { - this.appEvents.off('composer:insert-block'); - this.appEvents.off('composer:insert-text'); - this.appEvents.off('composer:replace-text'); - } - - const mouseTrap = this._mouseTrap; - Object.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc)); - mouseTrap.unbind('ctrl+/','command+/'); - this.$('.d-editor-preview').off('click.preview'); - }, - - @computed - toolbar() { - const toolbar = new Toolbar(this.site); - _createCallbacks.forEach(cb => cb(toolbar)); - this.sendAction('extraButtons', toolbar); - return toolbar; - }, - - _updatePreview() { - if (this._state !== "inDOM") { return; } - - const value = this.get('value'); - const markdownOptions = this.get('markdownOptions') || {}; - - cookAsync(value, markdownOptions).then(cooked => { - if (this.get('isDestroyed')) { return; } - this.set('preview', cooked); - Ember.run.scheduleOnce('afterRender', () => { - if (this._state !== "inDOM") { return; } - const $preview = this.$('.d-editor-preview'); - if ($preview.length === 0) return; - this.sendAction('previewUpdated', $preview); - }); - }); - }, - - @observes('ready', 'value') - _watchForChanges() { - if (!this.get('ready')) { return; } - - // Debouncing in test mode is complicated - if (Ember.testing) { - this._updatePreview(); - } else { - Ember.run.debounce(this, this._updatePreview, 30); - } - }, - - _getSelected(trimLeading, opts) { - if (!this.get('ready')) { return; } - - const textarea = this.$('textarea.d-editor-input')[0]; - const value = textarea.value; - let start = textarea.selectionStart; - let end = textarea.selectionEnd; - - // trim trailing spaces cause **test ** would be invalid - while (end > start && /\s/.test(value.charAt(end-1))) { - end--; - } - - if (trimLeading) { - // trim leading spaces cause ** test** would be invalid - while(end > start && /\s/.test(value.charAt(start))) { - start++; - } - } - - const selVal = value.substring(start, end); - const pre = value.slice(0, start); - const post = value.slice(end); - - if (opts && opts.lineVal) { - const lineVal = value.split("\n")[value.substr(0, textarea.selectionStart).split("\n").length - 1]; - return { start, end, value: selVal, pre, post, lineVal }; - } else { - return { start, end, value: selVal, pre, post }; - } - }, - - _selectText(from, length) { - Ember.run.scheduleOnce('afterRender', () => { - const $textarea = this.$('textarea.d-editor-input'); - const textarea = $textarea[0]; - const oldScrollPos = $textarea.scrollTop(); - if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) { - $textarea.focus(); - } - textarea.selectionStart = from; - textarea.selectionEnd = textarea.selectionStart + length; - $textarea.scrollTop(oldScrollPos); - }); - }, - - // perform the same operation over many lines of text - _getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) { - let operation = OP.NONE; - - const applyEmptyLines = opts && opts.applyEmptyLines; - - return lines.map(l => { - if (!applyEmptyLines && l.length === 0) { - return l; - } - - if (operation !== OP.ADDED && - (l.slice(0, hlen) === hval && tlen === 0 || - (tail.length && l.slice(-tlen) === tail))) { - - operation = OP.REMOVED; - if (tlen === 0) { - const result = l.slice(hlen); - [hval, hlen] = getHead(head, hval); - return result; - } else if (l.slice(-tlen) === tail) { - const result = l.slice(hlen, -tlen); - [hval, hlen] = getHead(head, hval); - return result; - } - } else if (operation === OP.NONE) { - operation = OP.ADDED; - } else if (operation === OP.REMOVED) { - return l; - } - - const result = `${hval}${l}${tail}`; - [hval, hlen] = getHead(head, hval); - return result; - }).join("\n"); - }, - - _applySurround(sel, head, tail, exampleKey, opts) { - const pre = sel.pre; - const post = sel.post; - - const tlen = tail.length; - if (sel.start === sel.end) { - if (tlen === 0) { return; } - - const [hval, hlen] = getHead(head); - const example = I18n.t(`wizard_composer.${exampleKey}`); - this.set('value', `${pre}${hval}${example}${tail}${post}`); - this._selectText(pre.length + hlen, example.length); - } else if (opts && !opts.multiline) { - const [hval, hlen] = getHead(head); - - if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) { - this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tail.length)}`); - this._selectText(sel.start - hlen, sel.value.length); - } else { - this.set('value', `${pre}${hval}${sel.value}${tail}${post}`); - this._selectText(sel.start + hlen, sel.value.length); - } - } else { - const lines = sel.value.split("\n"); - - let [hval, hlen] = getHead(head); - if (lines.length === 1 && pre.slice(-tlen) === tail && post.slice(0, hlen) === hval) { - this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`); - this._selectText(sel.start - hlen, sel.value.length); - } else { - const contents = this._getMultilineContents( - lines, - head, - hval, - hlen, - tail, - tlen, - opts - ); - - this.set('value', `${pre}${contents}${post}`); - if (lines.length === 1 && tlen > 0) { - this._selectText(sel.start + hlen, sel.value.length); - } else { - this._selectText(sel.start, contents.length); - } - } - } - }, - - _applyList(sel, head, exampleKey, opts) { - if (sel.value.indexOf("\n") !== -1) { - this._applySurround(sel, head, '', exampleKey, opts); - } else { - - const [hval, hlen] = getHead(head); - if (sel.start === sel.end) { - sel.value = I18n.t(`wizard_composer.${exampleKey}`); - } - - const trimmedPre = sel.pre.trim(); - const number = (sel.value.indexOf(hval) === 0) ? sel.value.slice(hlen) : `${hval}${sel.value}`; - const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : ""; - - const trimmedPost = sel.post.trim(); - const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost; - - this.set('value', `${preLines}${number}${post}`); - this._selectText(preLines.length, number.length); - } - }, - - _replaceText(oldVal, newVal) { - const val = this.get('value'); - const needleStart = val.indexOf(oldVal); - - if (needleStart === -1) { - // Nothing to replace. - return; - } - - const textarea = this.$('textarea.d-editor-input')[0]; - - // Determine post-replace selection. - const newSelection = determinePostReplaceSelection({ - selection: { start: textarea.selectionStart, end: textarea.selectionEnd }, - needle: { start: needleStart, end: needleStart + oldVal.length }, - replacement: { start: needleStart, end: needleStart + newVal.length } - }); - - // Replace value (side effect: cursor at the end). - this.set('value', val.replace(oldVal, newVal)); - - // Restore cursor. - this._selectText(newSelection.start, newSelection.end - newSelection.start); - }, - - _addBlock(sel, text) { - text = (text || '').trim(); - if (text.length === 0) { - return; - } - - let pre = sel.pre; - let post = sel.value + sel.post; - - if (pre.length > 0) { - pre = pre.replace(/\n*$/, "\n\n"); - } - - if (post.length > 0) { - post = post.replace(/^\n*/, "\n\n"); - } else { - post = "\n"; - } - - const value = pre + text + post; - const $textarea = this.$('textarea.d-editor-input'); - - this.set('value', value); - - $textarea.val(value); - $textarea.prop("selectionStart", (pre+text).length + 2); - $textarea.prop("selectionEnd", (pre+text).length + 2); - - Ember.run.scheduleOnce("afterRender", () => $textarea.focus()); - }, - - _addText(sel, text, options) { - const $textarea = this.$('textarea.d-editor-input'); - - if (options && options.ensureSpace) { - if ((sel.pre + '').length > 0) { - if (!sel.pre.match(/\s$/)) { - text = ' ' + text; - } - } - if ((sel.post + '').length > 0) { - if (!sel.post.match(/^\s/)) { - text = text + ' '; - } - } - } - - const insert = `${sel.pre}${text}`; - const value = `${insert}${sel.post}`; - this.set('value', value); - $textarea.val(value); - $textarea.prop("selectionStart", insert.length); - $textarea.prop("selectionEnd", insert.length); - Ember.run.scheduleOnce("afterRender", () => $textarea.focus()); - }, - - _extractTable(text) { - if (text.endsWith("\n")) { - text = text.substring(0, text.length - 1); - } - - let rows = text.split("\n"); - - if (rows.length > 1) { - const columns = rows.map(r => r.split("\t").length); - const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1) && - !(columns[0] === 2 && rows[0].split("\t")[0].match(/^•$|^\d+.$/)); // to skip tab delimited lists - - if (isTable) { - const splitterRow = [...Array(columns[0])].map(() => "---").join("\t"); - rows.splice(1, 0, splitterRow); - - return "|" + rows.map(r => r.split("\t").join("|")).join("|\n|") + "|\n"; - } - } - return null; - }, - - _toggleDirection() { - const $textArea = $(".d-editor-input"); - let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(), - newDir = currentDir === 'ltr' ? 'rtl' : 'ltr'; - - $textArea.attr('dir', newDir).focus(); - }, - - paste(e) { - if (!$(".d-editor-input").is(":focus")) { - return; - } - - const isComposer = $("#reply-control .d-editor-input").is(":focus"); - let { clipboard, canPasteHtml } = clipboardData(e, isComposer); - - let plainText = clipboard.getData("text/plain"); - let html = clipboard.getData("text/html"); - let handled = false; - - if (plainText) { - plainText = plainText.trim().replace(/\r/g,""); - const table = this._extractTable(plainText); - if (table) { - this.appEvents.trigger('composer:insert-text', table); - handled = true; - } - } - - const { pre, lineVal } = this._getSelected(null, {lineVal: true}); - const isInlinePasting = pre.match(/[^\n]$/); - - if (canPasteHtml && plainText) { - if (isInlinePasting) { - canPasteHtml = !(lineVal.match(/^```/) || isInside(pre, /`/g) || lineVal.match(/^ /)); - } else { - canPasteHtml = !isInside(pre, /(^|\n)```/g); - } - } - - if (canPasteHtml && !handled) { - let markdown = toMarkdown(html); - - if (!plainText || plainText.length < markdown.length) { - if(isInlinePasting) { - markdown = markdown.replace(/^#+/, "").trim(); - markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown; - } - - this.appEvents.trigger('composer:insert-text', markdown); - handled = true; - } - } - - if (handled) { - e.preventDefault(); - } - }, - - keyPress(e) { - if (e.keyCode === 13) { - const selected = this._getSelected(); - this._addText(selected, '\n'); - return false; - } - }, - - actions: { - toolbarButton(button) { - const selected = this._getSelected(button.trimLeading); - const toolbarEvent = { - selected, - selectText: (from, length) => this._selectText(from, length), - applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts), - applyList: (head, exampleKey, opts) => this._applyList(selected, head, exampleKey, opts), - addText: text => this._addText(selected, text), - replaceText: text => this._addText({pre: '', post: ''}, text), - getText: () => this.get('value'), - toggleDirection: () => this._toggleDirection(), - }; - - if (button.sendAction) { - return this.sendAction(button.sendAction, toolbarEvent); - } else { - button.perform(toolbarEvent); - } - }, - - formatCode() { - const sel = this._getSelected('', { lineVal: true }); - const selValue = sel.value; - const hasNewLine = selValue.indexOf("\n") !== -1; - const isBlankLine = sel.lineVal.trim().length === 0; - const isFourSpacesIndent = this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT; - - if (!hasNewLine) { - if (selValue.length === 0 && isBlankLine) { - if (isFourSpacesIndent) { - const example = I18n.t(`wizard_composer.code_text`); - this.set('value', `${sel.pre} ${example}${sel.post}`); - return this._selectText(sel.pre.length + 4, example.length); - } else { - return this._applySurround(sel, "```\n", "\n```", 'paste_code_text'); - } - } else { - return this._applySurround(sel, '`', '`', 'code_title'); - } - } else { - if (isFourSpacesIndent) { - return this._applySurround(sel, ' ', '', 'code_text'); - } else { - const preNewline = (sel.pre[-1] !== "\n" && sel.pre !== "") ? "\n" : ""; - const postNewline = sel.post[0] !== "\n" ? "\n" : ""; - return this._addText(sel, `${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`); - } - } - } - } -}); diff --git a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 b/assets/javascripts/wizard/components/wizard-field-composer.js.es6 index 86f5dd9e..50e09301 100644 --- a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 +++ b/assets/javascripts/wizard/components/wizard-field-composer.js.es6 @@ -1,6 +1,22 @@ -import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ + showPreview: false, + elementId: "reply-control", + classNameBindings: ["showPreview:show-preview:hide-preview"], + + didInsertElement() { + this.set('composer', Ember.Object.create({ + loading: false, + reply: this.get('field.value') + })) + }, + + @observes('composer.reply') + setField() { + this.set('field.value', this.get('composer.reply')); + }, + @computed('showPreview') togglePreviewLabel(showPreview) { return showPreview ? 'wizard_composer.hide_preview' : 'wizard_composer.show_preview'; @@ -9,6 +25,21 @@ export default Ember.Component.extend({ actions: { togglePreview() { this.toggleProperty('showPreview'); + }, + + groupsMentioned() { + }, + afterRefresh() { + }, + storeToolbarState() { + }, + cannotSeeMention() { + }, + importQuote() { + }, + onPopupMenuAction() { + }, + showUploadSelector() { } } }); diff --git a/assets/javascripts/wizard/custom-wizard.js.es6 b/assets/javascripts/wizard/custom-wizard.js.es6 index 352bbbf6..457c100b 100644 --- a/assets/javascripts/wizard/custom-wizard.js.es6 +++ b/assets/javascripts/wizard/custom-wizard.js.es6 @@ -1,5 +1,35 @@ -import WizardApplication from 'wizard/wizard'; +import { buildResolver } from "discourse-common/resolver"; -export default WizardApplication.extend({ - rootElement: '#custom-wizard-main' +export default Ember.Application.extend({ + rootElement: '#custom-wizard-main', + Resolver: buildResolver("wizard"), + + start() { + Object.keys(requirejs._eak_seen).forEach(key => { + if (/\/initializers\//.test(key)) { + const module = requirejs(key, null, null, true); + if (!module) { + throw new Error(key + " must export an initializer."); + } + this.initializer(module.default); + } + }); + + Object.keys(requirejs._eak_seen).forEach((key) => { + if (/\/pre\-initializers\//.test(key)) { + const module = requirejs(key, null, null, true); + if (!module) { + throw new Error(key + " must export an initializer."); + } + + const init = module.default; + const oldInitialize = init.initialize; + init.initialize = () => { + oldInitialize.call(this, this.__container__, this); + }; + + this.initializer(init); + } + }); + } }); diff --git a/assets/javascripts/wizard/helpers/plugin-outlet.js.es6 b/assets/javascripts/wizard/helpers/plugin-outlet.js.es6 new file mode 100644 index 00000000..c36e5346 --- /dev/null +++ b/assets/javascripts/wizard/helpers/plugin-outlet.js.es6 @@ -0,0 +1,5 @@ +import { registerUnbound } from "discourse-common/lib/helpers"; + +export default registerUnbound("plugin-outlet", function(attrs) { + return new Handlebars.SafeString(''); +}); \ No newline at end of file diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6 index 5c019f32..04dd7188 100644 --- a/assets/javascripts/wizard/initializers/custom.js.es6 +++ b/assets/javascripts/wizard/initializers/custom.js.es6 @@ -20,10 +20,14 @@ export default { const Singleton = requirejs("discourse/mixins/singleton").default; const WizardFieldDropdown = requirejs('wizard/components/wizard-field-dropdown').default; const Store = requirejs("discourse/models/store").default; + const registerRawHelpers = requirejs("discourse-common/lib/raw-handlebars-helpers").registerRawHelpers; + const RawHandlebars = requirejs("discourse-common/lib/raw-handlebars").default; Discourse.__container__ = app.__container__; Discourse.getURLWithCDN = getUrl; Discourse.getURL = getUrl; + + registerRawHelpers(RawHandlebars, Handlebars); WizardFieldDropdown.reopen({ tagName: 'span', @@ -65,6 +69,8 @@ export default { app.register("site:main", site); targets.forEach(t => app.inject(t, "site", "site:main")); + targets.forEach(t => app.inject(t, "appEvents", "service:app-events")); + site.reopenClass(Singleton); site.currentProp('can_create_tag', false); @@ -138,6 +144,9 @@ export default { .catch(() => this.animateInvalidFields()) .finally(() => this.set('saving', false)); }, + + keyPress(key) { + }, actions: { quit() { diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs new file mode 100644 index 00000000..64504e8d --- /dev/null +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -0,0 +1,19 @@ +{{d-editor + tabindex="4" + value=composer.reply + placeholder=replyPlaceholder + previewUpdated=(action "previewUpdated") + markdownOptions=markdownOptions + extraButtons=(action "extraButtons") + importQuote=(action "importQuote") + showUploadModal=showUploadModal + togglePreview=(action "togglePreview") + validation=validation + loading=composer.loading + showLink=showLink + composerEvents=true + onExpandPopupMenuOptions=(action "onExpandPopupMenuOptions") + onPopupMenuAction=onPopupMenuAction + popupMenuOptions=popupMenuOptions + disabled=disableTextarea + outletArgs=(hash composer=composer editorType="composer")}} \ No newline at end of file diff --git a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs index ca97246d..0f89e972 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs @@ -1,4 +1,14 @@ -{{wizard-editor showPreview=showPreview value=field.value placeholder=field.placeholder}} - \ No newline at end of file diff --git a/assets/stylesheets/wizard/wizard_composer.scss b/assets/stylesheets/wizard/wizard_composer.scss index fa8c8a62..9cd674f8 100644 --- a/assets/stylesheets/wizard/wizard_composer.scss +++ b/assets/stylesheets/wizard/wizard_composer.scss @@ -44,6 +44,14 @@ } } +#reply-control.show-preview .d-editor-textarea-wrapper { + display: none; +} + +.custom-wizard #reply-control .toggle-preview { + margin-top: 20px; +} + .d-editor-textarea-wrapper, .d-editor-preview-wrapper { background-color: $secondary; @@ -132,6 +140,7 @@ .d-editor-preview { height: auto; + padding: 10px; } .d-editor-plugin { @@ -163,6 +172,10 @@ resize: vertical; flex: initial; } + + textarea { + min-height: calc(200px - 32px); + } } .d-editor-modal.hidden {