diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6 index dbdf7d84177..3b0c22f891b 100644 --- a/app/assets/javascripts/discourse/components/mount-widget.js.es6 +++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6 @@ -2,6 +2,12 @@ import { diff, patch } from 'virtual-dom'; import { WidgetClickHook } from 'discourse/widgets/click-hook'; import { renderedKey } from 'discourse/widgets/widget'; +const _cleanCallbacks = {}; +export function addWidgetCleanCallback(widgetName, fn) { + _cleanCallbacks[widgetName] = _cleanCallbacks[widgetName] || []; + _cleanCallbacks[widgetName].push(fn); +} + export default Ember.Component.extend({ _tree: null, _rootNode: null, @@ -22,6 +28,13 @@ export default Ember.Component.extend({ this._timeout = Ember.run.scheduleOnce('render', this, this.rerenderWidget); }, + willClearRender() { + const callbacks = _cleanCallbacks[this.get('widget')]; + if (callbacks) { + callbacks.forEach(cb => cb()); + } + }, + willDestroyElement() { Ember.run.cancel(this._timeout); }, diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 5995bd2b06f..073a078239d 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -4,6 +4,7 @@ import { addPosterIcon } from 'discourse/widgets/poster-name'; import { addButton } from 'discourse/widgets/post-menu'; import { includeAttributes } from 'discourse/lib/transform-post'; import { addToolbarCallback } from 'discourse/components/d-editor'; +import { addWidgetCleanCallback } from 'discourse/components/mount-widget'; let _decorateId = 0; function decorate(klass, evt, cb) { @@ -29,23 +30,31 @@ class PluginApi { } /** - * decorateCooked(callback) + * decorateCooked(callback, options) * * Used for decorating the `cooked` content of a post after it is rendered using * jQuery. * * `callback` will be called when it is time to decorate with a jQuery selector. * + * Use `options.onlyStream` if you only want to decorate posts within a topic, + * and not in other places like the user stream. + * * For example, to add a yellow background to all posts you could do this: * * ``` * api.decorateCooked($elem => $elem.css({ backgroundColor: 'yellow' })); * ``` **/ - decorateCooked(cb) { - addDecorator(cb); - decorate(ComposerEditor, 'previewRefreshed', cb); - decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', cb); + decorateCooked(callback, opts) { + opts = opts || {}; + + addDecorator(callback); + + if (!opts.onlyStream) { + decorate(ComposerEditor, 'previewRefreshed', callback); + decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', callback); + } } /** @@ -91,6 +100,10 @@ class PluginApi { addToolbarCallback(callback); } + cleanupStream(fn) { + addWidgetCleanCallback('post-stream', fn); + } + } let _pluginv01; diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 index 34f8b088995..6aebe200e5c 100644 --- a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 @@ -10,10 +10,11 @@ export function addDecorator(cb) { export default class PostCooked { - constructor(attrs) { + constructor(attrs, getModel) { this.attrs = attrs; this.expanding = false; this._highlighted = false; + this.getModel = getModel; } update(prev) { @@ -29,7 +30,7 @@ export default class PostCooked { this._fixImageSizes($html); this._applySearchHighlight($html); - _decorators.forEach(cb => cb($html)); + _decorators.forEach(cb => cb($html, this.getModel)); return $html[0]; } diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index f17c9904de6..ec36fe6f5b0 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -221,6 +221,17 @@ createWidget('expand-post-button', { } }); +class DecoratorHelper { + constructor(widget) { + this.container = widget.container; + this._widget = widget; + } + + getModel() { + return this._widget.findAncestorModel(); + } +} + createWidget('post-contents', { buildKey: attrs => `post-contents-${attrs.id}`, @@ -240,7 +251,7 @@ createWidget('post-contents', { }, html(attrs, state) { - const result = [new PostCooked(attrs)]; + const result = [new PostCooked(attrs, new DecoratorHelper(this))]; if (attrs.cooked_hidden) { result.push(this.attach('expand-hidden', attrs)); diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 index f96da54dd7c..c27957849d9 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 @@ -1,12 +1,8 @@ -import PostView from "discourse/views/post"; -import TopicController from "discourse/controllers/topic"; -import Post from "discourse/models/post"; - -import { on } from "ember-addons/ember-computed-decorators"; +import { withPluginApi } from 'discourse/lib/plugin-api'; function createPollView(container, post, poll, vote) { - const controller = container.lookup("controller:poll", { singleton: false }), - view = container.lookup("view:poll"); + const controller = container.lookup("controller:poll", { singleton: false }); + const view = container.lookup("view:poll"); controller.set("vote", vote); controller.setProperties({ model: poll, post }); @@ -15,88 +11,95 @@ function createPollView(container, post, poll, vote) { return view; } +let _pollViews; + +function initializePolls(api) { + + const TopicController = api.container.lookupFactory('controller:topic'); + TopicController.reopen({ + subscribe(){ + this._super(); + this.messageBus.subscribe("/polls/" + this.get("model.id"), msg => { + const post = this.get('model.postStream').findLoadedPost(msg.post_id); + if (post) { + post.set('polls', msg.polls); + } + }); + }, + unsubscribe(){ + this.messageBus.unsubscribe('/polls/*'); + this._super(); + } + }); + + const Post = api.container.lookupFactory('model:post'); + Post.reopen({ + _polls: null, + pollsObject: null, + + // we need a proper ember object so it is bindable + pollsChanged: function(){ + const polls = this.get("polls"); + if (polls) { + this._polls = this._polls || {}; + _.map(polls, (v,k) => { + const existing = this._polls[k]; + if (existing) { + this._polls[k].setProperties(v); + } else { + this._polls[k] = Em.Object.create(v); + } + }); + this.set("pollsObject", this._polls); + } + }.observes("polls") + }); + + function cleanUpPollViews() { + if (_pollViews) { + Object.keys(_pollViews).forEach(pollName => _pollViews[pollName].destroy()); + } + _pollViews = null; + } + + function createPollViews($elem, helper) { + const $polls = $('.poll', $elem); + if (!$polls.length) { return; } + + const post = helper.getModel(); + const votes = post.get('polls_votes') || {}; + + post.pollsChanged(); + + const polls = post.get("pollsObject"); + if (!polls) { return; } + + cleanUpPollViews(); + const postPollViews = {}; + + $polls.each((idx, pollElem) => { + const $div = $("<div>"); + const $poll = $(pollElem); + + const pollName = $poll.data("poll-name"); + const pollView = createPollView(helper.container, post, polls[pollName], votes[pollName]); + + $poll.replaceWith($div); + Em.run.next(() => pollView.renderer.replaceIn(pollView, $div[0])); + postPollViews[pollName] = pollView; + }); + + _pollViews = postPollViews; + } + + api.decorateCooked(createPollViews, { onlyStream: true }); + api.cleanupStream(cleanUpPollViews); +} + export default { name: "extend-for-poll", - initialize(container) { - - Post.reopen({ - // we need a proper ember object so it is bindable - pollsChanged: function(){ - const polls = this.get("polls"); - if (polls) { - this._polls = this._polls || {}; - _.map(polls, (v,k) => { - const existing = this._polls[k]; - if (existing) { - this._polls[k].setProperties(v); - } else { - this._polls[k] = Em.Object.create(v); - } - }); - this.set("pollsObject", this._polls); - } - }.observes("polls") - }); - - TopicController.reopen({ - subscribe(){ - this._super(); - this.messageBus.subscribe("/polls/" + this.get("model.id"), msg => { - const post = this.get('model.postStream').findLoadedPost(msg.post_id); - if (post) { - post.set('polls', msg.polls); - } - }); - }, - unsubscribe(){ - this.messageBus.unsubscribe('/polls/*'); - this._super(); - } - }); - - // overwrite polls - PostView.reopen({ - - @on("postViewInserted", "postViewUpdated") - _createPollViews($post) { - const post = this.get("post"), - votes = post.get("polls_votes") || {}; - - post.pollsChanged(); - const polls = post.get("pollsObject"); - - // don't even bother when there's no poll - if (!polls) { return; } - - // TODO inject cleanly into - - // clean-up if needed - this._cleanUpPollViews(); - - const pollViews = {}; - - // iterate over all polls - $(".poll", $post).each(function() { - const $div = $("<div>"), - $poll = $(this), - pollName = $poll.data("poll-name"), - pollView = createPollView(container, post, polls[pollName], votes[pollName]); - - $poll.replaceWith($div); - Em.run.next(() => pollView.renderer.replaceIn(pollView, $div[0])); - pollViews[pollName] = pollView; - }); - - this.set("pollViews", pollViews); - }, - - @on("willClearRender") - _cleanUpPollViews() { - if (this.get("pollViews")) { - _.forEach(this.get("pollViews"), v => v.destroy()); - } - } - }); + initialize() { + withPluginApi('0.1', initializePolls); } };