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}}