From 14dbd5381ce23d5dbf48c69fec5ca48d8aec3f20 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 5 May 2015 17:07:58 +0930 Subject: [PATCH] JS cleanup/refactor --- extensions/tags/js/bootstrap.js | 254 +++++++----------- .../tags/js/src/components/category-hero.js | 16 ++ .../js/src/components/category-nav-item.js | 22 ++ .../src/components/move-discussion-modal.js | 56 ++++ .../notification-discussion-moved.js | 3 +- .../src/components/post-discussion-moved.js | 60 +---- .../tags/js/src/helpers/category-icon.js | 12 + .../tags/js/src/helpers/category-label.js | 3 + .../tags/js/src/{ => models}/category.js | 0 extensions/tags/less/categories.less | 2 +- 10 files changed, 220 insertions(+), 208 deletions(-) create mode 100644 extensions/tags/js/src/components/category-hero.js create mode 100644 extensions/tags/js/src/components/category-nav-item.js create mode 100644 extensions/tags/js/src/components/move-discussion-modal.js create mode 100644 extensions/tags/js/src/helpers/category-icon.js create mode 100644 extensions/tags/js/src/helpers/category-label.js rename extensions/tags/js/src/{ => models}/category.js (100%) diff --git a/extensions/tags/js/bootstrap.js b/extensions/tags/js/bootstrap.js index 7eeeb273e..69c47a426 100644 --- a/extensions/tags/js/bootstrap.js +++ b/extensions/tags/js/bootstrap.js @@ -1,69 +1,59 @@ import { extend, override } from 'flarum/extension-utils'; import Model from 'flarum/model'; -import Component from 'flarum/component'; import Discussion from 'flarum/models/discussion'; import IndexPage from 'flarum/components/index-page'; import DiscussionPage from 'flarum/components/discussion-page'; import DiscussionList from 'flarum/components/discussion-list'; import DiscussionHero from 'flarum/components/discussion-hero'; import Separator from 'flarum/components/separator'; -import NavItem from 'flarum/components/nav-item'; import ActionButton from 'flarum/components/action-button'; +import NavItem from 'flarum/components/nav-item'; import ComposerDiscussion from 'flarum/components/composer-discussion'; import ActivityPost from 'flarum/components/activity-post'; import icon from 'flarum/helpers/icon'; - -import CategoriesPage from 'categories/components/categories-page'; -import Category from 'categories/category'; -import PostDiscussionMoved from 'categories/components/post-discussion-moved'; -import NotificationDiscussionMoved from 'categories/components/notification-discussion-moved'; - import app from 'flarum/app'; -Discussion.prototype.category = Model.one('category'); +import Category from 'categories/models/category'; +import CategoriesPage from 'categories/components/categories-page'; +import CategoryHero from 'categories/components/category-hero'; +import CategoryNavItem from 'categories/components/category-nav-item'; +import MoveDiscussionModal from 'categories/components/move-discussion-modal'; +import NotificationDiscussionMoved from 'categories/components/notification-discussion-moved'; +import PostDiscussionMoved from 'categories/components/post-discussion-moved'; +import categoryLabel from 'categories/helpers/category-label'; +import categoryIcon from 'categories/helpers/category-icon'; app.initializers.add('categories', function() { + + // Register routes. app.routes['categories'] = ['/categories', CategoriesPage.component()]; - - app.routes['category'] = ['/c/:categories', IndexPage.component({category: true})]; - + app.routes['category'] = ['/c/:categories', IndexPage.component()]; // @todo support combination with filters // app.routes['category.filter'] = ['/c/:slug/:filter', IndexPage.component({category: true})]; + // Register models. + app.store.models['categories'] = Category; + Discussion.prototype.category = Model.one('category'); + + // Register components. app.postComponentRegistry['discussionMoved'] = PostDiscussionMoved; app.notificationComponentRegistry['discussionMoved'] = NotificationDiscussionMoved; - app.store.model('categories', Category); + // --------------------------------------------------------------------------- + // INDEX PAGE + // --------------------------------------------------------------------------- + + // Add a category label to each discussion in the discussion list. extend(DiscussionList.prototype, 'infoItems', function(items, discussion) { var category = discussion.category(); if (category && category.slug() !== this.props.params.categories) { - items.add('category', m('span.category', {style: 'color: '+category.color()}, category.title()), {first: true}); + items.add('category', categoryLabel(category), {first: true}); } - - return items; }); - class CategoryNavItem extends NavItem { - view() { - var category = this.props.category; - var active = this.constructor.active(this.props); - return m('li'+(active ? '.active' : ''), m('a', {href: this.props.href, config: m.route, onclick: () => {app.cache.discussionList = null; m.redraw.strategy('none')}, style: active ? 'color: '+category.color() : ''}, [ - m('span.icon.category-icon', {style: 'background-color: '+category.color()}), - category.title() - ])); - } - - static props(props) { - var category = props.category; - props.params.categories = category.slug(); - props.href = app.route('category', props.params); - props.label = category.title(); - - return props; - } - } - + // Add a link to the categories page, as well as a list of all the categories, + // to the index page's sidebar. extend(IndexPage.prototype, 'navItems', function(items) { items.add('categories', NavItem.component({ icon: 'reorder', @@ -77,200 +67,156 @@ app.initializers.add('categories', function() { app.store.all('categories').forEach(category => { items.add('category'+category.id(), CategoryNavItem.component({category, params: this.stickyParams()}), {last: true}); }); - - return items; }); - extend(IndexPage.prototype, 'params', function(params) { - params.categories = this.props.category ? m.route.param('categories') : undefined; - return params; - }); - - class CategoryHero extends Component { - view() { - var category = this.props.category; - - return m('header.hero.category-hero', {style: 'background-color: '+category.color()}, [ - m('div.container', [ - m('div.container-narrow', [ - m('h2', category.title()), - m('div.subtitle', category.description()) - ]) - ]) - ]); + IndexPage.prototype.currentCategory = function() { + var slug = this.params().categories; + if (slug) { + return app.store.getBy('categories', 'slug', slug); } - } + }; + // If currently viewing a category, insert a category hero at the top of the + // view. extend(IndexPage.prototype, 'view', function(view) { - if (this.props.category) { - var slug = this.params().categories; - var category = app.store.all('categories').filter(category => category.slug() == slug)[0]; + var category = this.currentCategory(); + if (category) { view.children[0] = CategoryHero.component({category}); } - return view; }); + // If currently viewing a category, restyle the 'new discussion' button to use + // the category's color. extend(IndexPage.prototype, 'sidebarItems', function(items) { - var slug = this.params().categories; - var category = app.store.all('categories').filter(category => category.slug() == slug)[0]; + var category = this.currentCategory(); if (category) { items.newDiscussion.content.props.style = 'background-color: '+category.color(); } - return items; }); + // Add a parameter for the IndexPage to pass on to the DiscussionList that + // will let us filter discussions by category. + extend(IndexPage.prototype, 'params', function(params) { + params.categories = m.route.param('categories'); + }); + + // Translate that parameter into a gambit appended to the search query. extend(DiscussionList.prototype, 'params', function(params) { if (params.categories) { params.q = (params.q || '')+' category:'+params.categories; delete params.categories; } - return params; }); + // --------------------------------------------------------------------------- + // DISCUSSION PAGE + // --------------------------------------------------------------------------- + + // Include a discussion's category when fetching it. extend(DiscussionPage.prototype, 'params', function(params) { params.include += ',category'; - return params; }); + // Restyle a discussion's hero to use its category color. extend(DiscussionHero.prototype, 'view', function(view) { var category = this.props.discussion.category(); if (category) { view.attrs.style = 'background-color: '+category.color(); } - return view; }); + // Add the name of a discussion's category to the discussion hero, displayed + // before the title. Put the title on its own line. extend(DiscussionHero.prototype, 'items', function(items) { var category = this.props.discussion.category(); if (category) { - items.add('category', m('span.category', category.title()), {before: 'title'}); + items.add('category', m('span.category-label', category.title()), {before: 'title'}); items.title.content.wrapperClass = 'block-item'; } - return items; }); - class MoveDiscussionModal extends Component { - constructor(props) { - super(props); - - this.categories = m.prop(app.store.all('categories')); - } - - view() { - var discussion = this.props.discussion; - - return m('div.modal-dialog.modal-move-discussion', [ - m('div.modal-content', [ - m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')), - m('div.modal-header', m('h3.title-control', discussion - ? ['Move ', m('em', discussion.title()), ' from ', m('span.category', {style: 'color: '+discussion.category().color()}, discussion.category().title()), ' to...'] - : ['Start a Discussion In...'])), - m('div', [ - m('ul.category-list', [ - this.categories().map(category => - (discussion && category.id() === discussion.category().id()) ? '' : m('li.category-tile', {style: 'background-color: '+category.color()}, [ - m('a[href=javascript:;]', {onclick: this.save.bind(this, category)}, [ - m('h3.title', category.title()), - m('p.description', category.description()), - m('span.count', category.discussionsCount()+' discussions'), - ]) - ]) - ) - ]) - ]) - ]) - ]); - } - - save(category) { - var discussion = this.props.discussion; - - if (discussion) { - discussion.save({links: {category}}).then(discussion => { - if (app.current instanceof DiscussionPage) { - app.current.stream().sync(); - } - m.redraw(); - }); - } - - this.props.onchange && this.props.onchange(category); - - app.modal.close(); - - m.redraw.strategy('none'); - } - } - - function move() { - app.modal.show(new MoveDiscussionModal({discussion: this})); - } - + // Add a control allowing the discussion to be moved to another category. extend(Discussion.prototype, 'controls', function(items) { if (this.canEdit()) { items.add('move', ActionButton.component({ label: 'Move', icon: 'arrow-right', - onclick: move.bind(this) + onclick: () => app.modal.show(new MoveDiscussionModal({discussion: this})) }), {after: 'rename'}); } - - return items; }); - override(IndexPage.prototype, 'newDiscussion', function(parent) { - var categories = app.store.all('categories'); + // --------------------------------------------------------------------------- + // COMPOSER + // --------------------------------------------------------------------------- + + // When the 'new discussion' button is clicked... + override(IndexPage.prototype, 'newDiscussion', function(original) { var slug = this.params().categories; - var category; + + // If we're currently viewing a specific category, or if the user isn't + // logged in, then we'll let the core code proceed. If that results in the + // composer appearing, we'll set the composer's current category to the one + // we're viewing. if (slug || !app.session.user()) { - parent(); - if (app.composer.component) { - category = categories.filter(category => category.slug() == slug)[0]; + if (original()) { + var category = app.store.getBy('categories', 'slug', slug); app.composer.component.category(category); } } else { - var modal = new MoveDiscussionModal({onchange: category => { - parent(); - app.composer.component.category(category); - }}); + // If we're logged in and we're viewing All Discussions, we'll present the + // user with a category selection dialog before proceeding to show the + // composer. + var modal = new MoveDiscussionModal({ + onchange: category => { + original(); + app.composer.component.category(category); + } + }); app.modal.show(modal); } }); + // Add category-selection abilities to the discussion composer. + ComposerDiscussion.prototype.category = m.prop(); ComposerDiscussion.prototype.chooseCategory = function() { - var modal = new MoveDiscussionModal({onchange: category => { - this.category(category); - this.$('textarea').focus(); - }}); + var modal = new MoveDiscussionModal({ + onchange: category => { + this.category(category); + this.$('textarea').focus(); + } + }); app.modal.show(modal); }; - ComposerDiscussion.prototype.category = m.prop(); + // Add a category-selection menu to the discussion composer's header, after + // the title. extend(ComposerDiscussion.prototype, 'headerItems', function(items) { var category = this.category(); - items.add('category', m('a[href=javascript:;][tabindex=-1].btn.btn-link.control-change-category', { - onclick: this.chooseCategory.bind(this) - }, [ - category ? m('span.category-icon', {style: 'background-color: '+category.color()}) : '', ' ', + items.add('category', m('a[href=javascript:;][tabindex=-1].btn.btn-link.control-change-category', {onclick: this.chooseCategory.bind(this)}, [ + categoryIcon(category), ' ', m('span.label', category ? category.title() : 'Uncategorized'), icon('sort') ])); - - return items; }); + // Add the selected category as data to submit to the server. extend(ComposerDiscussion.prototype, 'data', function(data) { data.links = data.links || {}; data.links.category = this.category(); - return data; - }) + }); + // --------------------------------------------------------------------------- + // ACTIVITY PAGE + // --------------------------------------------------------------------------- + + // Add a category label next to the discussion title in post activity items. extend(ActivityPost.prototype, 'headerItems', function(items) { var category = this.props.activity.post().discussion().category(); if (category) { - items.add('category', m('span.category', {style: {color: category.color()}}, category.title())); + items.add('category', categoryLabel(category)); } - return items; - }) + }); + }); diff --git a/extensions/tags/js/src/components/category-hero.js b/extensions/tags/js/src/components/category-hero.js new file mode 100644 index 000000000..babe90069 --- /dev/null +++ b/extensions/tags/js/src/components/category-hero.js @@ -0,0 +1,16 @@ +import Component from 'flarum/component'; + +export default class CategoryHero extends Component { + view() { + var category = this.props.category; + + return m('header.hero.category-hero', {style: 'background-color: '+category.color()}, [ + m('div.container', [ + m('div.container-narrow', [ + m('h2', category.title()), + m('div.subtitle', category.description()) + ]) + ]) + ]); + } +} diff --git a/extensions/tags/js/src/components/category-nav-item.js b/extensions/tags/js/src/components/category-nav-item.js new file mode 100644 index 000000000..a0806fdad --- /dev/null +++ b/extensions/tags/js/src/components/category-nav-item.js @@ -0,0 +1,22 @@ +import NavItem from 'flarum/components/nav-item'; +import categoryIcon from 'categories/helpers/category-icon'; + +export default class CategoryNavItem extends NavItem { + view() { + var category = this.props.category; + var active = this.constructor.active(this.props); + return m('li'+(active ? '.active' : ''), m('a', {href: this.props.href, config: m.route, onclick: () => {app.cache.discussionList = null; m.redraw.strategy('none')}, style: active ? 'color: '+category.color() : ''}, [ + categoryIcon(category, {className: 'icon'}), + category.title() + ])); + } + + static props(props) { + var category = props.category; + props.params.categories = category.slug(); + props.href = app.route('category', props.params); + props.label = category.title(); + + return props; + } +} diff --git a/extensions/tags/js/src/components/move-discussion-modal.js b/extensions/tags/js/src/components/move-discussion-modal.js new file mode 100644 index 000000000..54d4456ee --- /dev/null +++ b/extensions/tags/js/src/components/move-discussion-modal.js @@ -0,0 +1,56 @@ +import Component from 'flarum/component'; +import DiscussionPage from 'flarum/components/discussion-page'; +import icon from 'flarum/helpers/icon'; + +export default class MoveDiscussionModal extends Component { + constructor(props) { + super(props); + + this.categories = m.prop(app.store.all('categories')); + } + + view() { + var discussion = this.props.discussion; + + return m('div.modal-dialog.modal-move-discussion', [ + m('div.modal-content', [ + m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')), + m('div.modal-header', m('h3.title-control', discussion + ? ['Move ', m('em', discussion.title()), ' from ', m('span.category', {style: 'color: '+discussion.category().color()}, discussion.category().title()), ' to...'] + : ['Start a Discussion In...'])), + m('div', [ + m('ul.category-list', [ + this.categories().map(category => + (discussion && category.id() === discussion.category().id()) ? '' : m('li.category-tile', {style: 'background-color: '+category.color()}, [ + m('a[href=javascript:;]', {onclick: this.save.bind(this, category)}, [ + m('h3.title', category.title()), + m('p.description', category.description()), + m('span.count', category.discussionsCount()+' discussions'), + ]) + ]) + ) + ]) + ]) + ]) + ]); + } + + save(category) { + var discussion = this.props.discussion; + + if (discussion) { + discussion.save({links: {category}}).then(discussion => { + if (app.current instanceof DiscussionPage) { + app.current.stream().sync(); + } + m.redraw(); + }); + } + + this.props.onchange && this.props.onchange(category); + + app.modal.close(); + + m.redraw.strategy('none'); + } +} diff --git a/extensions/tags/js/src/components/notification-discussion-moved.js b/extensions/tags/js/src/components/notification-discussion-moved.js index f3ef00481..9e870f17c 100644 --- a/extensions/tags/js/src/components/notification-discussion-moved.js +++ b/extensions/tags/js/src/components/notification-discussion-moved.js @@ -3,6 +3,7 @@ import avatar from 'flarum/helpers/avatar'; import icon from 'flarum/helpers/icon'; import username from 'flarum/helpers/username'; import humanTime from 'flarum/helpers/human-time'; +import categoryLabel from 'categories/helpers/category-label'; export default class NotificationDiscussionMoved extends Notification { content() { @@ -19,7 +20,7 @@ export default class NotificationDiscussionMoved extends Notification { m('h3.notification-title', discussion.title()), m('div.notification-info', [ icon('arrow-right'), - ' Moved to ', m('span.category', {style: 'color: '+category.color()}, category.title()), ' by ', username(notification.sender()), + ' Moved to ', categoryLabel(category), ' by ', username(notification.sender()), ' ', humanTime(notification.time()) ]) ]); diff --git a/extensions/tags/js/src/components/post-discussion-moved.js b/extensions/tags/js/src/components/post-discussion-moved.js index 1168b42a8..3dd465829 100644 --- a/extensions/tags/js/src/components/post-discussion-moved.js +++ b/extensions/tags/js/src/components/post-discussion-moved.js @@ -1,59 +1,15 @@ -import Component from 'flarum/component'; -import icon from 'flarum/helpers/icon'; -import username from 'flarum/helpers/username'; -import humanTime from 'flarum/utils/human-time'; -import SubtreeRetainer from 'flarum/utils/subtree-retainer'; -import ItemList from 'flarum/utils/item-list'; -import ActionButton from 'flarum/components/action-button'; -import DropdownButton from 'flarum/components/dropdown-button'; - -export default class PostDiscussionMoved extends Component { - constructor(props) { - super(props); - - this.subtree = new SubtreeRetainer( - () => this.props.post.freshness, - () => this.props.post.user().freshness - ); - } - - view(ctrl) { - var controls = this.controlItems().toArray(); +import PostActivity from 'flarum/components/post-activity'; +import categoryLabel from 'categories/helpers/category-label'; +export default class PostDiscussionMoved extends PostActivity { + view() { var post = this.props.post; var oldCategory = app.store.getById('categories', post.content()[0]); var newCategory = app.store.getById('categories', post.content()[1]); - return m('article.post.post-activity.post-discussion-moved', this.subtree.retain() || m('div', [ - controls.length ? DropdownButton.component({ - items: controls, - className: 'contextual-controls', - buttonClass: 'btn btn-default btn-icon btn-sm btn-naked', - menuClass: 'pull-right' - }) : '', - icon('arrow-right post-icon'), - m('div.post-activity-info', [ - m('a.post-user', {href: app.route('user', {username: post.user().username()}), config: m.route}, username(post.user())), - ' moved the discussion from ', m('span.category', {style: {color: oldCategory.color()}}, oldCategory.title()), ' to ', m('span.category', {style: {color: newCategory.color()}}, newCategory.title()), '.' - ]), - m('div.post-activity-time', humanTime(post.time())) - ])); - } - - controlItems() { - var items = new ItemList(); - var post = this.props.post; - - if (post.canDelete()) { - items.add('delete', ActionButton.component({ icon: 'times', label: 'Delete', onclick: this.delete.bind(this) })); - } - - return items; - } - - delete() { - var post = this.props.post; - post.delete(); - this.props.ondelete && this.props.ondelete(post); + return super.view(['moved the discussion from ', categoryLabel(oldCategory), ' to ', categoryLabel(newCategory), '.'], { + className: 'post-discussion-moved', + icon: 'arrow-right' + }); } } diff --git a/extensions/tags/js/src/helpers/category-icon.js b/extensions/tags/js/src/helpers/category-icon.js new file mode 100644 index 000000000..2cefb14fc --- /dev/null +++ b/extensions/tags/js/src/helpers/category-icon.js @@ -0,0 +1,12 @@ +export default function categoryIcon(category, attrs) { + attrs = attrs || {}; + + if (category) { + attrs.style = attrs.style || {}; + attrs.style.backgroundColor = category.color(); + } else { + attrs.className = (attrs.className || '')+' uncategorized'; + } + + return m('span.icon.category-icon', attrs); +} diff --git a/extensions/tags/js/src/helpers/category-label.js b/extensions/tags/js/src/helpers/category-label.js new file mode 100644 index 000000000..8a39188d2 --- /dev/null +++ b/extensions/tags/js/src/helpers/category-label.js @@ -0,0 +1,3 @@ +export default function categoryLabel(category) { + return m('span.category-label', {style: {color: category.color()}}, category.title()); +} diff --git a/extensions/tags/js/src/category.js b/extensions/tags/js/src/models/category.js similarity index 100% rename from extensions/tags/js/src/category.js rename to extensions/tags/js/src/models/category.js diff --git a/extensions/tags/less/categories.less b/extensions/tags/less/categories.less index 5b1b9aeee..70331163b 100644 --- a/extensions/tags/less/categories.less +++ b/extensions/tags/less/categories.less @@ -1,4 +1,4 @@ -.category { +.category-label { text-transform: uppercase; font-size: 80%; font-weight: bold;