diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index d4f0f7d821b..83d9b5ce6fd 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 9f7d3f4ed45..bd517eabf81 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 00000000000..2e06ac523a3 --- /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 446e1aa53a2..53b3a8c2abd 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(`#${categorySlug}`); -} - 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 28c4fcbe7cc..d59cdaa488d 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 3f5239d1fe8..ff861bb2886 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/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index fe97b02525f..8f2d807fbdc 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -5,7 +5,7 @@ module CategoryHashtag class_methods do def query_from_hashtag_slug(category_slug) - parent_slug, child_slug = category_slug.split(":", 2) + parent_slug, child_slug = category_slug.split(SEPARATOR, 2) category = Category.where(slug: parent_slug, parent_category_id: nil) diff --git a/spec/components/concern/category_hashtag_spec.rb b/spec/components/concern/category_hashtag_spec.rb index 3fe10ba84cb..be60931f795 100644 --- a/spec/components/concern/category_hashtag_spec.rb +++ b/spec/components/concern/category_hashtag_spec.rb @@ -11,7 +11,7 @@ describe CategoryHashtag do end it "should return the right result for a parent and child category slug" do - expect(Category.query_from_hashtag_slug("#{parent_category.slug}:#{child_category.slug}")) + expect(Category.query_from_hashtag_slug("#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{child_category.slug}")) .to eq(child_category) end @@ -20,7 +20,7 @@ describe CategoryHashtag do end it "should return nil for incorrect parent and child category slug" do - expect(Category.query_from_hashtag_slug("random-slug:random-slug")).to eq(nil) + expect(Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug")).to eq(nil) end end end 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 00000000000..dea21dc13c2 --- /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'); +});