From 4a7f560a351558df0cd4031f7f247818cbc5b831 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Mon, 18 Jan 2016 17:08:24 +0800 Subject: [PATCH] FEATURE: Decorate category hashtag links. --- .../discourse/components/d-editor.js.es6 | 3 +- .../discourse/helpers/category-link.js.es6 | 2 +- .../discourse/lib/category-hashtags.js.es6 | 26 ++++ .../lib/link-category-hashtags.js.es6 | 6 +- .../javascripts/discourse/views/post.js.es6 | 10 +- .../common/components/badges.css.scss | 115 +++++++++++++----- .../lib/category-hashtags-test.js.es6 | 22 ++++ 7 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/category-hashtags.js.es6 create mode 100644 test/javascripts/lib/category-hashtags-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index d4f0f7d82..83d9b5ce6 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -3,6 +3,7 @@ import loadScript from 'discourse/lib/load-script'; import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; import Category from 'discourse/models/category'; +import { SEPARATOR as categoryHashtagSeparator } from 'discourse/lib/category-hashtags'; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -255,7 +256,7 @@ export default Ember.Component.extend({ template: template, key: '#', transformComplete(category) { - return Category.slugFor(category, ":"); + return Category.slugFor(category, categoryHashtagSeparator); }, dataSource(term) { return Category.search(term); diff --git a/app/assets/javascripts/discourse/helpers/category-link.js.es6 b/app/assets/javascripts/discourse/helpers/category-link.js.es6 index 9f7d3f4ed..bd517eabf 100644 --- a/app/assets/javascripts/discourse/helpers/category-link.js.es6 +++ b/app/assets/javascripts/discourse/helpers/category-link.js.es6 @@ -21,7 +21,7 @@ export function categoryBadgeHTML(category, opts) { var description = get(category, 'description_text'), restricted = get(category, 'read_restricted'), - url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category), + url = opts.url ? opts.url : Discourse.getURL("/c/") + Discourse.Category.slugFor(category), href = (opts.link === false ? '' : url), tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'), extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''), diff --git a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 new file mode 100644 index 000000000..2e06ac523 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 @@ -0,0 +1,26 @@ +import Category from 'discourse/models/category'; +import { categoryBadgeHTML } from 'discourse/helpers/category-link'; + +export const SEPARATOR = ":"; + +export function findCategoryByHashtagSlug(hashtagSlug) { + if (hashtagSlug.indexOf('#') === 0) hashtagSlug = hashtagSlug.slice(1); + return Category.findBySlug.apply(null, hashtagSlug.split(SEPARATOR, 2).reverse()); +}; + +export function replaceSpan($elem, categorySlug, categoryLink) { + const category = findCategoryByHashtagSlug(categorySlug); + + if (!category) { + $elem.replaceWith(categorySlug); + } else { + $elem.replaceWith(categoryBadgeHTML( + category, { url: categoryLink, allowUncategorized: true } + )); + } +}; + +export function decorateLinks($elems) { + $elems.each((_, elem) => replaceSpan($(elem), elem.text, elem.href)); +} + diff --git a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 index 446e1aa53..53b3a8c2a 100644 --- a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 @@ -1,12 +1,10 @@ +import { replaceSpan } from 'discourse/lib/category-hashtags'; + const validCategoryHashtags = {}; const checkedCategoryHashtags = []; const testedKey = 'tested'; const testedClass = `hashtag-${testedKey}`; -function replaceSpan($elem, categorySlug, categoryLink) { - $elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#${categorySlug}</a>`); -} - function updateFound($hashtags, categorySlugs) { Ember.run.schedule('afterRender', () => { $hashtags.each((index, hashtag) => { diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index 28c4fcbe7..d59cdaa48 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -3,6 +3,7 @@ import { number } from 'discourse/lib/formatter'; import DiscourseURL from 'discourse/lib/url'; import { default as computed, on } from 'ember-addons/ember-computed-decorators'; import { fmt } from 'discourse/lib/computed'; +import { decorateLinks as decorateCategoryHashtagLinks } from 'discourse/lib/category-hashtags'; const DAY = 60 * 50 * 1000; @@ -75,6 +76,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { _cookedWasChanged() { this.trigger('postViewUpdated', this.$()); this._insertQuoteControls(); + this._decorateCategoryHashtagLinks(); }, mouseUp(e) { @@ -318,6 +320,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { const $post = this.$(), postNumber = this.get('post').get('post_number'); + this._decorateCategoryHashtagLinks(); this._showLinkCounts(); ScreenTrack.current().track($post.prop('id'), postNumber); @@ -375,7 +378,12 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { cooked.unhighlight(); this._highlighted = false; } - }.observes('searchService.highlightTerm', 'cooked') + }.observes('searchService.highlightTerm', 'cooked'), + + _decorateCategoryHashtagLinks() { + const $elems = this.$('.cooked a.hashtag'); + if ($elems.length > 0) decorateCategoryHashtagLinks($elems); + } }); export default PostView; diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 3f5239d1f..ff861bb28 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -30,7 +30,7 @@ color: $primary !important; padding: 3px; vertical-align: text-top; - margin-top: -3px; //vertical alignment fix + margin-top: -2px; //vertical alignment fix display: inline-block; overflow: hidden; text-overflow: ellipsis; @@ -38,21 +38,18 @@ .extra-info-wrapper & { color: $header-primary !important; } - } + } - .badge-category-parent-bg, .badge-category-bg { - display: inline-block; - padding: 1px; - - &:before { - content: "\a0"; - } - - } + .badge-category-parent-bg, .badge-category-bg { + display: inline-block; + padding: 1px; + &:before { + content: "\a0"; + } + } } - &.bullet { //bullet category style display: inline-flex; align-items: baseline; @@ -71,31 +68,30 @@ .extra-info-wrapper & { color: $header-primary !important; } - } + } .badge-category-parent-bg, .badge-category-bg { - width: 10px; - height: 10px; - margin-right: 5px; - display: inline-block; - line-height: 1; + width: 10px; + height: 10px; + margin-right: 5px; + display: inline-block; + line-height: 1; - &:before { - content: "\a0"; - } - } + &:before { + content: "\a0"; + } + } - span { - &.badge-category-parent-bg { //subcategory style - width: 5px; - margin-right: 0; - & + .badge-category-bg { - width: 5px; - } - } - } - - } + span { + &.badge-category-parent-bg { //subcategory style + width: 5px; + margin-right: 0; + & + .badge-category-bg { + width: 5px; + } + } + } + } &.box { //box category style (apply custom widths to the wrapper, not the children) @@ -134,6 +130,59 @@ } } +@mixin cooked-badge-bullet($length, $offset:0px) { + .badge-wrapper.bullet { + span { + position: relative; + + &.badge-category-bg { + width: $length; + height: $length; + top: $offset; + } + + &.badge-category-parent-bg { + width: $length / 2; + height: $length; + top: $offset; + + & + .badge-category-bg { + width: $length / 2; + } + } + } + } +} + +.cooked, .d-editor-preview { + p .badge-wrapper.bullet { + margin: 0px 2.5px; + } + + h1 { @include cooked-badge-bullet(22px) } + h2 { @include cooked-badge-bullet(17px) } + h3 { @include cooked-badge-bullet(14px) } + h4 { @include cooked-badge-bullet(12px) } + h5 { @include cooked-badge-bullet(10px, -1.1px) } + h6 { @include cooked-badge-bullet(9px, -1.5px) } + + .badge-wrapper.box { + span { + display: inline; + } + + .badge-notification.clicks { + display: inline-block; + overflow: visible; + top: 0px; + } + + .badge-category-bg { + padding-right: 5px; + } + } +} + // Category badge dropdown // -------------------------------------------------- diff --git a/test/javascripts/lib/category-hashtags-test.js.es6 b/test/javascripts/lib/category-hashtags-test.js.es6 new file mode 100644 index 000000000..dea21dc13 --- /dev/null +++ b/test/javascripts/lib/category-hashtags-test.js.es6 @@ -0,0 +1,22 @@ +import createStore from 'helpers/create-store'; +import Category from 'discourse/models/category'; +import { findCategoryByHashtagSlug } from "discourse/lib/category-hashtags"; + +module("lib:category-hashtags"); + +test('findCategoryByHashtagSlug', () => { + const store = createStore(); + + const parentCategory = store.createRecord('category', { slug: 'test1' }); + + const childCategory = store.createRecord('category', { + slug: 'test2', parentCategory: parentCategory + }); + + sandbox.stub(Category, 'list').returns([parentCategory, childCategory]); + + equal(findCategoryByHashtagSlug('test1'), parentCategory, 'returns the right category'); + equal(findCategoryByHashtagSlug('test1:test2'), childCategory, 'returns the right category'); + equal(findCategoryByHashtagSlug('#test1'), parentCategory, 'returns the right category'); + equal(findCategoryByHashtagSlug('#test1:test2'), childCategory, 'returns the right category'); +});