From 3f9c4100eff34dd965778b8b9dd465efebebee29 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 May 2014 15:32:42 -0400 Subject: [PATCH] Many Plugin upgrades. --- app/assets/javascripts/discourse.js | 12 +- .../components/composer-text-area.js.es6 | 14 ++ .../discourse/controllers/composer.js.es6 | 147 ++++++++---------- .../javascripts/discourse/ember/resolver.js | 4 + .../discourse/helpers/plugin-outlet.js.es6 | 84 ++++++++++ .../templates/application.js.handlebars | 1 + .../templates/composer.js.handlebars | 6 +- .../discourse/templates/topic.js.handlebars | 1 + .../discourse/views/composer/composer_view.js | 32 +--- lib/plugin/instance.rb | 9 ++ 10 files changed, 194 insertions(+), 116 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/composer-text-area.js.es6 create mode 100644 app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 393c7bedc89..5c609ff0c79 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -117,12 +117,12 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { start: function() { // Load any ES6 initializers - Ember.keys(requirejs._eak_seen).filter(function(key) { - return (/\/initializers\//).test(key); - }).forEach(function(moduleName) { - var module = require(moduleName, null, null, true); - if (!module) { throw new Error(moduleName + ' must export an initializer.'); } - Discourse.initializer(module.default); + Ember.keys(requirejs._eak_seen).forEach(function(key) { + if (/\/initializers\//.test(key)) { + var module = require(key, null, null, true); + if (!module) { throw new Error(key + ' must export an initializer.'); } + Discourse.initializer(module.default); + } }); var initializers = this.initializers; diff --git a/app/assets/javascripts/discourse/components/composer-text-area.js.es6 b/app/assets/javascripts/discourse/components/composer-text-area.js.es6 new file mode 100644 index 00000000000..b6d66d2f022 --- /dev/null +++ b/app/assets/javascripts/discourse/components/composer-text-area.js.es6 @@ -0,0 +1,14 @@ +export default Ember.TextArea.extend({ + placeholder: function() { + return I18n.t(this.get('placeholderKey')); + }.property('placeholderKey'), + + _signalParentInsert: function() { + return this.get('parentView').childDidInsertElement(this); + }.on('didInsertElement'), + + _signalParentDestroy: function() { + return this.get('parentView').childWillDestroyElement(this); + }.on('willDestroyElement') +}); + diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index fc5027a2f61..5aaa5e85e2c 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -15,10 +15,9 @@ export default Discourse.Controller.extend({ showEditReason: false, editReason: null, - init: function() { - this._super(); - this.set('similarTopics', Em.A()); - }, + _initializeSimilar: function() { + this.set('similarTopics', []); + }.on('init'), actions: { // Toggle the reply view @@ -45,7 +44,20 @@ export default Discourse.Controller.extend({ displayEditReason: function() { this.set("showEditReason", true); - } + }, + + hitEsc: function() { + if (this.get('model.viewOpen')) { + this.shrink(); + } + }, + + openIfDraft: function() { + if (this.get('model.viewDraft')) { + this.set('model.composeState', Discourse.Composer.OPEN); + } + }, + }, updateDraftStatus: function() { @@ -233,89 +245,74 @@ export default Discourse.Controller.extend({ open: function(opts) { if (!opts) opts = {}; - this.setProperties({ - showEditReason: false, - editReason: null - }); - - var composerMessages = this.get('controllers.composerMessages'); - composerMessages.reset(); - - var promise = opts.promise || Ember.Deferred.create(); - opts.promise = promise; - if (!opts.draftKey) { alert("composer was opened without a draft key"); throw "composer opened without a proper draft key"; } - // ensure we have a view now, without it transitions are going to be messed - var view = this.get('view'); - var self = this; - if (!view) { + var composerMessages = this.get('controllers.composerMessages'), + self = this, + composerModel = this.get('model'); - // TODO: We should refactor how composer is inserted. It should probably use a - // {{render}} and then the controller and view will be wired up automatically. - var appView = Discourse.__container__.lookup('view:application'); - view = appView.createChildView(Discourse.ComposerView, {controller: this}); - view.appendTo($('#main')); - this.set('view', view); + this.setProperties({ showEditReason: false, editReason: null }); + composerMessages.reset(); + this.set('view', this.container.lookup('view:composer')); - // the next runloop is too soon, need to get the control rendered and then - // we need to change stuff, otherwise css animations don't kick in - Em.run.next(function() { - Em.run.next(function() { - self.open(opts); - }); - }); - return promise; - } - - var composer = this.get('model'); - if (composer && opts.draftKey !== composer.draftKey && composer.composeState === Discourse.Composer.DRAFT) { + // If we want a different draft than the current composer, close it and clear our model. + if (composerModel && opts.draftKey !== composerModel.draftKey && + composerModel.composeState === Discourse.Composer.DRAFT) { this.close(); - composer = null; + composerModel = null; } - if (composer && !opts.tested && composer.get('replyDirty')) { - if (composer.composeState === Discourse.Composer.DRAFT && composer.draftKey === opts.draftKey && composer.action === opts.action) { - composer.set('composeState', Discourse.Composer.OPEN); - promise.resolve(); - return promise; - } else { - opts.tested = true; - if (!opts.ignoreIfChanged) { - this.cancelComposer().then(function() { self.open(opts); }).catch(function() { return promise.reject(); }); + return new Ember.RSVP.Promise(function(resolve, reject) { + if (composerModel && composerModel.get('replyDirty')) { + if (composerModel.get('composeState') === Discourse.Composer.DRAFT && + composerModel.get('draftKey') === opts.draftKey && + composerModel.action === opts.action) { + + // If it's the same draft, just open it up again. + composerModel.set('composeState', Discourse.Composer.OPEN); + return resolve(); + } else { + // If it's a different draft, cancel it and try opening again. + return self.cancelComposer().then(function() { + return self.open(opts); + }).then(resolve, reject); } - return promise; } - } - // we need a draft sequence, without it drafts are bust - if (opts.draftSequence === void 0) { - Discourse.Draft.get(opts.draftKey).then(function(data) { - opts.draftSequence = data.draft_sequence; - opts.draft = data.draft; - return self.open(opts); - }); - return promise; - } + // we need a draft sequence for the composer to work + if (opts.draftSequence === void 0) { + return Discourse.Draft.get(opts.draftKey).then(function(data) { + opts.draftSequence = data.draft_sequence; + opts.draft = data.draft; + self._setModel(composerModel, opts); + }).then(resolve, reject); + } + self._setModel(composerModel, opts); + resolve(); + }); + }, + + // Given a potential instance and options, set the model for this composer. + _setModel: function(composerModel, opts) { if (opts.draft) { - composer = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft); - if (composer) { - composer.set('topic', opts.topic); + composerModel = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft); + if (composerModel) { + composerModel.set('topic', opts.topic); } } else { - composer = composer || Discourse.Composer.create(); - composer.open(opts); + composerModel = composerModel || Discourse.Composer.create(); + composerModel.open(opts); } - this.set('model', composer); - composer.set('composeState', Discourse.Composer.OPEN); - composerMessages.queryFor(this.get('model')); - promise.resolve(); - return promise; + this.set('model', composerModel); + composerModel.set('composeState', Discourse.Composer.OPEN); + + var composerMessages = this.get('controllers.composerMessages'); + composerMessages.queryFor(composerModel); }, // View a new reply we've made @@ -356,11 +353,6 @@ export default Discourse.Controller.extend({ }); }, - openIfDraft: function() { - if (this.get('model.viewDraft')) { - this.set('model.composeState', Discourse.Composer.OPEN); - } - }, shrink: function() { if (this.get('model.replyDirty')) { @@ -388,13 +380,6 @@ export default Discourse.Controller.extend({ $('#wmd-input').autocomplete({ cancel: true }); }, - // ESC key hit - hitEsc: function() { - if (this.get('model.viewOpen')) { - this.shrink(); - } - }, - showOptions: function() { var _ref; return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({ diff --git a/app/assets/javascripts/discourse/ember/resolver.js b/app/assets/javascripts/discourse/ember/resolver.js index 5794055600e..831fc397b42 100644 --- a/app/assets/javascripts/discourse/ember/resolver.js +++ b/app/assets/javascripts/discourse/ember/resolver.js @@ -65,6 +65,10 @@ Discourse.Resolver = Ember.DefaultResolver.extend({ return this.customResolve(parsedName) || this._super(parsedName); }, + resolveHelper: function(parsedName) { + return this.customResolve(parsedName) || this._super(parsedName); + }, + resolveController: function(parsedName) { return this.customResolve(parsedName) || this._super(parsedName); }, diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 new file mode 100644 index 00000000000..e0f95f575d6 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -0,0 +1,84 @@ +/** + A plugin outlet is an extension point for templates where other templates can + be inserted by plugins. + + If you handlebars template has: + + ```handlebars + {{plugin-outlet "evil-trout"}} + ``` + + Then any handlebars files you create in the `connectors/evil-trout` directory + will automatically be appended. For example: + + plugins/hello/assets/javascripts/discourse/templates/connectors/evil-trout/hello.handlebars + + With the contents: + + ```handlebars + Hello World + ``` + + Will insert Hello World at that point in the template. + + Optionally you can also define a view class for the outlet as: + + plugins/hello/assets/javascripts/discourse/views/connectors/evil-trout/hello.js.es6 + + And it will be wired up automatically. + +**/ + +var _connectorCache; + +function findOutlets(collection, callback) { + Ember.keys(collection).forEach(function(i) { + if (i.indexOf("/connectors/") !== -1) { + var segments = i.split("/"), + outletName = segments[segments.length-2], + uniqueName = segments[segments.length-1]; + + callback(outletName, i, uniqueName); + } + }); +} + +function buildConnectorCache() { + _connectorCache = {}; + + var uniqueViews = {}; + findOutlets(requirejs._eak_seen, function(outletName, idx, uniqueName) { + _connectorCache[outletName] = _connectorCache[outletName] || []; + + var viewClass = require(idx, null, null, true).default; + uniqueViews[uniqueName] = viewClass; + _connectorCache[outletName].pushObject(viewClass); + }); + + findOutlets(Ember.TEMPLATES, function(outletName, idx, uniqueName) { + _connectorCache[outletName] = _connectorCache[outletName] || []; + + var mixin = {templateName: idx.replace('javascripts/', '')}, + viewClass = uniqueViews[uniqueName]; + + if (viewClass) { + // We are going to add it back with the proper template + _connectorCache[outletName].removeObject(viewClass); + } else { + viewClass = Em.View; + } + _connectorCache[outletName].pushObject(viewClass.extend(mixin)); + }); + +} + +export default function(connectionName, options) { + if (!_connectorCache) { buildConnectorCache(); } + + if (_connectorCache[connectionName]) { + var CustomContainerView = Ember.ContainerView.extend({ + childViews: _connectorCache[connectionName].map(function(vc) { return vc.create(); }) + }); + return Ember.Handlebars.helpers.view.call(this, CustomContainerView, options); + } +} diff --git a/app/assets/javascripts/discourse/templates/application.js.handlebars b/app/assets/javascripts/discourse/templates/application.js.handlebars index 43e5273bd0d..8319dc7b42c 100644 --- a/app/assets/javascripts/discourse/templates/application.js.handlebars +++ b/app/assets/javascripts/discourse/templates/application.js.handlebars @@ -5,3 +5,4 @@ {{render "modal"}} +{{render "composer"}} diff --git a/app/assets/javascripts/discourse/templates/composer.js.handlebars b/app/assets/javascripts/discourse/templates/composer.js.handlebars index ba494254238..9aff0cb1594 100644 --- a/app/assets/javascripts/discourse/templates/composer.js.handlebars +++ b/app/assets/javascripts/discourse/templates/composer.js.handlebars @@ -9,6 +9,8 @@ {{#if model.viewOpen}}
+ {{plugin-outlet "composer-open"}} +
{{{model.actionTitle}}}: {{#if canEdit}} @@ -57,7 +59,7 @@
- {{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="model.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}} + {{composer-text-area tabindex="3" value=model.reply id="wmd-input" placeholderKey="composer.reply_placeholder"}} {{popupInputTip validation=view.replyValidation shownAt=view.showReplyTip}}
@@ -75,7 +77,7 @@ {{i18n upload}} {{/if}} {{/if}} -
+
{{model.draftStatus}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/topic.js.handlebars b/app/assets/javascripts/discourse/templates/topic.js.handlebars index dd08889096a..35146649286 100644 --- a/app/assets/javascripts/discourse/templates/topic.js.handlebars +++ b/app/assets/javascripts/discourse/templates/topic.js.handlebars @@ -48,6 +48,7 @@ {{/if}} {{/if}} + {{plugin-outlet "topic-title"}}
diff --git a/app/assets/javascripts/discourse/views/composer/composer_view.js b/app/assets/javascripts/discourse/views/composer/composer_view.js index 90e9ef8c9e0..657feff003a 100644 --- a/app/assets/javascripts/discourse/views/composer/composer_view.js +++ b/app/assets/javascripts/discourse/views/composer/composer_view.js @@ -26,15 +26,9 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { content: Em.computed.alias('model'), composeState: function() { - var state = this.get('model.composeState'); - if (state) return state; - return Discourse.Composer.CLOSED; + return this.get('model.composeState') || Discourse.Composer.CLOSED; }.property('model.composeState'), - draftStatus: function() { - $('#draft-status').text(this.get('model.draftStatus') || ""); - }.observes('model.draftStatus'), - // Disable fields when we're loading loadingChanged: function() { if (this.get('loading')) { @@ -48,7 +42,6 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { return this.present('controller.createdPost') ? 'created-post' : null; }.property('model.createdPost'), - refreshPreview: Discourse.debounce(function() { if (this.editor) { this.editor.refreshPreview(); @@ -101,7 +94,7 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { keyDown: function(e) { if (e.which === 27) { // ESC - this.get('controller').hitEsc(); + this.get('controller').send('hitEsc'); return false; } else if (e.which === 13 && (e.ctrlKey || e.metaKey)) { // CTRL+ENTER or CMD+ENTER @@ -110,12 +103,12 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { } }, - didInsertElement: function() { + _enableResizing: function() { var $replyControl = $('#reply-control'); $replyControl.DivResizer({ resize: this.resize, onDrag: this.movePanels }); Discourse.TransitionHelper.after($replyControl, this.resize); this.ensureMaximumDimensionForImagesInPreview(); - }, + }.on('didInsertElement'), ensureMaximumDimensionForImagesInPreview: function() { // This enforce maximum dimensions of images in the preview according @@ -132,7 +125,7 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { }, click: function() { - this.get('controller').openIfDraft(); + this.get('controller').send('openIfDraft'); }, // Called after the preview renders. Debounced for performance @@ -487,19 +480,4 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, { } }); -// not sure if this is the right way, keeping here for now, we could use a mixin perhaps -Discourse.NotifyingTextArea = Ember.TextArea.extend({ - placeholder: function() { - return I18n.t(this.get('placeholderKey')); - }.property('placeholderKey'), - - didInsertElement: function() { - return this.get('parent').childDidInsertElement(this); - }, - - willDestroyElement: function() { - return this.get('parent').childWillDestroyElement(this); - } -}); - RSVP.EventTarget.mixin(Discourse.ComposerView); diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 55fffea1147..2b8ec9218fd 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -23,6 +23,15 @@ class Plugin::Instance @metadata = metadata @path = path @assets = [] + + # Automatically include all ES6 JS files + if @path + dir = File.dirname(@path) + Dir.glob("#{dir}/assets/javascripts/**/*.js.es6") do |f| + relative = f.sub("#{dir}/assets/", "") + register_asset(relative) + end + end end def name