mirror of
https://github.com/flarum/framework.git
synced 2025-04-24 21:54:04 +08:00
Update for new extension API; implement i10n
This commit is contained in:
parent
7d38f0880e
commit
180b87c71e
32
extensions/tags/.editorconfig
Normal file
32
extensions/tags/.editorconfig
Normal file
@ -0,0 +1,32 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.html]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
5
extensions/tags/.eslintignore
Normal file
5
extensions/tags/.eslintignore
Normal file
@ -0,0 +1,5 @@
|
||||
**/bower_components/**/*
|
||||
**/node_modules/**/*
|
||||
vendor/**/*
|
||||
**/Gulpfile.js
|
||||
**/dist/**/*
|
171
extensions/tags/.eslintrc
Normal file
171
extensions/tags/.eslintrc
Normal file
@ -0,0 +1,171 @@
|
||||
{
|
||||
"parser": "babel-eslint", // https://github.com/babel/babel-eslint
|
||||
"env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
|
||||
"browser": true // browser global variables
|
||||
},
|
||||
"ecmaFeatures": {
|
||||
"arrowFunctions": true,
|
||||
"blockBindings": true,
|
||||
"classes": true,
|
||||
"defaultParams": true,
|
||||
"destructuring": true,
|
||||
"forOf": true,
|
||||
"generators": false,
|
||||
"modules": true,
|
||||
"objectLiteralComputedProperties": true,
|
||||
"objectLiteralDuplicateProperties": false,
|
||||
"objectLiteralShorthandMethods": true,
|
||||
"objectLiteralShorthandProperties": true,
|
||||
"spread": true,
|
||||
"superInFunctions": true,
|
||||
"templateStrings": true,
|
||||
"jsx": true
|
||||
},
|
||||
"globals": {
|
||||
"m": true,
|
||||
"app": true,
|
||||
"$": true,
|
||||
"moment": true
|
||||
},
|
||||
"rules": {
|
||||
/**
|
||||
* Strict mode
|
||||
*/
|
||||
// babel inserts "use strict"; for us
|
||||
"strict": [2, "never"], // http://eslint.org/docs/rules/strict
|
||||
|
||||
/**
|
||||
* ES6
|
||||
*/
|
||||
"no-var": 2, // http://eslint.org/docs/rules/no-var
|
||||
"prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*/
|
||||
"no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
|
||||
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
|
||||
"no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
|
||||
"vars": "local",
|
||||
"args": "after-used"
|
||||
}],
|
||||
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
|
||||
|
||||
/**
|
||||
* Possible errors
|
||||
*/
|
||||
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
|
||||
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
|
||||
"no-console": 1, // http://eslint.org/docs/rules/no-console
|
||||
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
|
||||
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
|
||||
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
|
||||
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
|
||||
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
|
||||
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
|
||||
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
|
||||
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
|
||||
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
|
||||
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
|
||||
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
|
||||
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
|
||||
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
|
||||
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
|
||||
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
|
||||
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
|
||||
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
|
||||
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
|
||||
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
|
||||
|
||||
/**
|
||||
* Best practices
|
||||
*/
|
||||
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
|
||||
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
|
||||
"default-case": 2, // http://eslint.org/docs/rules/default-case
|
||||
"dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
|
||||
"allowKeywords": true
|
||||
}],
|
||||
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
|
||||
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
|
||||
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
|
||||
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
|
||||
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
|
||||
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
|
||||
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
|
||||
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
|
||||
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
|
||||
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
|
||||
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
|
||||
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
|
||||
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
|
||||
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
|
||||
"no-new": 2, // http://eslint.org/docs/rules/no-new
|
||||
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
|
||||
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
|
||||
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
|
||||
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
|
||||
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
|
||||
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
|
||||
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
|
||||
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
|
||||
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
|
||||
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
|
||||
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
|
||||
"no-with": 2, // http://eslint.org/docs/rules/no-with
|
||||
"radix": 2, // http://eslint.org/docs/rules/radix
|
||||
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
|
||||
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
|
||||
"yoda": 2, // http://eslint.org/docs/rules/yoda
|
||||
|
||||
/**
|
||||
* Style
|
||||
*/
|
||||
"indent": [2, 2], // http://eslint.org/docs/rules/indent
|
||||
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
|
||||
"1tbs", {
|
||||
"allowSingleLine": true
|
||||
}],
|
||||
"quotes": [
|
||||
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
|
||||
],
|
||||
"camelcase": [2, { // http://eslint.org/docs/rules/camelcase
|
||||
"properties": "never"
|
||||
}],
|
||||
"comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
|
||||
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
|
||||
"func-names": 1, // http://eslint.org/docs/rules/func-names
|
||||
"key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
}],
|
||||
"new-cap": [2, { // http://eslint.org/docs/rules/new-cap
|
||||
"newIsCap": true
|
||||
}],
|
||||
"no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
|
||||
"max": 2
|
||||
}],
|
||||
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
|
||||
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
|
||||
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
|
||||
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
|
||||
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
|
||||
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
|
||||
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
|
||||
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
|
||||
"semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
|
||||
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
|
||||
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
|
||||
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
|
||||
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
|
||||
"spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment
|
||||
}
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
<?php
|
||||
|
||||
// Require the extension's composer autoload file. This will enable all of our
|
||||
// classes in the src directory to be autoloaded.
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Register our service provider with the Flarum application. In here we can
|
||||
// register bindings and execute code when the application boots.
|
||||
return $this->app->register('Flarum\Tags\TagsServiceProvider');
|
||||
return 'Flarum\Tags\Extension';
|
||||
|
@ -1,16 +1,21 @@
|
||||
{
|
||||
"name": "flarum-tags",
|
||||
"name": "tags",
|
||||
"title": "Tags",
|
||||
"description": "Organise discussions into a heirarchy of tags and categories.",
|
||||
"tags": [],
|
||||
"keywords": ["discussions"],
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Toby Zerner",
|
||||
"email": "toby.zerner@gmail.com"
|
||||
"email": "toby@flarum.org",
|
||||
"homepage": "http://tobyzerner.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"flarum": ">0.1.0"
|
||||
},
|
||||
"support": {
|
||||
"source": "https://github.com/flarum/tags",
|
||||
"issues": "https://github.com/flarum/tags/issues"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
extensions/tags/js/bootstrap.js
vendored
43
extensions/tags/js/bootstrap.js
vendored
@ -1,43 +0,0 @@
|
||||
import app from 'flarum/app';
|
||||
import Model from 'flarum/model';
|
||||
import Discussion from 'flarum/models/discussion';
|
||||
import IndexPage from 'flarum/components/index-page';
|
||||
|
||||
import Tag from 'flarum-tags/models/tag';
|
||||
import TagsPage from 'flarum-tags/components/tags-page';
|
||||
import DiscussionTaggedPost from 'flarum-tags/components/discussion-tagged-post';
|
||||
import addTagList from 'flarum-tags/add-tag-list';
|
||||
import addTagFilter from 'flarum-tags/add-tag-filter';
|
||||
import addTagLabels from 'flarum-tags/add-tag-labels';
|
||||
import addTagDiscussionControl from 'flarum-tags/add-tag-discussion-control';
|
||||
import addTagComposer from 'flarum-tags/add-tag-composer';
|
||||
|
||||
app.initializers.add('flarum-tags', function() {
|
||||
// Register routes.
|
||||
app.routes['tags'] = ['/tags', TagsPage.component()];
|
||||
app.routes['tag'] = ['/t/:tags', IndexPage.component()];
|
||||
|
||||
app.route.tag = function(tag) {
|
||||
return app.route('tag', { tags: tag.slug() });
|
||||
};
|
||||
|
||||
// Register models.
|
||||
app.store.models['tags'] = Tag;
|
||||
Discussion.prototype.tags = Model.many('tags');
|
||||
Discussion.prototype.canTag = Model.prop('canTag');
|
||||
|
||||
app.postComponentRegistry['discussionTagged'] = DiscussionTaggedPost;
|
||||
|
||||
// Add a list of tags to the index navigation.
|
||||
addTagList();
|
||||
|
||||
// When a tag is selected, filter the discussion list by that tag.
|
||||
addTagFilter();
|
||||
|
||||
// Add tags to the discussion list and discussion hero.
|
||||
addTagLabels();
|
||||
|
||||
addTagDiscussionControl();
|
||||
|
||||
addTagComposer();
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
gulp({
|
||||
modulePrefix: 'flarum-tags'
|
||||
modulePrefix: 'tags'
|
||||
});
|
53
extensions/tags/js/forum/src/addTagComposer.js
Normal file
53
extensions/tags/js/forum/src/addTagComposer.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { extend, override } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import DiscussionComposer from 'flarum/components/DiscussionComposer';
|
||||
|
||||
import TagDiscussionModal from 'tags/components/TagDiscussionModal';
|
||||
import tagsLabel from 'tags/helpers/tagsLabel';
|
||||
|
||||
export default function() {
|
||||
override(IndexPage.prototype, 'composeNewDiscussion', function(original, deferred) {
|
||||
const tag = app.store.getBy('tags', 'slug', this.params().tags);
|
||||
|
||||
app.modal.show(
|
||||
new TagDiscussionModal({
|
||||
selectedTags: tag ? [tag] : [],
|
||||
onsubmit: tags => {
|
||||
original(deferred).then(component => component.tags = tags);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
// Add tag-selection abilities to the discussion composer.
|
||||
DiscussionComposer.prototype.tags = [];
|
||||
DiscussionComposer.prototype.chooseTags = function() {
|
||||
app.modal.show(
|
||||
new TagDiscussionModal({
|
||||
selectedTags: this.tags.slice(0),
|
||||
onsubmit: tags => {
|
||||
this.tags = tags;
|
||||
this.$('textarea').focus();
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Add a tag-selection menu to the discussion composer's header, after the
|
||||
// title.
|
||||
extend(DiscussionComposer.prototype, 'headerItems', function(items) {
|
||||
items.add('tags', (
|
||||
<a className="DiscussionComposer-changeTags" onclick={this.chooseTags.bind(this)}>
|
||||
{tagsLabel(this.tags)}
|
||||
</a>
|
||||
), 10);
|
||||
});
|
||||
|
||||
// Add the selected tags as data to submit to the server.
|
||||
extend(DiscussionComposer.prototype, 'data', function(data) {
|
||||
data.relationships = data.relationships || {};
|
||||
data.relationships.tags = this.tags;
|
||||
});
|
||||
}
|
18
extensions/tags/js/forum/src/addTagControl.js
Normal file
18
extensions/tags/js/forum/src/addTagControl.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { extend } from 'flarum/extend';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
import TagDiscussionModal from 'tags/components/TagDiscussionModal';
|
||||
|
||||
export default function() {
|
||||
// Add a control allowing the discussion to be moved to another category.
|
||||
extend(DiscussionControls, 'moderationControls', function(items, discussion) {
|
||||
if (discussion.canTag()) {
|
||||
items.add('tags', Button.component({
|
||||
children: app.trans('tags.edit_discussion_tags_link'),
|
||||
icon: 'tag',
|
||||
onclick: () => app.modal.show(new TagDiscussionModal({discussion}))
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
53
extensions/tags/js/forum/src/addTagFilter.js
Normal file
53
extensions/tags/js/forum/src/addTagFilter.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { extend, override } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import DiscussionList from 'flarum/components/DiscussionList';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
import TagHero from 'tags/components/TagHero';
|
||||
|
||||
export default function() {
|
||||
IndexPage.prototype.currentTag = function() {
|
||||
const slug = this.params().tags;
|
||||
|
||||
if (slug) return app.store.getBy('tags', 'slug', slug);
|
||||
};
|
||||
|
||||
// If currently viewing a tag, insert a tag hero at the top of the view.
|
||||
override(IndexPage.prototype, 'hero', function(original) {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) return TagHero.component({tag});
|
||||
|
||||
return original();
|
||||
});
|
||||
|
||||
// If currently viewing a tag, restyle the 'new discussion' button to use
|
||||
// the tag's color.
|
||||
extend(IndexPage.prototype, 'sidebarItems', function(items) {
|
||||
const tag = this.currentTag();
|
||||
|
||||
if (tag) {
|
||||
const color = tag.color();
|
||||
|
||||
if (color) {
|
||||
items.newDiscussion.content.props.style = {backgroundColor: color};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add a parameter for the IndexPage to pass on to the DiscussionList that
|
||||
// will let us filter discussions by tag.
|
||||
extend(IndexPage.prototype, 'params', function(params) {
|
||||
params.tags = m.route.param('tags');
|
||||
});
|
||||
|
||||
// Translate that parameter into a gambit appended to the search query.
|
||||
extend(DiscussionList.prototype, 'requestParams', function(params) {
|
||||
params.include.push('tags');
|
||||
|
||||
if (params.tags) {
|
||||
params.filter = params.filter || {};
|
||||
params.filter.q = (params.filter.q || '') + ' tag:' + extract(params, 'tags');
|
||||
}
|
||||
});
|
||||
}
|
46
extensions/tags/js/forum/src/addTagLabels.js
Normal file
46
extensions/tags/js/forum/src/addTagLabels.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { extend } from 'flarum/extend';
|
||||
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import DiscussionHero from 'flarum/components/DiscussionHero';
|
||||
|
||||
import tagsLabel from 'tags/helpers/tagsLabel';
|
||||
import sortTags from 'tags/utils/sortTags';
|
||||
|
||||
export default function() {
|
||||
// Add tag labels to each discussion in the discussion list.
|
||||
extend(DiscussionListItem.prototype, 'infoItems', function(items) {
|
||||
const tags = this.props.discussion.tags();
|
||||
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags), 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Include a discussion's tags when fetching it.
|
||||
extend(DiscussionPage.prototype, 'params', function(params) {
|
||||
params.include.push('tags');
|
||||
});
|
||||
|
||||
// Restyle a discussion's hero to use its first tag's color.
|
||||
extend(DiscussionHero.prototype, 'view', function(view) {
|
||||
const tags = sortTags(this.props.discussion.tags());
|
||||
|
||||
if (tags && tags.length) {
|
||||
const color = tags[0].color();
|
||||
if (color) {
|
||||
view.attrs.style = {backgroundColor: color};
|
||||
view.attrs.className += ' DiscussionHero--colored';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add a list of a discussion's tags to the discussion hero, displayed
|
||||
// before the title. Put the title on its own line.
|
||||
extend(DiscussionHero.prototype, 'items', function(items) {
|
||||
const tags = this.props.discussion.tags();
|
||||
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags, {link: true}), 5);
|
||||
}
|
||||
});
|
||||
}
|
55
extensions/tags/js/forum/src/addTagList.js
Normal file
55
extensions/tags/js/forum/src/addTagList.js
Normal file
@ -0,0 +1,55 @@
|
||||
import { extend } from 'flarum/extend';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
|
||||
import TagLinkButton from 'tags/components/TagLinkButton';
|
||||
import TagsPage from 'tags/components/TagsPage';
|
||||
import sortTags from 'tags/utils/sortTags';
|
||||
|
||||
export default function() {
|
||||
// Add a link to the tags page, as well as a list of all the tags,
|
||||
// to the index page's sidebar.
|
||||
extend(IndexPage.prototype, 'navItems', function(items) {
|
||||
items.add('tags', LinkButton.component({
|
||||
icon: 'th-large',
|
||||
children: 'Tags',
|
||||
href: app.route('tags')
|
||||
}), -10);
|
||||
|
||||
if (app.current instanceof TagsPage) return;
|
||||
|
||||
items.add('separator', Separator.component(), -10);
|
||||
|
||||
const params = this.stickyParams();
|
||||
const tags = app.store.all('tags');
|
||||
const currentTag = this.currentTag();
|
||||
|
||||
const addTag = tag => {
|
||||
let active = currentTag === tag;
|
||||
|
||||
if (!active && currentTag) {
|
||||
active = currentTag.parent() === tag;
|
||||
}
|
||||
|
||||
items.add('tag' + tag.id(), TagLinkButton.component({tag, params, active}), -10);
|
||||
};
|
||||
|
||||
sortTags(tags)
|
||||
.filter(tag => tag.position() !== null && (!tag.isChild() || (currentTag && (tag.parent() === currentTag || tag.parent() === currentTag.parent()))))
|
||||
.forEach(addTag);
|
||||
|
||||
const more = tags
|
||||
.filter(tag => tag.position() === null)
|
||||
.sort((a, b) => b.discussionsCount() - a.discussionsCount());
|
||||
|
||||
more.splice(0, 3).forEach(addTag);
|
||||
|
||||
if (more.length) {
|
||||
items.add('moreTags', LinkButton.component({
|
||||
children: app.trans('tags.more'),
|
||||
href: app.route('tags')
|
||||
}), -10);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import EventPost from 'flarum/components/EventPost';
|
||||
import punctuate from 'flarum/helpers/punctuate';
|
||||
import tagsLabel from 'tags/helpers/tagsLabel';
|
||||
|
||||
export default class DiscussionTaggedPost extends EventPost {
|
||||
icon() {
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
descriptionKey() {
|
||||
return 'tags.discussion_tagged_post';
|
||||
}
|
||||
|
||||
descriptionData() {
|
||||
const post = this.props.post;
|
||||
const oldTags = post.content()[0];
|
||||
const newTags = post.content()[1];
|
||||
|
||||
function diffTags(tags1, tags2) {
|
||||
return tags1
|
||||
.filter(tag => tags2.indexOf(tag) === -1)
|
||||
.map(id => app.store.getById('tags', id));
|
||||
}
|
||||
|
||||
const added = diffTags(newTags, oldTags);
|
||||
const removed = diffTags(oldTags, newTags);
|
||||
const actions = [];
|
||||
|
||||
if (added.length) {
|
||||
actions.push(app.trans('tags.added_tags', {
|
||||
tags: tagsLabel(added, {link: true}),
|
||||
count: added
|
||||
}));
|
||||
}
|
||||
|
||||
if (removed.length) {
|
||||
actions.push(app.trans('tags.removed_tags', {
|
||||
tags: tagsLabel(removed, {link: true}),
|
||||
count: removed
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
action: punctuate(actions),
|
||||
count: added.length + removed.length
|
||||
};
|
||||
}
|
||||
}
|
284
extensions/tags/js/forum/src/components/TagDiscussionModal.js
Normal file
284
extensions/tags/js/forum/src/components/TagDiscussionModal.js
Normal file
@ -0,0 +1,284 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import Button from 'flarum/components/Button';
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
import tagLabel from 'tags/helpers/tagLabel';
|
||||
import tagIcon from 'tags/helpers/tagIcon';
|
||||
import sortTags from 'tags/utils/sortTags';
|
||||
|
||||
export default class TagDiscussionModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.tags = sortTags(app.store.all('tags').filter(tag => tag.canStartDiscussion()));
|
||||
|
||||
this.selected = [];
|
||||
this.filter = m.prop('');
|
||||
this.index = this.tags[0].id();
|
||||
this.focused = false;
|
||||
|
||||
if (this.props.selectedTags) {
|
||||
this.props.selectedTags.map(this.addTag.bind(this));
|
||||
} else if (this.props.discussion) {
|
||||
this.props.discussion.tags().map(this.addTag.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given tag to the list of selected tags.
|
||||
*
|
||||
* @param {Tag} tag
|
||||
*/
|
||||
addTag(tag) {
|
||||
if (!tag.canStartDiscussion()) return;
|
||||
|
||||
// If this tag has a parent, we'll also need to add the parent tag to the
|
||||
// selected list if it's not already in there.
|
||||
const parent = tag.parent();
|
||||
if (parent) {
|
||||
const index = this.selected.indexOf(parent);
|
||||
if (index === -1) {
|
||||
this.selected.push(parent);
|
||||
}
|
||||
}
|
||||
|
||||
this.selected.push(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given tag from the list of selected tags.
|
||||
*
|
||||
* @param {Tag} tag
|
||||
*/
|
||||
removeTag(tag) {
|
||||
const index = this.selected.indexOf(tag);
|
||||
if (index !== -1) {
|
||||
this.selected.splice(index, 1);
|
||||
|
||||
// Look through the list of selected tags for any tags which have the tag
|
||||
// we just removed as their parent. We'll need to remove them too.
|
||||
this.selected
|
||||
.filter(selected => selected.parent() && selected.parent() === tag)
|
||||
.forEach(this.removeTag);
|
||||
}
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'TagDiscussionModal';
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.props.discussion
|
||||
? app.trans('tags.edit_discussion_tags_title', {title: <em>{this.props.discussion.title()}</em>})
|
||||
: app.trans('tags.tag_new_discussion_title');
|
||||
}
|
||||
|
||||
content() {
|
||||
let tags = this.tags;
|
||||
const filter = this.filter().toLowerCase();
|
||||
|
||||
if (filter) {
|
||||
tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter);
|
||||
}
|
||||
|
||||
if (tags.indexOf(this.index) === -1) {
|
||||
this.index = tags[0];
|
||||
}
|
||||
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
<div className="TagDiscussionModal-form">
|
||||
<div className="TagDiscussionModal-form-input">
|
||||
<div className={'TagsInput FormControl ' + (this.focused ? 'focus' : '')}>
|
||||
<span className="TagsInput-selected">
|
||||
{this.selected.map(tag =>
|
||||
<span className="TagsInput-tag" onclick={() => {
|
||||
this.removeTag(tag);
|
||||
this.onready();
|
||||
}}>
|
||||
{tagLabel(tag)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<input className="FormControl"
|
||||
placeholder={!this.selected.length ? app.trans('tags.discussion_tags_placeholder') : ''}
|
||||
value={this.filter()}
|
||||
oninput={m.withAttr('value', this.filter)}
|
||||
onkeydown={this.onkeydown.bind(this)}
|
||||
onfocus={() => this.focused = true}
|
||||
onblur={() => this.focused = false}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="TagDiscussionModal-form-submit App-primaryControl">
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
disabled: !this.selected.length,
|
||||
icon: 'check',
|
||||
children: app.trans('tags.confirm')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
<div className="Modal-footer">
|
||||
<ul className="TagDiscussionModal-list SelectTagList">
|
||||
{tags.map(tag => {
|
||||
if (!filter && tag.parent() && this.selected.indexOf(tag.parent()) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<li data-index={tag.id()}
|
||||
className={classList({
|
||||
pinned: tag.position() !== null,
|
||||
child: !!tag.parent(),
|
||||
colored: !!tag.color(),
|
||||
selected: this.selected.indexOf(tag) !== -1,
|
||||
active: this.index === tag
|
||||
})}
|
||||
style={{color: tag.color()}}
|
||||
onmouseover={() => this.index = tag}
|
||||
onclick={this.toggleTag.bind(this, tag)}
|
||||
>
|
||||
{tagIcon(tag)}
|
||||
<span className="SelectTagListItem-name">
|
||||
{highlight(tag.name(), filter)}
|
||||
</span>
|
||||
{tag.description()
|
||||
? (
|
||||
<span className="SelectTagListItem-description">
|
||||
{tag.description()}
|
||||
</span>
|
||||
) : ''}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
toggleTag(tag) {
|
||||
const index = this.selected.indexOf(tag);
|
||||
|
||||
if (index !== -1) {
|
||||
this.removeTag(tag);
|
||||
} else {
|
||||
this.addTag(tag);
|
||||
}
|
||||
|
||||
if (this.filter()) {
|
||||
this.filter('');
|
||||
this.index = this.tags[0];
|
||||
}
|
||||
|
||||
this.onready();
|
||||
}
|
||||
|
||||
onkeydown(e) {
|
||||
switch (e.which) {
|
||||
case 40:
|
||||
case 38: // Down/Up
|
||||
e.preventDefault();
|
||||
this.setIndex(this.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true);
|
||||
break;
|
||||
|
||||
case 13: // Return
|
||||
e.preventDefault();
|
||||
if (e.metaKey || e.ctrlKey || this.selected.indexOf(this.index) !== -1) {
|
||||
if (this.selected.length) {
|
||||
this.$('form').submit();
|
||||
}
|
||||
} else {
|
||||
this.getItem(this.index)[0].dispatchEvent(new Event('click'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 8: // Backspace
|
||||
if (e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
|
||||
e.preventDefault();
|
||||
this.selected.splice(this.selected.length - 1, 1);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
||||
selectableItems() {
|
||||
return this.$('.TagDiscussionModal-list > li');
|
||||
}
|
||||
|
||||
getCurrentNumericIndex() {
|
||||
return this.selectableItems().index(
|
||||
this.getItem(this.index)
|
||||
);
|
||||
}
|
||||
|
||||
getItem(index) {
|
||||
return this.selectableItems().filter(`[data-index="${index.id()}"]`);
|
||||
}
|
||||
|
||||
setIndex(index, scrollToItem) {
|
||||
const $items = this.selectableItems();
|
||||
const $dropdown = $items.parent();
|
||||
|
||||
if (index < 0) {
|
||||
index = $items.length - 1;
|
||||
} else if (index >= $items.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
const $item = $items.eq(index);
|
||||
|
||||
this.index = app.store.getById('tags', $item.attr('data-index'));
|
||||
|
||||
m.redraw();
|
||||
|
||||
if (scrollToItem) {
|
||||
const dropdownScroll = $dropdown.scrollTop();
|
||||
const dropdownTop = $dropdown.offset().top;
|
||||
const dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
||||
const itemTop = $item.offset().top;
|
||||
const itemBottom = itemTop + $item.outerHeight();
|
||||
|
||||
let scrollTop;
|
||||
if (itemTop < dropdownTop) {
|
||||
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
|
||||
} else if (itemBottom > dropdownBottom) {
|
||||
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
|
||||
}
|
||||
|
||||
if (typeof scrollTop !== 'undefined') {
|
||||
$dropdown.stop(true).animate({scrollTop}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const discussion = this.props.discussion;
|
||||
const tags = this.selected;
|
||||
|
||||
if (discussion) {
|
||||
discussion.save({relationships: {tags}})
|
||||
.then(() => {
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.onsubmit) this.props.onsubmit(tags);
|
||||
|
||||
app.modal.close();
|
||||
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
}
|
20
extensions/tags/js/forum/src/components/TagHero.js
Normal file
20
extensions/tags/js/forum/src/components/TagHero.js
Normal file
@ -0,0 +1,20 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
export default class TagHero extends Component {
|
||||
view() {
|
||||
const tag = this.props.tag;
|
||||
const color = tag.color();
|
||||
|
||||
return (
|
||||
<header className="Hero TagHero"
|
||||
style={color ? {color: '#fff', backgroundColor: color} : ''}>
|
||||
<div className="container">
|
||||
<div className="containerNarrow">
|
||||
<h2 className="Hero-title">{tag.name()}</h2>
|
||||
<div className="Hero-subtitle">{tag.description()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
27
extensions/tags/js/forum/src/components/TagLinkButton.js
Normal file
27
extensions/tags/js/forum/src/components/TagLinkButton.js
Normal file
@ -0,0 +1,27 @@
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import tagIcon from 'tags/helpers/tagIcon';
|
||||
|
||||
export default class TagLinkButton extends LinkButton {
|
||||
view() {
|
||||
const tag = this.props.tag;
|
||||
const active = this.constructor.isActive(this.props);
|
||||
const description = tag && tag.description();
|
||||
|
||||
return (
|
||||
<a className={'TagLinkButton hasIcon ' + (tag.isChild() ? 'child' : '')} href={this.props.href} config={m.route}
|
||||
style={active && tag ? {color: tag.color()} : ''}
|
||||
title={description || ''}>
|
||||
{tagIcon(tag, {className: 'Button-icon'})}
|
||||
{this.props.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
static initProps(props) {
|
||||
const tag = props.tag;
|
||||
|
||||
props.params.tags = tag ? tag.slug() : 'untagged';
|
||||
props.href = app.route('tag', props.params);
|
||||
props.children = tag ? tag.name() : app.trans('tags.untagged');
|
||||
}
|
||||
}
|
101
extensions/tags/js/forum/src/components/TagsPage.js
Normal file
101
extensions/tags/js/forum/src/components/TagsPage.js
Normal file
@ -0,0 +1,101 @@
|
||||
import Component from 'flarum/Component';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
|
||||
import sortTags from 'tags/utils/sortTags';
|
||||
|
||||
export default class TagsPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.tags = sortTags(app.store.all('tags').filter(tag => !tag.parent()));
|
||||
|
||||
app.current = this;
|
||||
app.history.push('tags');
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
}
|
||||
|
||||
view() {
|
||||
const pinned = this.tags.filter(tag => tag.position() !== null);
|
||||
const cloud = this.tags.filter(tag => tag.position() === null);
|
||||
|
||||
return (
|
||||
<div className="TagsPage">
|
||||
{IndexPage.prototype.hero()}
|
||||
<div className="container">
|
||||
<nav className="TagsPage-nav IndexPage-nav sideNav" config={IndexPage.prototype.affixSidebar}>
|
||||
<ul>{listItems(IndexPage.prototype.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
|
||||
<div className="TagsPage-content sideNavOffset">
|
||||
<ul className="TagTiles">
|
||||
{pinned.map(tag => {
|
||||
const lastDiscussion = tag.lastDiscussion();
|
||||
const children = app.store.all('tags').filter(child => child.parent() === tag);
|
||||
|
||||
return (
|
||||
<li className={'TagTile ' + (tag.color() ? 'colored' : '')}
|
||||
style={{backgroundColor: tag.color()}}>
|
||||
<a className="TagTile-info" href={app.route.tag(tag)} config={m.route}>
|
||||
<h3 className="TagTile-name">{tag.name()}</h3>
|
||||
<p className="TagTile-description">{tag.description()}</p>
|
||||
{children
|
||||
? (
|
||||
<div className="TagTile-children">
|
||||
{children.map(child =>
|
||||
<a href={app.route.tag(child)} config={m.route} onclick={e => e.stopPropagation()}>
|
||||
{child.name()}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : ''}
|
||||
</a>
|
||||
{lastDiscussion
|
||||
? (
|
||||
<a className="TagTile-lastDiscussion"
|
||||
href={app.route.discussion(lastDiscussion, lastDiscussion.lastPostNumber())}
|
||||
config={m.route}>
|
||||
{humanTime(lastDiscussion.lastTime())}
|
||||
<span className="TagTile-lastDiscussion-title">{lastDiscussion.title()}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="TagTile-lastDiscussion"/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{cloud.length ? (
|
||||
<div className="TagCloud">
|
||||
<h4 className="TagCloud-title">{app.trans('tags.tag_cloud_title')}</h4>
|
||||
<div className="TagCloud-content">
|
||||
{cloud.map(tag => {
|
||||
const color = tag.color();
|
||||
|
||||
return [
|
||||
<a href={app.route.tag(tag)} config={m.route} style={color ? {color} : ''}>
|
||||
{tag.name()}
|
||||
</a>,
|
||||
' '
|
||||
];
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
config() {
|
||||
IndexPage.prototype.config.apply(this, arguments);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
IndexPage.prototype.onunload.apply(this, arguments);
|
||||
}
|
||||
}
|
12
extensions/tags/js/forum/src/helpers/tagIcon.js
Normal file
12
extensions/tags/js/forum/src/helpers/tagIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
export default function tagIcon(tag, attrs = {}) {
|
||||
attrs.className = 'icon TagIcon ' + (attrs.className || '');
|
||||
|
||||
if (tag) {
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.style.backgroundColor = tag.color();
|
||||
} else {
|
||||
attrs.className += ' untagged';
|
||||
}
|
||||
|
||||
return <span {...attrs}/>;
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
export default function tagsLabel(tag, attrs) {
|
||||
attrs = attrs || {};
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.className = attrs.className || '';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
var link = attrs.link;
|
||||
delete attrs.link;
|
||||
export default function tagLabel(tag, attrs = {}) {
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.className = 'TagLabel ' + (attrs.className || '');
|
||||
|
||||
const link = extract(attrs, 'link');
|
||||
if (link) {
|
||||
attrs.href = app.route('tag', {tags: tag.slug()});
|
||||
attrs.config = m.route;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
var color = tag.color();
|
||||
const color = tag.color();
|
||||
if (color) {
|
||||
attrs.style.backgroundColor = attrs.style.color = color;
|
||||
attrs.className += ' colored';
|
||||
@ -24,5 +24,11 @@ export default function tagsLabel(tag, attrs) {
|
||||
attrs.className += ' untagged';
|
||||
}
|
||||
|
||||
return m((link ? 'a' : 'span')+'.tag-label', attrs, m('span.tag-label-text', tag ? tag.name() : 'Untagged'));
|
||||
return (
|
||||
m((link ? 'a' : 'span'), attrs,
|
||||
<span className="TagLabel-text">
|
||||
{tag ? tag.name() : app.trans('tags.untagged')}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
22
extensions/tags/js/forum/src/helpers/tagsLabel.js
Normal file
22
extensions/tags/js/forum/src/helpers/tagsLabel.js
Normal file
@ -0,0 +1,22 @@
|
||||
import extract from 'flarum/utils/extract';
|
||||
import tagLabel from 'tags/helpers/tagLabel';
|
||||
import sortTags from 'tags/utils/sortTags';
|
||||
|
||||
export default function tagsLabel(tags, attrs = {}) {
|
||||
const children = [];
|
||||
const link = extract(attrs, 'link');
|
||||
|
||||
attrs.className = 'TagsLabel ' + (attrs.className || '');
|
||||
|
||||
if (tags) {
|
||||
sortTags(tags).forEach(tag => {
|
||||
if (tag || tags.length === 1) {
|
||||
children.push(tagLabel(tag, {link}));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
children.push(tagLabel());
|
||||
}
|
||||
|
||||
return <span {...attrs}>{children}</span>;
|
||||
}
|
33
extensions/tags/js/forum/src/main.js
Normal file
33
extensions/tags/js/forum/src/main.js
Normal file
@ -0,0 +1,33 @@
|
||||
import Model from 'flarum/Model';
|
||||
import Discussion from 'flarum/models/Discussion';
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
|
||||
import Tag from 'tags/models/Tag';
|
||||
import TagsPage from 'tags/components/TagsPage';
|
||||
import DiscussionTaggedPost from 'tags/components/DiscussionTaggedPost';
|
||||
|
||||
import addTagList from 'tags/addTagList';
|
||||
import addTagFilter from 'tags/addTagFilter';
|
||||
import addTagLabels from 'tags/addTagLabels';
|
||||
import addTagControl from 'tags/addTagControl';
|
||||
import addTagComposer from 'tags/addTagComposer';
|
||||
|
||||
app.initializers.add('tags', function(app) {
|
||||
app.routes.tags = {path: '/tags', component: TagsPage.component()};
|
||||
app.routes.tag = {path: '/t/:tags', component: IndexPage.component()};
|
||||
|
||||
app.route.tag = tag => app.route('tag', {tags: tag.slug()});
|
||||
|
||||
app.postComponents.discussionTagged = DiscussionTaggedPost;
|
||||
|
||||
app.store.models.tags = Tag;
|
||||
|
||||
Discussion.prototype.tags = Model.hasMany('tags');
|
||||
Discussion.prototype.canTag = Model.attribute('canTag');
|
||||
|
||||
addTagList();
|
||||
addTagFilter();
|
||||
addTagLabels();
|
||||
addTagControl();
|
||||
addTagComposer();
|
||||
});
|
24
extensions/tags/js/forum/src/models/Tag.js
Normal file
24
extensions/tags/js/forum/src/models/Tag.js
Normal file
@ -0,0 +1,24 @@
|
||||
import Model from 'flarum/Model';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
|
||||
export default class Tag extends mixin(Model, {
|
||||
name: Model.attribute('name'),
|
||||
slug: Model.attribute('slug'),
|
||||
description: Model.attribute('description'),
|
||||
|
||||
color: Model.attribute('color'),
|
||||
backgroundUrl: Model.attribute('backgroundUrl'),
|
||||
backgroundMode: Model.attribute('backgroundMode'),
|
||||
iconUrl: Model.attribute('iconUrl'),
|
||||
|
||||
position: Model.attribute('position'),
|
||||
parent: Model.hasOne('parent'),
|
||||
defaultSort: Model.attribute('defaultSort'),
|
||||
isChild: Model.attribute('isChild'),
|
||||
|
||||
discussionsCount: Model.attribute('discussionsCount'),
|
||||
lastTime: Model.attribute('lastTime', Model.transformDate),
|
||||
lastDiscussion: Model.hasOne('lastDiscussion'),
|
||||
|
||||
canStartDiscussion: Model.attribute('canStartDiscussion')
|
||||
}) {}
|
@ -1,10 +1,10 @@
|
||||
export default function sortTags(tags) {
|
||||
return tags.slice(0).sort((a, b) => {
|
||||
var aPos = a.position();
|
||||
var bPos = b.position();
|
||||
const aPos = a.position();
|
||||
const bPos = b.position();
|
||||
|
||||
var aParent = a.parent();
|
||||
var bParent = b.parent();
|
||||
const aParent = a.parent();
|
||||
const bParent = b.parent();
|
||||
|
||||
if (aPos === null && bPos === null) {
|
||||
return b.discussionsCount() - a.discussionsCount();
|
||||
@ -22,4 +22,4 @@ export default function sortTags(tags) {
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { extend, override } from 'flarum/extension-utils';
|
||||
import IndexPage from 'flarum/components/index-page';
|
||||
import DiscussionComposer from 'flarum/components/discussion-composer';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
import TagDiscussionModal from 'flarum-tags/components/tag-discussion-modal';
|
||||
import tagsLabel from 'flarum-tags/helpers/tags-label';
|
||||
|
||||
export default function() {
|
||||
override(IndexPage.prototype, 'composeNewDiscussion', function(original, deferred) {
|
||||
var tag = app.store.getBy('tags', 'slug', this.params().tags);
|
||||
|
||||
app.modal.show(
|
||||
new TagDiscussionModal({
|
||||
selectedTags: tag ? [tag] : [],
|
||||
onsubmit: tags => {
|
||||
original(deferred).then(component => component.tags(tags));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
// Add tag-selection abilities to the discussion composer.
|
||||
DiscussionComposer.prototype.tags = m.prop([]);
|
||||
DiscussionComposer.prototype.chooseTags = function() {
|
||||
app.modal.show(
|
||||
new TagDiscussionModal({
|
||||
selectedTags: this.tags().slice(0),
|
||||
onsubmit: tags => {
|
||||
this.tags(tags);
|
||||
this.$('textarea').focus();
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Add a tag-selection menu to the discussion composer's header, after the
|
||||
// title.
|
||||
extend(DiscussionComposer.prototype, 'headerItems', function(items) {
|
||||
var tags = this.tags();
|
||||
|
||||
items.add('tags', m('a[href=javascript:;][tabindex=-1].control-change-tags', {onclick: this.chooseTags.bind(this)}, [
|
||||
tagsLabel(tags)
|
||||
]));
|
||||
});
|
||||
|
||||
// Add the selected tags as data to submit to the server.
|
||||
extend(DiscussionComposer.prototype, 'data', function(data) {
|
||||
data.links = data.links || {};
|
||||
data.links.tags = this.tags();
|
||||
});
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { extend } from 'flarum/extension-utils';
|
||||
import Discussion from 'flarum/models/discussion';
|
||||
import ActionButton from 'flarum/components/action-button';
|
||||
|
||||
import TagDiscussionModal from 'flarum-tags/components/tag-discussion-modal';
|
||||
|
||||
export default function() {
|
||||
// Add a control allowing the discussion to be moved to another category.
|
||||
extend(Discussion.prototype, 'moderationControls', function(items) {
|
||||
if (this.canTag()) {
|
||||
items.add('tags', ActionButton.component({
|
||||
label: 'Edit Tags',
|
||||
icon: 'tag',
|
||||
onclick: () => app.modal.show(new TagDiscussionModal({ discussion: this }))
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import { extend } from 'flarum/extension-utils';
|
||||
import IndexPage from 'flarum/components/index-page';
|
||||
import DiscussionList from 'flarum/components/discussion-list';
|
||||
|
||||
import TagHero from 'flarum-tags/components/tag-hero';
|
||||
|
||||
export default function() {
|
||||
IndexPage.prototype.currentTag = function() {
|
||||
var slug = this.params().tags;
|
||||
if (slug) {
|
||||
return app.store.getBy('tags', 'slug', slug);
|
||||
}
|
||||
};
|
||||
|
||||
// If currently viewing a tag, insert a tag hero at the top of the
|
||||
// view.
|
||||
extend(IndexPage.prototype, 'view', function(view) {
|
||||
var tag = this.currentTag();
|
||||
if (tag) {
|
||||
view.children[0] = TagHero.component({tag});
|
||||
}
|
||||
});
|
||||
|
||||
// If currently viewing a tag, restyle the 'new discussion' button to use
|
||||
// the tag's color.
|
||||
extend(IndexPage.prototype, 'sidebarItems', function(items) {
|
||||
var tag = this.currentTag();
|
||||
if (tag) {
|
||||
var color = tag.color();
|
||||
if (color) {
|
||||
items.newDiscussion.content.props.style = 'background-color: '+color;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add a parameter for the IndexPage to pass on to the DiscussionList that
|
||||
// will let us filter discussions by tag.
|
||||
extend(IndexPage.prototype, 'params', function(params) {
|
||||
params.tags = m.route.param('tags');
|
||||
});
|
||||
|
||||
// Translate that parameter into a gambit appended to the search query.
|
||||
extend(DiscussionList.prototype, 'params', function(params) {
|
||||
params.include.push('tags');
|
||||
if (params.tags) {
|
||||
params.q = (params.q || '')+' tag:'+params.tags;
|
||||
delete params.tags;
|
||||
}
|
||||
});
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import { extend } from 'flarum/extension-utils';
|
||||
import DiscussionListItem from 'flarum/components/discussion-list-item';
|
||||
import DiscussionPage from 'flarum/components/discussion-page';
|
||||
import DiscussionHero from 'flarum/components/discussion-hero';
|
||||
|
||||
import tagsLabel from 'flarum-tags/helpers/tags-label';
|
||||
import sortTags from 'flarum-tags/utils/sort-tags';
|
||||
|
||||
export default function() {
|
||||
// Add tag labels to each discussion in the discussion list.
|
||||
extend(DiscussionListItem.prototype, 'infoItems', function(items) {
|
||||
var tags = this.props.discussion.tags();
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags), {first: true});
|
||||
}
|
||||
});
|
||||
|
||||
// Include a discussion's tags when fetching it.
|
||||
extend(DiscussionPage.prototype, 'params', function(params) {
|
||||
params.include.push('tags');
|
||||
});
|
||||
|
||||
// Restyle a discussion's hero to use its first tag's color.
|
||||
extend(DiscussionHero.prototype, 'view', function(view) {
|
||||
var tags = sortTags(this.props.discussion.tags());
|
||||
if (tags && tags.length) {
|
||||
var color = tags[0].color();
|
||||
if (color) {
|
||||
view.attrs.style = 'background-color: '+color;
|
||||
view.attrs.className += ' discussion-hero-colored';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add a list of a discussion's tags to the discussion hero, displayed
|
||||
// before the title. Put the title on its own line.
|
||||
extend(DiscussionHero.prototype, 'items', function(items) {
|
||||
var tags = this.props.discussion.tags();
|
||||
if (tags && tags.length) {
|
||||
items.add('tags', tagsLabel(tags, {link: true}), {before: 'title'});
|
||||
|
||||
items.title.content.wrapperClass = 'block-item';
|
||||
}
|
||||
});
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
import { extend } from 'flarum/extension-utils';
|
||||
import IndexPage from 'flarum/components/index-page';
|
||||
import NavItem from 'flarum/components/nav-item';
|
||||
import Separator from 'flarum/components/separator';
|
||||
|
||||
import TagNavItem from 'flarum-tags/components/tag-nav-item';
|
||||
import TagsPage from 'flarum-tags/components/tags-page';
|
||||
|
||||
export default function() {
|
||||
// Add a link to the tags page, as well as a list of all the tags,
|
||||
// to the index page's sidebar.
|
||||
extend(IndexPage.prototype, 'navItems', function(items) {
|
||||
items.add('tags', NavItem.component({
|
||||
icon: 'th-large',
|
||||
label: 'Tags',
|
||||
href: app.route('tags'),
|
||||
config: m.route
|
||||
}), {last: true});
|
||||
|
||||
if (app.current instanceof TagsPage) return;
|
||||
|
||||
items.add('separator', Separator.component(), {last: true});
|
||||
|
||||
var params = this.stickyParams();
|
||||
var tags = app.store.all('tags');
|
||||
|
||||
var addTag = tag => {
|
||||
var currentTag = this.currentTag();
|
||||
var active = currentTag === tag;
|
||||
if (!active && currentTag) {
|
||||
currentTag = currentTag.parent();
|
||||
active = currentTag === tag;
|
||||
}
|
||||
items.add('tag'+tag.id(), TagNavItem.component({tag, params, active}), {last: true});
|
||||
}
|
||||
|
||||
tags.filter(tag => tag.position() !== null && !tag.isChild()).sort((a, b) => a.position() - b.position()).forEach(addTag);
|
||||
|
||||
var more = tags.filter(tag => tag.position() === null).sort((a, b) => b.discussionsCount() - a.discussionsCount());
|
||||
|
||||
more.splice(0, 3).forEach(addTag);
|
||||
|
||||
if (more.length) {
|
||||
items.add('moreTags', NavItem.component({
|
||||
label: 'More...',
|
||||
href: app.route('tags'),
|
||||
config: m.route
|
||||
}), {last: true});;
|
||||
}
|
||||
});
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import EventPost from 'flarum/components/event-post';
|
||||
import tagsLabel from 'flarum-tags/helpers/tags-label';
|
||||
|
||||
export default class DiscussionTaggedPost extends EventPost {
|
||||
view() {
|
||||
var post = this.props.post;
|
||||
var oldTags = post.content()[0];
|
||||
var newTags = post.content()[1];
|
||||
|
||||
var added = newTags.filter(tag => oldTags.indexOf(tag) === -1).map(id => app.store.getById('tags', id));
|
||||
var removed = oldTags.filter(tag => newTags.indexOf(tag) === -1).map(id => app.store.getById('tags', id));
|
||||
var total = added.concat(removed);
|
||||
|
||||
var build = function(verb, tags, only) {
|
||||
return tags.length ? [verb, ' ', only && tags.length == 1 ? 'the ' : '', tagsLabel(tags, {link: true})] : '';
|
||||
};
|
||||
|
||||
return super.view('tag', [
|
||||
build('added', added, !removed.length),
|
||||
added.length && removed.length ? ' and ' : '',
|
||||
build('removed', removed, !added.length),
|
||||
total.length ? (total.length == 1 ? ' tag.' : ' tags.') : ''
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
import FormModal from 'flarum/components/form-modal';
|
||||
import DiscussionPage from 'flarum/components/discussion-page';
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import classList from 'flarum/utils/class-list';
|
||||
|
||||
import tagLabel from 'flarum-tags/helpers/tag-label';
|
||||
import tagIcon from 'flarum-tags/helpers/tag-icon';
|
||||
import sortTags from 'flarum-tags/utils/sort-tags';
|
||||
|
||||
export default class TagDiscussionModal extends FormModal {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.tags = sortTags(app.store.all('tags').filter(tag => tag.canStartDiscussion()));
|
||||
|
||||
this.selected = m.prop([]);
|
||||
if (this.props.selectedTags) {
|
||||
this.props.selectedTags.map(this.addTag.bind(this));
|
||||
} else if (this.props.discussion) {
|
||||
this.props.discussion.tags().map(this.addTag.bind(this));
|
||||
}
|
||||
|
||||
this.filter = m.prop('');
|
||||
|
||||
this.index = m.prop(this.tags[0].id());
|
||||
|
||||
this.focused = m.prop(false);
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
if (!tag.canStartDiscussion()) return;
|
||||
|
||||
var selected = this.selected();
|
||||
var parent = tag.parent();
|
||||
if (parent) {
|
||||
var index = selected.indexOf(parent);
|
||||
if (index === -1) {
|
||||
selected.push(parent);
|
||||
}
|
||||
}
|
||||
selected.push(tag);
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
var selected = this.selected();
|
||||
var index = selected.indexOf(tag);
|
||||
selected.splice(index, 1);
|
||||
selected.filter(selected => selected.parent() && selected.parent() === tag).forEach(child => {
|
||||
var index = selected.indexOf(child);
|
||||
selected.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
view() {
|
||||
var discussion = this.props.discussion;
|
||||
var selected = this.selected();
|
||||
|
||||
var tags = this.tags;
|
||||
var filter = this.filter().toLowerCase();
|
||||
|
||||
if (filter) {
|
||||
tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter);
|
||||
}
|
||||
|
||||
if (tags.indexOf(this.index()) === -1) {
|
||||
this.index(tags[0]);
|
||||
}
|
||||
|
||||
return super.view({
|
||||
className: 'tag-discussion-modal',
|
||||
title: discussion
|
||||
? ['Edit Tags for ', m('em', discussion.title())]
|
||||
: 'Start a Discussion About...',
|
||||
body: [
|
||||
m('div.tags-form', [
|
||||
m('div.tags-input.form-control', {className: this.focused() ? 'focus' : ''}, [
|
||||
m('span.tags-input-selected', selected.map(tag =>
|
||||
m('span.remove-tag', {onclick: () => {
|
||||
this.removeTag(tag);
|
||||
this.ready();
|
||||
}}, tagLabel(tag))
|
||||
)),
|
||||
m('input.form-control', {
|
||||
placeholder: !selected.length ? 'Choose one or more topics' : '',
|
||||
value: this.filter(),
|
||||
oninput: m.withAttr('value', this.filter),
|
||||
onkeydown: this.onkeydown.bind(this),
|
||||
onfocus: () => this.focused(true),
|
||||
onblur: () => this.focused(false)
|
||||
})
|
||||
]),
|
||||
m('span.primary-control',
|
||||
m('button[type=submit].btn.btn-primary', {disabled: !selected.length}, icon('check icon'), m('span.label', 'Confirm'))
|
||||
)
|
||||
])
|
||||
],
|
||||
footer: [
|
||||
m('ul.tags-select', tags.map(tag =>
|
||||
filter || !tag.parent() || selected.indexOf(tag.parent()) !== -1
|
||||
? m('li', {
|
||||
'data-index': tag.id(),
|
||||
className: classList({
|
||||
pinned: tag.position() !== null,
|
||||
child: !!tag.parent(),
|
||||
colored: !!tag.color(),
|
||||
selected: selected.indexOf(tag) !== -1,
|
||||
active: this.index() == tag
|
||||
}),
|
||||
style: {
|
||||
color: tag.color()
|
||||
},
|
||||
onmouseover: () => {
|
||||
this.index(tag);
|
||||
},
|
||||
onclick: () => {
|
||||
var selected = this.selected();
|
||||
var index = selected.indexOf(tag);
|
||||
if (index !== -1) {
|
||||
this.removeTag(tag);
|
||||
} else {
|
||||
this.addTag(tag);
|
||||
}
|
||||
if (this.filter()) {
|
||||
this.filter('');
|
||||
this.index(this.tags[0]);
|
||||
}
|
||||
this.ready();
|
||||
}
|
||||
}, [
|
||||
tagIcon(tag),
|
||||
m('span.name', highlight(tag.name(), filter)),
|
||||
tag.description() ? m('span.description', {title: tag.description()}, tag.description()) : ''
|
||||
])
|
||||
: ''
|
||||
))
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
onkeydown(e) {
|
||||
switch (e.which) {
|
||||
case 40:
|
||||
case 38: // Down/Up
|
||||
e.preventDefault();
|
||||
this.setIndex(this.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true);
|
||||
break;
|
||||
|
||||
case 13: // Return
|
||||
e.preventDefault();
|
||||
if (e.metaKey || e.ctrlKey || this.selected().indexOf(this.index()) !== -1) {
|
||||
if (this.selected().length) {
|
||||
this.$('form').submit();
|
||||
}
|
||||
} else {
|
||||
this.getItem(this.index())[0].dispatchEvent(new Event('click'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 8: // Backspace
|
||||
if (e.target.selectionStart == 0 && e.target.selectionEnd == 0) {
|
||||
e.preventDefault();
|
||||
var selected = this.selected();
|
||||
selected.splice(selected.length - 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectableItems() {
|
||||
return this.$('.tags-select > li');
|
||||
}
|
||||
|
||||
getCurrentNumericIndex() {
|
||||
return this.selectableItems().index(
|
||||
this.getItem(this.index())
|
||||
);
|
||||
}
|
||||
|
||||
getItem(index) {
|
||||
var $items = this.selectableItems();
|
||||
return $items.filter('[data-index='+index.id()+']');
|
||||
}
|
||||
|
||||
setIndex(index, scrollToItem) {
|
||||
var $items = this.selectableItems();
|
||||
var $dropdown = $items.parent();
|
||||
|
||||
if (index < 0) {
|
||||
index = $items.length - 1;
|
||||
} else if (index >= $items.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
var $item = $items.eq(index);
|
||||
|
||||
this.index(app.store.getById('tags', $item.attr('data-index')));
|
||||
|
||||
m.redraw();
|
||||
|
||||
if (scrollToItem) {
|
||||
var dropdownScroll = $dropdown.scrollTop();
|
||||
var dropdownTop = $dropdown.offset().top;
|
||||
var dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
||||
var itemTop = $item.offset().top;
|
||||
var itemBottom = itemTop + $item.outerHeight();
|
||||
|
||||
var scrollTop;
|
||||
if (itemTop < dropdownTop) {
|
||||
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'));
|
||||
} else if (itemBottom > dropdownBottom) {
|
||||
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'));
|
||||
}
|
||||
|
||||
if (typeof scrollTop !== 'undefined') {
|
||||
$dropdown.stop(true).animate({scrollTop}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var discussion = this.props.discussion;
|
||||
var tags = this.selected();
|
||||
|
||||
if (discussion) {
|
||||
discussion.save({links: {tags}}).then(discussion => {
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
app.current.stream.sync();
|
||||
}
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
this.props.onsubmit && this.props.onsubmit(tags);
|
||||
|
||||
app.modal.close();
|
||||
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import Component from 'flarum/component';
|
||||
|
||||
export default class TagHero extends Component {
|
||||
view() {
|
||||
var tag = this.props.tag;
|
||||
var color = tag.color();
|
||||
|
||||
return m('header.hero.tag-hero', {style: color ? 'color: #fff; background-color: '+tag.color() : ''}, [
|
||||
m('div.container', [
|
||||
m('div.container-narrow', [
|
||||
m('h2', tag.name()),
|
||||
m('div.subtitle', tag.description())
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import NavItem from 'flarum/components/nav-item';
|
||||
import tagIcon from 'flarum-tags/helpers/tag-icon';
|
||||
|
||||
export default class TagNavItem extends NavItem {
|
||||
view() {
|
||||
var tag = this.props.tag;
|
||||
var active = this.constructor.active(this.props);
|
||||
var description = tag && tag.description();
|
||||
var children;
|
||||
|
||||
if (active && tag) {
|
||||
children = app.store.all('tags').filter(child => {
|
||||
var parent = child.parent();
|
||||
return parent && parent.id() == tag.id();
|
||||
});
|
||||
}
|
||||
|
||||
return m('li'+(active ? '.active' : ''),
|
||||
m('a.has-icon', {
|
||||
href: this.props.href,
|
||||
config: m.route,
|
||||
onclick: () => {
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.forceReload = true;
|
||||
}
|
||||
m.redraw.strategy('none');
|
||||
},
|
||||
style: (active && tag) ? 'color: '+tag.color() : '',
|
||||
title: description || ''
|
||||
}, [
|
||||
tagIcon(tag, {className: 'icon'}),
|
||||
this.props.label
|
||||
]),
|
||||
children && children.length ? m('ul.dropdown-menu', children.map(tag => TagNavItem.component({tag, params: this.props.params}))) : ''
|
||||
);
|
||||
}
|
||||
|
||||
static props(props) {
|
||||
var tag = props.tag;
|
||||
props.params.tags = tag ? tag.slug() : 'untagged';
|
||||
props.href = app.route('tag', props.params);
|
||||
props.label = tag ? tag.name() : 'Untagged';
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
import Component from 'flarum/component';
|
||||
import WelcomeHero from 'flarum/components/welcome-hero';
|
||||
import IndexPage from 'flarum/components/index-page';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
import abbreviateNumber from 'flarum/utils/abbreviate-number';
|
||||
import humanTime from 'flarum/helpers/human-time';
|
||||
|
||||
import sortTags from 'flarum-tags/utils/sort-tags';
|
||||
|
||||
export default class TagsPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.tags = sortTags(app.store.all('tags').filter(tag => !tag.parent()));
|
||||
|
||||
app.current = this;
|
||||
app.history.push('tags');
|
||||
}
|
||||
|
||||
view() {
|
||||
var pinned = this.tags.filter(tag => tag.position() !== null);
|
||||
var cloud = this.tags.filter(tag => tag.position() === null);
|
||||
|
||||
return m('div.tags-area', {config: this.onload.bind(this)}, [
|
||||
IndexPage.prototype.hero(),
|
||||
m('div.container', [
|
||||
m('nav.side-nav.index-nav', [
|
||||
m('ul', listItems(IndexPage.prototype.sidebarItems().toArray()))
|
||||
]),
|
||||
m('div.offset-content.tags-content', [
|
||||
m('ul.tag-tiles', [
|
||||
pinned.map(tag => {
|
||||
var lastDiscussion = tag.lastDiscussion();
|
||||
var children = app.store.all('tags').filter(child => {
|
||||
var parent = child.parent();
|
||||
return parent && parent.id() == tag.id();
|
||||
});
|
||||
|
||||
return m('li.tag-tile', {className: tag.color() ? 'colored' : '', style: 'background-color: '+tag.color()}, [
|
||||
m('a.tag-info', {href: app.route.tag(tag), config: m.route}, [
|
||||
m('h3.name', tag.name()),
|
||||
m('p.description', tag.description()),
|
||||
children ? m('div.children', children.map(tag =>
|
||||
m('a', {href: app.route.tag(tag), config: function(element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).on('click', e => e.stopPropagation());
|
||||
m.route.apply(this, arguments);
|
||||
}}, tag.name())
|
||||
)) : ''
|
||||
]),
|
||||
lastDiscussion
|
||||
? m('a.last-discussion', {
|
||||
href: app.route.discussion(lastDiscussion, lastDiscussion.lastPostNumber()),
|
||||
config: m.route
|
||||
}, [humanTime(lastDiscussion.lastTime()), m('span.title', lastDiscussion.title())])
|
||||
: m('span.last-discussion')
|
||||
]);
|
||||
})
|
||||
]),
|
||||
cloud.length ? m('div.tag-cloud', [
|
||||
m('h4', 'Tags'),
|
||||
m('div.tag-cloud-content', cloud.map(tag => [
|
||||
m('a', {href: app.route.tag(tag), config: m.route, style: tag.color() ? 'color: '+tag.color() : ''}, tag.name()),
|
||||
' '
|
||||
]))
|
||||
]) : ''
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
onload(element, isInitialized, context) {
|
||||
IndexPage.prototype.onload.apply(this, arguments);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
IndexPage.prototype.onunload.apply(this);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
export default function tagIcon(tag, attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (tag) {
|
||||
attrs.style = attrs.style || {};
|
||||
attrs.style.backgroundColor = tag.color();
|
||||
} else {
|
||||
attrs.className = (attrs.className || '')+' untagged';
|
||||
}
|
||||
|
||||
return m('span.icon.tag-icon', attrs);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import tagLabel from 'flarum-tags/helpers/tag-label';
|
||||
import sortTags from 'flarum-tags/utils/sort-tags';
|
||||
|
||||
export default function tagsLabel(tags, attrs) {
|
||||
attrs = attrs || {};
|
||||
var children = [];
|
||||
|
||||
var link = attrs.link;
|
||||
delete attrs.link;
|
||||
|
||||
if (tags) {
|
||||
sortTags(tags).forEach(tag => {
|
||||
if (tag || tags.length === 1) {
|
||||
children.push(tagLabel(tag, {link}));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
children.push(tagLabel());
|
||||
}
|
||||
|
||||
return m('span.tags-label', attrs, children);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import Model from 'flarum/model';
|
||||
|
||||
class Tag extends Model {}
|
||||
|
||||
Tag.prototype.id = Model.prop('id');
|
||||
Tag.prototype.name = Model.prop('name');
|
||||
Tag.prototype.slug = Model.prop('slug');
|
||||
Tag.prototype.description = Model.prop('description');
|
||||
|
||||
Tag.prototype.color = Model.prop('color');
|
||||
Tag.prototype.backgroundUrl = Model.prop('backgroundUrl');
|
||||
Tag.prototype.backgroundMode = Model.prop('backgroundMode');
|
||||
Tag.prototype.iconUrl = Model.prop('iconUrl');
|
||||
|
||||
Tag.prototype.position = Model.prop('position');
|
||||
Tag.prototype.parent = Model.one('parent');
|
||||
Tag.prototype.defaultSort = Model.prop('defaultSort');
|
||||
Tag.prototype.isChild = Model.prop('isChild');
|
||||
|
||||
Tag.prototype.discussionsCount = Model.prop('discussionsCount');
|
||||
Tag.prototype.lastTime = Model.prop('lastTime', Model.date);
|
||||
Tag.prototype.lastDiscussion = Model.one('lastDiscussion');
|
||||
|
||||
Tag.prototype.canStartDiscussion = Model.prop('canStartDiscussion');
|
||||
|
||||
export default Tag;
|
@ -1,236 +0,0 @@
|
||||
.category-label {
|
||||
text-transform: uppercase;
|
||||
font-size: 80%;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.45em;
|
||||
border-radius: 4px;
|
||||
|
||||
&.uncategorized {
|
||||
border: 1px dotted @fl-body-muted-color;
|
||||
color: @fl-body-muted-color;
|
||||
}
|
||||
|
||||
& .category-label-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.discussion-summary & {
|
||||
margin-right: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.discussion-hero & {
|
||||
font-size: 14px;
|
||||
background: #fff !important;
|
||||
padding: 2px 6px;
|
||||
|
||||
& .category-label-text {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-moved-post & {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
.discussion-hero {
|
||||
& .block-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
border-radius: @border-radius-base;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
margin-left: 1px;
|
||||
|
||||
&.uncategorized {
|
||||
border: 1px dotted @fl-body-muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-area .container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control-change-category {
|
||||
vertical-align: 1px;
|
||||
margin: -10px 0;
|
||||
|
||||
& .label {
|
||||
margin: 0 2px 0 5px;
|
||||
}
|
||||
|
||||
.minimized & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-move-discussion {
|
||||
& .modal-header {
|
||||
padding: 20px;
|
||||
|
||||
& h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
& .modal-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
& .category-list {
|
||||
background: @fl-body-secondary-color;
|
||||
}
|
||||
& .category-tile .title {
|
||||
margin-bottom: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
& .category-tile .description {
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
& .count {
|
||||
display: none;
|
||||
}
|
||||
& .category-tile {
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 125px;
|
||||
|
||||
& > a {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.category-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: @fl-body-control-bg;
|
||||
color: @fl-body-control-color;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media @tablet {
|
||||
.category-list-tiles {
|
||||
& > li {
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 175px;
|
||||
|
||||
& > a {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.categories-forum-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media @desktop, @desktop-hd {
|
||||
.category-list-tiles {
|
||||
& > li {
|
||||
float: left;
|
||||
width: 33.333%;
|
||||
height: 175px;
|
||||
|
||||
& > a {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.category-tile {
|
||||
position: relative;
|
||||
|
||||
&, & > a {
|
||||
color: #fff;
|
||||
}
|
||||
& > a {
|
||||
display: block;
|
||||
padding: 25px;
|
||||
transition: background 0.1s;
|
||||
text-decoration: none;
|
||||
}
|
||||
& > a:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
& > a:active {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
& .title {
|
||||
font-size: 20px;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
& .description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
& .count {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
.filter-tile a {
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 18px;
|
||||
color: @fl-body-control-color;
|
||||
}
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.filter-tile {
|
||||
& > a {
|
||||
float: left;
|
||||
width: 50%;
|
||||
border-right: 1px solid #fff;
|
||||
|
||||
&:first-child:last-child {
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
& a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
font-size: 18px;
|
||||
color: @fl-body-control-color;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.filter-list {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
height: 100%;
|
||||
display: table;
|
||||
width: 50%;
|
||||
table-layout: fixed;
|
||||
|
||||
& > li {
|
||||
display: table-row;
|
||||
height: 1%;
|
||||
|
||||
&:not(:last-child) a {
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,391 +0,0 @@
|
||||
.tag-label {
|
||||
font-size: 85%;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.55em;
|
||||
border-radius: @border-radius-base;
|
||||
background: @fl-body-secondary-color;
|
||||
color: @fl-body-muted-color;
|
||||
|
||||
&.untagged {
|
||||
background: transparent;
|
||||
border: 1px dotted @fl-body-muted-color;
|
||||
color: @fl-body-muted-color;
|
||||
}
|
||||
|
||||
&.colored {
|
||||
& .tag-label-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-hero .tags-label & {
|
||||
background: transparent;
|
||||
border-radius: 4px !important;
|
||||
|
||||
&.colored {
|
||||
margin-right: 5px;
|
||||
background: #fff !important;
|
||||
color: @fl-body-muted-color;
|
||||
|
||||
& .tag-label-text {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.discussion-hero-colored {
|
||||
&, & a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.tags-label {
|
||||
.discussion-summary & {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.discussion-tagged-post & {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
& .tag-label {
|
||||
border-radius: 0;
|
||||
margin-right: 1px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: @border-radius-base 0 0 @border-radius-base;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 @border-radius-base @border-radius-base 0;
|
||||
}
|
||||
&:first-child:last-child {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @todo give all <li>s a class in core, get rid of block-item
|
||||
.discussion-hero {
|
||||
& .block-item {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
border-radius: @border-radius-base;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
margin-left: 1px;
|
||||
background: @fl-body-secondary-color;
|
||||
|
||||
&.untagged {
|
||||
border: 1px dotted @fl-body-muted-color;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.side-nav .dropdown-menu > li > .dropdown-menu {
|
||||
margin-bottom: 10px;
|
||||
|
||||
& .tag-icon {
|
||||
display: none;
|
||||
}
|
||||
& > li > a {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-discussion-modal {
|
||||
& .modal-header {
|
||||
background: @fl-body-secondary-color;
|
||||
padding: 20px 20px 0;
|
||||
|
||||
& h3 {
|
||||
text-align: left;
|
||||
color: @fl-body-muted-color;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media @phone {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
& .modal-body {
|
||||
padding: 20px;
|
||||
|
||||
@media @phone {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
& .modal-footer {
|
||||
padding: 1px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.tags-form {
|
||||
padding-right: 100px;
|
||||
overflow: hidden;
|
||||
|
||||
& .tags-input {
|
||||
float: left;
|
||||
}
|
||||
& .primary-control {
|
||||
margin-right: -100px;
|
||||
float: right;
|
||||
width: 85px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tags-input {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
& input {
|
||||
display: inline;
|
||||
outline: none;
|
||||
margin-top: -2px;
|
||||
border: 0 !important;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
}
|
||||
& .remove-tag {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.tags-input-selected {
|
||||
& .tag-label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-select {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 50vh;
|
||||
|
||||
@media @phone {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 7px 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&.pinned:not(.child) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
& .name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
&.colored {
|
||||
&.selected .tag-icon:before {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background: @fl-body-secondary-color;
|
||||
}
|
||||
& .name {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
@media @phone {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
& .description {
|
||||
color: @fl-body-muted-color;
|
||||
font-size: 12px;
|
||||
|
||||
@media @phone {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
& .tag-icon {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
.fa();
|
||||
content: @fa-var-check;
|
||||
color: @fl-body-muted-color;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& mark {
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tag-tiles {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@media @phone {
|
||||
margin: -15px -15px 0;
|
||||
}
|
||||
|
||||
& > li {
|
||||
height: 200px;
|
||||
margin-bottom: 1px;
|
||||
overflow: hidden;
|
||||
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
float: left;
|
||||
width: ~"calc(50% - 1px)";
|
||||
margin-right: 1px;
|
||||
&:first-child {
|
||||
border-top-left-radius: @border-radius-base;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
border-top-right-radius: @border-radius-base;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(even), &:last-child {
|
||||
border-bottom-right-radius: @border-radius-base;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(odd), &:last-child:nth-child(odd) {
|
||||
border-bottom-left-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag-tile {
|
||||
position: relative;
|
||||
background: @fl-body-secondary-color;
|
||||
|
||||
&, & a {
|
||||
color: @fl-body-muted-color;
|
||||
}
|
||||
&.colored {
|
||||
&, & a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
& .tag-info, & .last-discussion {
|
||||
padding: 20px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
& > a {
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: fade(#000, 5%);
|
||||
}
|
||||
&:active {
|
||||
background: fade(#000, 15%);
|
||||
}
|
||||
}
|
||||
& .tag-info {
|
||||
top: 0;
|
||||
bottom: 45px;
|
||||
padding-right: 20px;
|
||||
|
||||
& .name {
|
||||
font-size: 20px;
|
||||
margin: 0 0 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
& .description {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
& .children {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
|
||||
& > a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& .last-discussion {
|
||||
bottom: 0;
|
||||
height: 45px;
|
||||
padding-top: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 21px;
|
||||
opacity: 0.5;
|
||||
|
||||
&, &:hover, &:active {
|
||||
background: fade(#000, 10%);
|
||||
}
|
||||
|
||||
& .title {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:hover .title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
& time {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag-cloud {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
|
||||
& h4 {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: @fl-body-muted-color;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:before {
|
||||
.fa();
|
||||
content: @fa-var-tags;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag-cloud-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
|
||||
&, & a {
|
||||
color: @fl-body-muted-color;
|
||||
}
|
||||
& a {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
29
extensions/tags/less/forum/TagCloud.less
Normal file
29
extensions/tags/less/forum/TagCloud.less
Normal file
@ -0,0 +1,29 @@
|
||||
.TagCloud {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.TagCloud-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: @muted-color;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:before {
|
||||
.fa();
|
||||
content: @fa-var-tags;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.TagCloud-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
|
||||
&, a {
|
||||
color: @muted-color;
|
||||
}
|
||||
a {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
145
extensions/tags/less/forum/TagDiscussionModal.less
Normal file
145
extensions/tags/less/forum/TagDiscussionModal.less
Normal file
@ -0,0 +1,145 @@
|
||||
.TagDiscussionModal {
|
||||
@media @tablet-up {
|
||||
.Modal-header {
|
||||
background: @control-bg;
|
||||
padding: 20px 20px 0;
|
||||
|
||||
& h3 {
|
||||
text-align: left;
|
||||
color: @control-color;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.Modal-body {
|
||||
padding: 20px;
|
||||
|
||||
@media @phone {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
.Modal-footer {
|
||||
padding: 1px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.TagDiscussionModal-form {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
.TagDiscussionModal-form-input {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
}
|
||||
.TagDiscussionModal-form-submit {
|
||||
display: table-cell;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
.TagsInput {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
input {
|
||||
display: inline;
|
||||
outline: none;
|
||||
margin-top: -2px;
|
||||
border: 0 !important;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
}
|
||||
}
|
||||
.TagsInput-tag {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.TagsInput-selected {
|
||||
.TagLabel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTagList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 50vh;
|
||||
|
||||
@media @phone {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
> li {
|
||||
padding: 7px 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
&.pinned:not(.child) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.SelectTagListItem-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
&.child {
|
||||
padding-left: 48px;
|
||||
}
|
||||
&.active {
|
||||
background: @control-bg;
|
||||
}
|
||||
&.selected {
|
||||
.TagIcon {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
.fa();
|
||||
content: @fa-var-check;
|
||||
color: @muted-color;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 1px;
|
||||
}
|
||||
}
|
||||
&.colored .TagIcon:before {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.SelectTagListItem-name {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
@media @phone {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.SelectTagListItem-description {
|
||||
color: @muted-color;
|
||||
font-size: 12px;
|
||||
width: 370px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
|
||||
@media @phone {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.SelectTagListItem mark {
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
}
|
14
extensions/tags/less/forum/TagIcon.less
Normal file
14
extensions/tags/less/forum/TagIcon.less
Normal file
@ -0,0 +1,14 @@
|
||||
.TagIcon {
|
||||
border-radius: @border-radius;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
margin-left: 1px;
|
||||
background: @control-bg;
|
||||
|
||||
&.untagged {
|
||||
border: 1px dotted @muted-color;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
61
extensions/tags/less/forum/TagLabel.less
Normal file
61
extensions/tags/less/forum/TagLabel.less
Normal file
@ -0,0 +1,61 @@
|
||||
.TagLabel {
|
||||
font-size: 85%;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.5em;
|
||||
border-radius: @border-radius;
|
||||
background: @control-bg;
|
||||
color: @control-color;
|
||||
|
||||
&.untagged {
|
||||
background: transparent;
|
||||
border: 1px dotted @muted-color;
|
||||
color: @muted-color;
|
||||
}
|
||||
|
||||
&.colored {
|
||||
.TagLabel-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DiscussionHero .TagsLabel & {
|
||||
background: transparent;
|
||||
border-radius: @border-radius !important;
|
||||
font-size: 14px;
|
||||
|
||||
&.colored {
|
||||
margin-right: 5px;
|
||||
background: #fff !important;
|
||||
color: @muted-color;
|
||||
|
||||
.TagLabel-text {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.DiscussionHero--colored {
|
||||
&, a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.TagsLabel {
|
||||
.DiscussionTaggedPost & {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.TagLabel {
|
||||
border-radius: 0;
|
||||
|
||||
&:first-child {
|
||||
border-radius: @border-radius 0 0 @border-radius;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 @border-radius @border-radius 0;
|
||||
}
|
||||
&:first-child:last-child {
|
||||
border-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
}
|
115
extensions/tags/less/forum/TagTiles.less
Normal file
115
extensions/tags/less/forum/TagTiles.less
Normal file
@ -0,0 +1,115 @@
|
||||
.TagTiles {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@media @phone {
|
||||
margin: -15px -15px 0;
|
||||
}
|
||||
|
||||
> li {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
|
||||
@media @tablet-up {
|
||||
float: left;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: @border-radius;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
border-top-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(even), &:last-child {
|
||||
border-bottom-right-radius: @border-radius;
|
||||
}
|
||||
&:nth-last-child(2):nth-child(odd), &:last-child:nth-child(odd) {
|
||||
border-bottom-left-radius: @border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TagTile {
|
||||
position: relative;
|
||||
background: @control-bg;
|
||||
|
||||
&, a {
|
||||
color: @control-color;
|
||||
}
|
||||
&.colored {
|
||||
&, a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.TagTile-info, .TagTile-lastDiscussion {
|
||||
padding: 20px;
|
||||
text-decoration: none !important;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.TagTile-info {
|
||||
top: 0;
|
||||
bottom: 45px;
|
||||
padding-right: 20px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: fade(#000, 5%);
|
||||
}
|
||||
&:active {
|
||||
background: fade(#000, 15%);
|
||||
}
|
||||
}
|
||||
.TagTile-name {
|
||||
font-size: 20px;
|
||||
margin: 0 0 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.TagTile-description {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.TagTile-children {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
|
||||
a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
.TagTile-lastDiscussion {
|
||||
bottom: 0;
|
||||
height: 45px;
|
||||
padding-top: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 21px;
|
||||
opacity: 0.5;
|
||||
|
||||
&, &:hover, &:active {
|
||||
background: fade(#000, 10%);
|
||||
}
|
||||
|
||||
&:hover .TagTile-lastDiscussion-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
time {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.TagTile-lastDiscussion-title {
|
||||
margin-right: 10px;
|
||||
}
|
37
extensions/tags/less/forum/extension.less
Normal file
37
extensions/tags/less/forum/extension.less
Normal file
@ -0,0 +1,37 @@
|
||||
@import "TagCloud.less";
|
||||
@import "TagDiscussionModal.less";
|
||||
@import "TagIcon.less";
|
||||
@import "TagLabel.less";
|
||||
@import "TagTiles.less";
|
||||
|
||||
.DiscussionHero {
|
||||
.item-title {
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.TagLinkButton.child {
|
||||
@media @tablet-up {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
margin-left: 10px;
|
||||
|
||||
.TagIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.DiscussionComposer-changeTags {
|
||||
margin-right: 15px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
.DiscussionListItem-info > .item-tags {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@media @tablet-up {
|
||||
.IndexPage .DiscussionListItem-info > .item-tags {
|
||||
float: right;
|
||||
margin-top: -14px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
13
extensions/tags/locale/en.yml
Normal file
13
extensions/tags/locale/en.yml
Normal file
@ -0,0 +1,13 @@
|
||||
tags:
|
||||
discussion_tagged_post:
|
||||
one: "{username} {action} tag."
|
||||
other: "{username} {action} tags."
|
||||
added_tags: "added the {tags}"
|
||||
removed_tags: "removed the {tags}"
|
||||
tag_new_discussion_title: Start a Discussion About...
|
||||
edit_discussion_tags_title: "Edit Tags for {title}"
|
||||
edit_discussion_tags_link: Edit Tags
|
||||
discussion_tags_placeholder: Choose one or more topics
|
||||
confirm: Confirm
|
||||
more: More...
|
||||
tag_cloud_title: Tags
|
@ -1,17 +1,17 @@
|
||||
<?php namespace Flarum\Tags\Events;
|
||||
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Core\Models\User;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Core\Users\User;
|
||||
|
||||
class DiscussionWasTagged
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\Core\Models\Discussion
|
||||
* @var Discussion
|
||||
*/
|
||||
public $discussion;
|
||||
|
||||
/**
|
||||
* @var \Flarum\Core\Models\User
|
||||
* @var User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
@ -21,9 +21,9 @@ class DiscussionWasTagged
|
||||
public $oldTags;
|
||||
|
||||
/**
|
||||
* @param \Flarum\Core\Models\Discussion $discussion
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @param \Flarum\Categories\Category $oldCategory
|
||||
* @param Discussion $discussion
|
||||
* @param User $user
|
||||
* @param \Flarum\Tags\Tag[] $oldTags
|
||||
*/
|
||||
public function __construct(Discussion $discussion, User $user, array $oldTags)
|
||||
{
|
||||
|
20
extensions/tags/src/Extension.php
Normal file
20
extensions/tags/src/Extension.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Support\Extension as BaseExtension;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
|
||||
class Extension extends BaseExtension
|
||||
{
|
||||
public function boot(Dispatcher $events)
|
||||
{
|
||||
$events->subscribe('Flarum\Tags\Listeners\AddClientAssets');
|
||||
$events->subscribe('Flarum\Tags\Listeners\AddModelRelationship');
|
||||
$events->subscribe('Flarum\Tags\Listeners\ConfigureDiscussionPermissions');
|
||||
$events->subscribe('Flarum\Tags\Listeners\ConfigureTagPermissions');
|
||||
$events->subscribe('Flarum\Tags\Listeners\AddApiAttributes');
|
||||
$events->subscribe('Flarum\Tags\Listeners\PersistData');
|
||||
$events->subscribe('Flarum\Tags\Listeners\LogDiscussionTagged');
|
||||
$events->subscribe('Flarum\Tags\Listeners\UpdateTagMetadata');
|
||||
$events->subscribe('Flarum\Tags\Listeners\AddTagGambit');
|
||||
}
|
||||
}
|
49
extensions/tags/src/Gambits/TagGambit.php
Normal file
49
extensions/tags/src/Gambits/TagGambit.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php namespace Flarum\Tags\Gambits;
|
||||
|
||||
use Flarum\Tags\TagRepository;
|
||||
use Flarum\Core\Search\Search;
|
||||
use Flarum\Core\Search\RegexGambit;
|
||||
|
||||
class TagGambit extends RegexGambit
|
||||
{
|
||||
protected $pattern = 'tag:(.+)';
|
||||
|
||||
/**
|
||||
* @var \Flarum\Tags\TagRepository
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* @param \Flarum\Tags\TagRepository $tags
|
||||
*/
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
protected function conditions(Search $search, array $matches, $negate)
|
||||
{
|
||||
$slugs = explode(',', trim($matches[1], '"'));
|
||||
|
||||
// TODO: implement $negate
|
||||
$search->getQuery()->where(function ($query) use ($slugs) {
|
||||
foreach ($slugs as $slug) {
|
||||
if ($slug === 'untagged') {
|
||||
$query->orWhereNotExists(function ($query) {
|
||||
$query->select(app('flarum.db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereRaw('discussion_id = discussions.id');
|
||||
});
|
||||
} else {
|
||||
$id = $this->tags->getIdForSlug($slug);
|
||||
|
||||
$query->orWhereExists(function ($query) use ($id) {
|
||||
$query->select(app('flarum.db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereRaw('discussion_id = discussions.id AND tag_id = ?', [$id]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php namespace Flarum\Tags\Handlers;
|
||||
|
||||
use Flarum\Tags\DiscussionTaggedPost;
|
||||
use Flarum\Tags\Events\DiscussionWasTagged;
|
||||
use Flarum\Core\Notifications\NotificationSyncer;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class DiscussionTaggedNotifier
|
||||
{
|
||||
/**
|
||||
* Register the listeners for the subscriber.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
*/
|
||||
public function subscribe(Dispatcher $events)
|
||||
{
|
||||
$events->listen('Flarum\Tags\Events\DiscussionWasTagged', __CLASS__.'@whenDiscussionWasTagged');
|
||||
}
|
||||
|
||||
public function whenDiscussionWasTagged(DiscussionWasTagged $event)
|
||||
{
|
||||
$post = DiscussionTaggedPost::reply(
|
||||
$event->discussion->id,
|
||||
$event->user->id,
|
||||
array_pluck($event->oldTags, 'id'),
|
||||
$event->discussion->tags()->lists('id')
|
||||
);
|
||||
|
||||
$post = $event->discussion->addPost($post);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?php namespace Flarum\Tags\Handlers;
|
||||
|
||||
use Flarum\Api\Events\WillSerializeData;
|
||||
use Flarum\Api\Actions\Forum\ShowAction as ForumShowAction;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class TagLoader
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen('Flarum\Api\Events\WillSerializeData', __CLASS__.'@whenWillSerializeData');
|
||||
}
|
||||
|
||||
public function whenWillSerializeData(WillSerializeData $event)
|
||||
{
|
||||
if ($event->action instanceof ForumShowAction) {
|
||||
$forum = $event->data;
|
||||
|
||||
$query = Tag::whereVisibleTo($event->request->actor->getUser());
|
||||
|
||||
$forum->tags = $query->with('lastDiscussion')->get();
|
||||
$forum->tags_ids = $forum->tags->lists('id');
|
||||
}
|
||||
}
|
||||
}
|
72
extensions/tags/src/Listeners/AddApiAttributes.php
Executable file
72
extensions/tags/src/Listeners/AddApiAttributes.php
Executable file
@ -0,0 +1,72 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\ApiRelationship;
|
||||
use Flarum\Events\WillSerializeData;
|
||||
use Flarum\Events\BuildApiAction;
|
||||
use Flarum\Events\ApiAttributes;
|
||||
use Flarum\Api\Actions\Forum;
|
||||
use Flarum\Api\Actions\Discussions;
|
||||
use Flarum\Api\Serializers\ForumSerializer;
|
||||
use Flarum\Api\Serializers\DiscussionSerializer;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class AddApiAttributes
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen(ApiRelationship::class, __CLASS__.'@addTagsRelationship');
|
||||
$events->listen(WillSerializeData::class, __CLASS__.'@loadTagsRelationship');
|
||||
$events->listen(BuildApiAction::class, __CLASS__.'@includeTagsRelationship');
|
||||
$events->listen(ApiAttributes::class, __CLASS__.'@addAttributes');
|
||||
}
|
||||
|
||||
public function addTagsRelationship(ApiRelationship $event)
|
||||
{
|
||||
if ($event->serializer instanceof ForumSerializer &&
|
||||
$event->relationship === 'tags') {
|
||||
return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags');
|
||||
}
|
||||
|
||||
if ($event->serializer instanceof DiscussionSerializer &&
|
||||
$event->relationship === 'tags') {
|
||||
return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags');
|
||||
}
|
||||
}
|
||||
|
||||
public function loadTagsRelationship(WillSerializeData $event)
|
||||
{
|
||||
// Expose the complete tag list to clients by adding it as a
|
||||
// relationship to the /api/forum endpoint. Since the Forum model
|
||||
// doesn't actually have a tags relationship, we will manually load and
|
||||
// assign the tags data to it using an event listener.
|
||||
if ($event->action instanceof Forum\ShowAction) {
|
||||
$forum = $event->data;
|
||||
|
||||
$query = Tag::whereVisibleTo($event->request->actor);
|
||||
|
||||
$forum->tags = $query->with('lastDiscussion')->get();
|
||||
$forum->tags_ids = $forum->tags->lists('id');
|
||||
}
|
||||
}
|
||||
|
||||
public function includeTagsRelationship(BuildApiAction $event)
|
||||
{
|
||||
if ($event->action instanceof Forum\ShowAction) {
|
||||
$event->addInclude('tags');
|
||||
$event->addInclude('tags.lastDiscussion');
|
||||
$event->addLink('tags.parent');
|
||||
}
|
||||
|
||||
if ($event->action instanceof Discussions\IndexAction ||
|
||||
$event->action instanceof Discussions\ShowAction) {
|
||||
$event->addInclude('tags');
|
||||
}
|
||||
}
|
||||
|
||||
public function addAttributes(ApiAttributes $event)
|
||||
{
|
||||
if ($event->serializer instanceof DiscussionSerializer) {
|
||||
$event->attributes['canTag'] = $event->model->can($event->actor, 'tag');
|
||||
}
|
||||
}
|
||||
}
|
50
extensions/tags/src/Listeners/AddClientAssets.php
Executable file
50
extensions/tags/src/Listeners/AddClientAssets.php
Executable file
@ -0,0 +1,50 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\RegisterLocales;
|
||||
use Flarum\Events\BuildClientView;
|
||||
use Flarum\Events\RegisterForumRoutes;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class AddClientAssets
|
||||
{
|
||||
public function subscribe(Dispatcher $events)
|
||||
{
|
||||
$events->listen(RegisterLocales::class, __CLASS__.'@addLocale');
|
||||
$events->listen(BuildClientView::class, __CLASS__.'@addAssets');
|
||||
$events->listen(RegisterForumRoutes::class, __CLASS__.'@addRoutes');
|
||||
}
|
||||
|
||||
public function addLocale(RegisterLocales $event)
|
||||
{
|
||||
$event->addTranslations('en', __DIR__.'/../../locale/en.yml');
|
||||
}
|
||||
|
||||
public function addAssets(BuildClientView $event)
|
||||
{
|
||||
$event->forumAssets([
|
||||
__DIR__.'/../../js/forum/dist/extension.js',
|
||||
__DIR__.'/../../less/forum/extension.less'
|
||||
]);
|
||||
|
||||
$event->forumBootstrapper('tags/main');
|
||||
|
||||
$event->forumTranslations([
|
||||
'tags.discussion_tagged_post',
|
||||
'tags.added_tags',
|
||||
'tags.removed_tags',
|
||||
'tags.tag_new_discussion_title',
|
||||
'tags.edit_discussion_tags_title',
|
||||
'tags.edit_discussion_tags_link',
|
||||
'tags.discussion_tags_placeholder',
|
||||
'tags.confirm',
|
||||
'tags.more',
|
||||
'tags.tag_cloud_title'
|
||||
]);
|
||||
}
|
||||
|
||||
public function addRoutes(RegisterForumRoutes $event)
|
||||
{
|
||||
$event->get('/t/{slug}', 'tags.forum.tag');
|
||||
$event->get('/tags', 'tags.forum.tags');
|
||||
}
|
||||
}
|
21
extensions/tags/src/Listeners/AddModelRelationship.php
Executable file
21
extensions/tags/src/Listeners/AddModelRelationship.php
Executable file
@ -0,0 +1,21 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\ModelRelationship;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class AddModelRelationship
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen(ModelRelationship::class, __CLASS__.'@addTagsRelationship');
|
||||
}
|
||||
|
||||
public function addTagsRelationship(ModelRelationship $event)
|
||||
{
|
||||
if ($event->model instanceof Discussion &&
|
||||
$event->relationship === 'tags') {
|
||||
return $event->model->belongsToMany('Flarum\Tags\Tag', 'discussions_tags', null, null, 'tags');
|
||||
}
|
||||
}
|
||||
}
|
17
extensions/tags/src/Listeners/AddTagGambit.php
Executable file
17
extensions/tags/src/Listeners/AddTagGambit.php
Executable file
@ -0,0 +1,17 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\RegisterDiscussionGambits;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class AddTagGambit
|
||||
{
|
||||
public function subscribe(Dispatcher $events)
|
||||
{
|
||||
$events->listen(RegisterDiscussionGambits::class, __CLASS__.'@registerTagGambit');
|
||||
}
|
||||
|
||||
public function registerTagGambit(RegisterDiscussionGambits $event)
|
||||
{
|
||||
$event->gambits->add('Flarum\Tags\Gambits\TagGambit');
|
||||
}
|
||||
}
|
59
extensions/tags/src/Listeners/ConfigureDiscussionPermissions.php
Executable file
59
extensions/tags/src/Listeners/ConfigureDiscussionPermissions.php
Executable file
@ -0,0 +1,59 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\ScopeModelVisibility;
|
||||
use Flarum\Events\ModelAllow;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class ConfigureDiscussionPermissions
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen(ScopeModelVisibility::class, __CLASS__.'@scopeDiscussionVisibility');
|
||||
$events->listen(ModelAllow::class, __CLASS__.'@allowDiscussionPermissions');
|
||||
}
|
||||
|
||||
public function scopeDiscussionVisibility(ScopeModelVisibility $event)
|
||||
{
|
||||
// Hide discussions which have tags that the user is not allowed to see.
|
||||
if ($event->model instanceof Discussion) {
|
||||
$event->query->whereNotExists(function ($query) use ($event) {
|
||||
return $query->select(app('flarum.db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereIn('tag_id', Tag::getNotVisibleTo($event->actor))
|
||||
->whereRaw('discussion_id = discussions.id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function allowDiscussionPermissions(ModelAllow $event)
|
||||
{
|
||||
// Wrap all discussion permission checks with some logic pertaining to
|
||||
// the discussion's tags. If the discussion has a tag that has been
|
||||
// restricted, and the user has this permission for that tag, then they
|
||||
// are allowed. If the discussion only has tags that have been
|
||||
// restricted, then the user *must* have permission for at least one of
|
||||
// them.
|
||||
if ($event->model instanceof Discussion) {
|
||||
$tags = $event->model->tags;
|
||||
|
||||
if (count($tags)) {
|
||||
$restricted = true;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag->is_restricted) {
|
||||
if ($event->actor->hasPermission('tag' . $tag->id . '.discussion.' . $event->action)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$restricted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($restricted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
extensions/tags/src/Listeners/ConfigureTagPermissions.php
Executable file
31
extensions/tags/src/Listeners/ConfigureTagPermissions.php
Executable file
@ -0,0 +1,31 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\ScopeModelVisibility;
|
||||
use Flarum\Events\ModelAllow;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class ConfigureTagPermissions
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen(ScopeModelVisibility::class, __CLASS__.'@scopeTagVisibility');
|
||||
$events->listen(ModelAllow::class, __CLASS__.'@allowStartDiscussion');
|
||||
}
|
||||
|
||||
public function scopeTagVisibility(ScopeModelVisibility $event)
|
||||
{
|
||||
if ($event->model instanceof Tag) {
|
||||
$event->query->whereNotIn('id', Tag::getNotVisibleTo($event->actor));
|
||||
}
|
||||
}
|
||||
|
||||
public function allowStartDiscussion(ModelAllow $event)
|
||||
{
|
||||
if ($event->model instanceof Tag) {
|
||||
if (! $event->model->is_restricted ||
|
||||
$event->actor->hasPermission('tag' . $event->model->id . '.startDiscussion')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
extensions/tags/src/Listeners/LogDiscussionTagged.php
Executable file
32
extensions/tags/src/Listeners/LogDiscussionTagged.php
Executable file
@ -0,0 +1,32 @@
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Events\RegisterPostTypes;
|
||||
use Flarum\Tags\Posts\DiscussionTaggedPost;
|
||||
use Flarum\Tags\Events\DiscussionWasTagged;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class LogDiscussionTagged
|
||||
{
|
||||
public function subscribe(Dispatcher $events)
|
||||
{
|
||||
$events->listen(RegisterPostTypes::class, __CLASS__.'@registerPostType');
|
||||
$events->listen(DiscussionWasTagged::class, __CLASS__.'@whenDiscussionWasTagged');
|
||||
}
|
||||
|
||||
public function registerPostType(RegisterPostTypes $event)
|
||||
{
|
||||
$event->register(DiscussionTaggedPost::class);
|
||||
}
|
||||
|
||||
public function whenDiscussionWasTagged(DiscussionWasTagged $event)
|
||||
{
|
||||
$post = DiscussionTaggedPost::reply(
|
||||
$event->discussion->id,
|
||||
$event->user->id,
|
||||
array_pluck($event->oldTags, 'id'),
|
||||
$event->discussion->tags()->lists('id')
|
||||
);
|
||||
|
||||
$event->discussion->mergePost($post);
|
||||
}
|
||||
}
|
@ -1,24 +1,24 @@
|
||||
<?php namespace Flarum\Tags\Handlers;
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Tags\Events\DiscussionWasTagged;
|
||||
use Flarum\Core\Events\DiscussionWillBeSaved;
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Events\DiscussionWillBeSaved;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
|
||||
class TagSaver
|
||||
class PersistData
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen('Flarum\Core\Events\DiscussionWillBeSaved', __CLASS__.'@whenDiscussionWillBeSaved');
|
||||
$events->listen(DiscussionWillBeSaved::class, __CLASS__.'@whenDiscussionWillBeSaved');
|
||||
}
|
||||
|
||||
public function whenDiscussionWillBeSaved(DiscussionWillBeSaved $event)
|
||||
{
|
||||
if (isset($event->command->data['links']['tags']['linkage'])) {
|
||||
if (isset($event->data['relationships']['tags']['data'])) {
|
||||
$discussion = $event->discussion;
|
||||
$user = $event->command->user;
|
||||
$linkage = (array) $event->command->data['links']['tags']['linkage'];
|
||||
$actor = $event->actor;
|
||||
$linkage = (array) $event->data['relationships']['tags']['data'];
|
||||
|
||||
$newTagIds = [];
|
||||
foreach ($linkage as $link) {
|
||||
@ -27,7 +27,7 @@ class TagSaver
|
||||
|
||||
$newTags = Tag::whereIn('id', $newTagIds);
|
||||
foreach ($newTags as $tag) {
|
||||
if (! $tag->can($user, 'startDiscussion')) {
|
||||
if (! $tag->can($actor, 'startDiscussion')) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
}
|
||||
@ -41,18 +41,13 @@ class TagSaver
|
||||
if ($oldTagIds == $newTagIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->raise(new DiscussionWasTagged($discussion, $actor, $oldTags->all()));
|
||||
}
|
||||
|
||||
// @todo is there a better (safer) way to do this?
|
||||
// maybe store some info on the discussion model and then use the
|
||||
// DiscussionWasTagged event to actually save the data?
|
||||
Discussion::saved(function ($discussion) use ($newTagIds) {
|
||||
$discussion->tags()->sync($newTagIds);
|
||||
});
|
||||
|
||||
if ($discussion->exists) {
|
||||
$discussion->raise(new DiscussionWasTagged($discussion, $user, $oldTags->all()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +1,28 @@
|
||||
<?php namespace Flarum\Tags\Handlers;
|
||||
<?php namespace Flarum\Tags\Listeners;
|
||||
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Tags\Events\DiscussionWasTagged;
|
||||
use Flarum\Core\Events\DiscussionWasStarted;
|
||||
use Flarum\Core\Events\DiscussionWasDeleted;
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Core\Models\Post;
|
||||
use Flarum\Core\Events\PostWasPosted;
|
||||
use Flarum\Core\Events\PostWasDeleted;
|
||||
use Flarum\Core\Events\PostWasHidden;
|
||||
use Flarum\Core\Events\PostWasRestored;
|
||||
use Flarum\Events\DiscussionWasStarted;
|
||||
use Flarum\Events\DiscussionWasDeleted;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Core\Posts\Post;
|
||||
use Flarum\Events\PostWasPosted;
|
||||
use Flarum\Events\PostWasDeleted;
|
||||
use Flarum\Events\PostWasHidden;
|
||||
use Flarum\Events\PostWasRestored;
|
||||
|
||||
class TagMetadataUpdater
|
||||
class UpdateTagMetadata
|
||||
{
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen('Flarum\Core\Events\DiscussionWasStarted', __CLASS__.'@whenDiscussionWasStarted');
|
||||
$events->listen('Flarum\Tags\Events\DiscussionWasTagged', __CLASS__.'@whenDiscussionWasTagged');
|
||||
$events->listen('Flarum\Core\Events\DiscussionWasDeleted', __CLASS__.'@whenDiscussionWasDeleted');
|
||||
$events->listen(DiscussionWasStarted::class, __CLASS__.'@whenDiscussionWasStarted');
|
||||
$events->listen(DiscussionWasTagged::class, __CLASS__.'@whenDiscussionWasTagged');
|
||||
$events->listen(DiscussionWasDeleted::class, __CLASS__.'@whenDiscussionWasDeleted');
|
||||
|
||||
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
|
||||
$events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
|
||||
$events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
|
||||
$events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
|
||||
$events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
|
||||
$events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
|
||||
$events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
|
||||
$events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
|
||||
}
|
||||
|
||||
public function whenDiscussionWasStarted(DiscussionWasStarted $event)
|
||||
@ -69,7 +69,7 @@ class TagMetadataUpdater
|
||||
protected function updateTags($discussion, $delta = 0, $tags = null)
|
||||
{
|
||||
if (! $tags) {
|
||||
$tags = $discussion->getRelation('tags');
|
||||
$tags = $discussion->tags;
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
@ -1,37 +1,34 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
<?php namespace Flarum\Tags\Posts;
|
||||
|
||||
use Flarum\Core\Models\Model;
|
||||
use Flarum\Core\Models\EventPost;
|
||||
use Flarum\Core\Posts\Post;
|
||||
use Flarum\Core\Posts\EventPost;
|
||||
use Flarum\Core\Posts\MergeablePost;
|
||||
|
||||
class DiscussionTaggedPost extends EventPost
|
||||
class DiscussionTaggedPost extends EventPost implements MergeablePost
|
||||
{
|
||||
/**
|
||||
* The type of post this is, to be stored in the posts table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $type = 'discussionTagged';
|
||||
|
||||
/**
|
||||
* Merge the post into another post of the same type.
|
||||
*
|
||||
* @param \Flarum\Core\Models\Model $previous
|
||||
* @return \Flarum\Core\Models\Model|null The final model, or null if the
|
||||
* previous post was deleted.
|
||||
*/
|
||||
protected function mergeInto(Model $previous)
|
||||
public function saveAfter(Post $previous)
|
||||
{
|
||||
if ($this->user_id === $previous->user_id) {
|
||||
// If the previous post is another 'discussion tagged' post, and it's
|
||||
// by the same user, then we can merge this post into it. If we find
|
||||
// that we've in fact reverted the tag changes, delete it. Otherwise,
|
||||
// update its content.
|
||||
if ($previous instanceof static && $this->user_id === $previous->user_id) {
|
||||
if ($previous->content[0] == $this->content[1]) {
|
||||
return;
|
||||
}
|
||||
$previous->delete();
|
||||
} else {
|
||||
$previous->content = static::buildContent($previous->content[0], $this->content[1]);
|
||||
$previous->time = $this->time;
|
||||
|
||||
$previous->content = static::buildContent($previous->content[0], $this->content[1]);
|
||||
$previous->time = $this->time;
|
||||
$previous->save();
|
||||
}
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -1,15 +1,10 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Core\Models\Model;
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Core\Support\Locked;
|
||||
use Flarum\Core\Support\VisibleScope;
|
||||
use Flarum\Core\Model;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
use Locked;
|
||||
use VisibleScope;
|
||||
|
||||
protected $table = 'tags';
|
||||
|
||||
protected $dates = ['last_time'];
|
||||
@ -21,12 +16,12 @@ class Tag extends Model
|
||||
|
||||
public function lastDiscussion()
|
||||
{
|
||||
return $this->belongsTo('Flarum\Core\Models\Discussion', 'last_discussion_id');
|
||||
return $this->belongsTo('Flarum\Core\Discussions\Discussion', 'last_discussion_id');
|
||||
}
|
||||
|
||||
public function discussions()
|
||||
{
|
||||
return $this->belongsToMany('Flarum\Core\Models\Discussion', 'discussions_tags');
|
||||
return $this->belongsToMany('Flarum\Core\Discussions\Discussion', 'discussions_tags');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,7 +31,7 @@ class Tag extends Model
|
||||
*/
|
||||
public function refreshLastDiscussion()
|
||||
{
|
||||
if ($lastDiscussion = $this->discussions()->orderBy('last_time', 'desc')->first()) {
|
||||
if ($lastDiscussion = $this->discussions()->latest('last_time')->first()) {
|
||||
$this->setLastDiscussion($lastDiscussion);
|
||||
}
|
||||
|
||||
@ -46,7 +41,7 @@ class Tag extends Model
|
||||
/**
|
||||
* Set the tag's last discussion details.
|
||||
*
|
||||
* @param \Flarum\Core\Models\Discussion $discussion
|
||||
* @param Discussion $discussion
|
||||
* @return $this
|
||||
*/
|
||||
public function setLastDiscussion(Discussion $discussion)
|
||||
@ -60,13 +55,15 @@ class Tag extends Model
|
||||
public static function getNotVisibleTo($user)
|
||||
{
|
||||
static $tags;
|
||||
|
||||
if (! $tags) {
|
||||
$tags = static::all();
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag->is_restricted && ! $user->hasPermission('tag'.$tag->id.'.view')) {
|
||||
if ($tag->is_restricted && ! $user->hasPermission('tag' . $tag->id . '.view')) {
|
||||
$ids[] = $tag->id;
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
|
||||
use Flarum\Core\Search\SearcherInterface;
|
||||
use Flarum\Core\Search\GambitAbstract;
|
||||
|
||||
class TagGambit extends GambitAbstract
|
||||
{
|
||||
/**
|
||||
* The gambit's regex pattern.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $pattern = 'tag:(.+)';
|
||||
|
||||
/**
|
||||
* @var \Flarum\Tags\TagRepositoryInterface
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* Instantiate the gambit.
|
||||
*
|
||||
* @param \Flarum\Tags\TagRepositoryInterface $categories
|
||||
*/
|
||||
public function __construct(TagRepositoryInterface $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply conditions to the searcher, given matches from the gambit's
|
||||
* regex.
|
||||
*
|
||||
* @param array $matches The matches from the gambit's regex.
|
||||
* @param \Flarum\Core\Search\SearcherInterface $searcher
|
||||
* @return void
|
||||
*/
|
||||
protected function conditions(SearcherInterface $searcher, array $matches, $negate)
|
||||
{
|
||||
$slugs = explode(',', trim($matches[1], '"'));
|
||||
|
||||
// TODO: implement $negate
|
||||
$searcher->getQuery()->where(function ($query) use ($slugs) {
|
||||
foreach ($slugs as $slug) {
|
||||
if ($slug === 'untagged') {
|
||||
$query->orWhereNotExists(function ($query) {
|
||||
$query->select(app('db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereRaw('discussion_id = discussions.id');
|
||||
});
|
||||
} else {
|
||||
$id = $this->tags->getIdForSlug($slug);
|
||||
|
||||
$query->orWhereExists(function ($query) use ($id) {
|
||||
$query->select(app('db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereRaw('discussion_id = discussions.id AND tag_id = ?', [$id]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -4,47 +4,47 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Flarum\Core\Models\User;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class EloquentTagRepository implements TagRepositoryInterface
|
||||
class TagRepository
|
||||
{
|
||||
/**
|
||||
* Find all tags, optionally making sure they are visible to a
|
||||
* certain user.
|
||||
*
|
||||
* @param \Flarum\Core\Models\User|null $user
|
||||
* @param User|null $user
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function find(User $user = null)
|
||||
{
|
||||
$query = Tag::newQuery();
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->get();
|
||||
return $this->scopeVisibleTo($query, $user)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of a tag with the given slug.
|
||||
*
|
||||
* @param string $slug
|
||||
* @param \Flarum\Core\Models\User|null $user
|
||||
* @param User|null $user
|
||||
* @return integer
|
||||
*/
|
||||
public function getIdForSlug($slug, User $user = null)
|
||||
{
|
||||
$query = Tag::where('slug', 'like', $slug);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->pluck('id');
|
||||
return $this->scopeVisibleTo($query, $user)->pluck('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include records that are visible to a user.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
* @param Builder $query
|
||||
* @param User $user
|
||||
* @return Builder
|
||||
*/
|
||||
protected function scopeVisibleForUser(Builder $query, User $user = null)
|
||||
protected function scopeVisibleTo(Builder $query, User $user = null)
|
||||
{
|
||||
if ($user !== null) {
|
||||
$query->whereCan($user, 'view');
|
||||
$query->whereVisibleTo($user);
|
||||
}
|
||||
|
||||
return $query;
|
@ -1,24 +0,0 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Core\Models\User;
|
||||
|
||||
interface TagRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Find all tags, optionally making sure they are visible to a
|
||||
* certain user.
|
||||
*
|
||||
* @param \Flarum\Core\Models\User|null $user
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function find(User $user = null);
|
||||
|
||||
/**
|
||||
* Get the ID of a tag with the given slug.
|
||||
*
|
||||
* @param string $slug
|
||||
* @param \Flarum\Core\Models\User|null $user
|
||||
* @return integer
|
||||
*/
|
||||
public function getIdForSlug($slug, User $user = null);
|
||||
}
|
@ -1,43 +1,28 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Api\Serializers\BaseSerializer;
|
||||
use Flarum\Api\Serializers\Serializer;
|
||||
|
||||
class TagSerializer extends BaseSerializer
|
||||
class TagSerializer extends Serializer
|
||||
{
|
||||
/**
|
||||
* The resource type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'tags';
|
||||
|
||||
/**
|
||||
* Serialize tag attributes to be exposed in the API.
|
||||
*
|
||||
* @param \Flarum\Tags\Tag $tag
|
||||
* @return array
|
||||
*/
|
||||
protected function attributes($tag)
|
||||
protected function getDefaultAttributes($tag)
|
||||
{
|
||||
$user = $this->actor->getUser();
|
||||
|
||||
$attributes = [
|
||||
'name' => $tag->name,
|
||||
'description' => $tag->description,
|
||||
'slug' => $tag->slug,
|
||||
'color' => $tag->color,
|
||||
'backgroundUrl' => $tag->background_path,
|
||||
'backgroundMode' => $tag->background_mode,
|
||||
'iconUrl' => $tag->icon_path,
|
||||
'discussionsCount' => (int) $tag->discussions_count,
|
||||
'position' => $tag->position === null ? null : (int) $tag->position,
|
||||
'defaultSort' => $tag->default_sort,
|
||||
'isChild' => (bool) $tag->parent_id,
|
||||
'lastTime' => $tag->last_time ? $tag->last_time->toRFC3339String() : null,
|
||||
'canStartDiscussion' => $tag->can($user, 'startDiscussion')
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'description' => $tag->description,
|
||||
'slug' => $tag->slug,
|
||||
'color' => $tag->color,
|
||||
'backgroundUrl' => $tag->background_path,
|
||||
'backgroundMode' => $tag->background_mode,
|
||||
'iconUrl' => $tag->icon_path,
|
||||
'discussionsCount' => (int) $tag->discussions_count,
|
||||
'position' => $tag->position === null ? null : (int) $tag->position,
|
||||
'defaultSort' => $tag->default_sort,
|
||||
'isChild' => (bool) $tag->parent_id,
|
||||
'lastTime' => $tag->last_time ? $tag->last_time->toRFC3339String() : null,
|
||||
'canStartDiscussion' => $tag->can($this->actor, 'startDiscussion')
|
||||
];
|
||||
|
||||
return $this->extendAttributes($tag, $attributes);
|
||||
}
|
||||
|
||||
protected function parent()
|
||||
|
@ -1,138 +0,0 @@
|
||||
<?php namespace Flarum\Tags;
|
||||
|
||||
use Flarum\Support\ServiceProvider;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Core\Models\User;
|
||||
|
||||
class TagsServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind(
|
||||
'Flarum\Tags\TagRepositoryInterface',
|
||||
'Flarum\Tags\EloquentTagRepository'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap the application events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->extend([
|
||||
(new Extend\ForumClient())
|
||||
->assets([
|
||||
__DIR__.'/../js/dist/extension.js',
|
||||
__DIR__.'/../less/extension.less'
|
||||
])
|
||||
->route('get', '/t/{slug}', 'flarum-tags.forum.tag')
|
||||
->route('get', '/tags', 'flarum-tags.forum.tags'),
|
||||
|
||||
(new Extend\Model('Flarum\Tags\Tag'))
|
||||
// Hide tags that the user doesn't have permission to see.
|
||||
->scopeVisible(function ($query, User $user) {
|
||||
$query->whereNotIn('id', Tag::getNotVisibleTo($user));
|
||||
})
|
||||
|
||||
// Allow the user to start discussions in tags which aren't
|
||||
// restricted, or for which the user has explicitly been granted
|
||||
// permission.
|
||||
->allow('startDiscussion', function (Tag $tag, User $user) {
|
||||
if (! $tag->is_restricted || $user->hasPermission('tag'.$tag->id.'.startDiscussion')) {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
|
||||
// Expose the complete tag list to clients by adding it as a
|
||||
// relationship to the /api/forum endpoint. Since the Forum model
|
||||
// doesn't actually have a tags relationship, we will manually
|
||||
// load and assign the tags data to it using an event listener.
|
||||
(new Extend\ApiSerializer('Flarum\Api\Serializers\ForumSerializer'))
|
||||
->hasMany('tags', 'Flarum\Tags\TagSerializer'),
|
||||
|
||||
(new Extend\ApiAction('Flarum\Api\Actions\Forum\ShowAction'))
|
||||
->addInclude('tags')
|
||||
->addInclude('tags.lastDiscussion')
|
||||
->addLink('tags.parent'),
|
||||
|
||||
new Extend\EventSubscriber('Flarum\Tags\Handlers\TagLoader'),
|
||||
|
||||
// Extend the Discussion model and API: add the tags relationship
|
||||
// and modify permissions.
|
||||
(new Extend\Model('Flarum\Core\Models\Discussion'))
|
||||
->belongsToMany('tags', 'Flarum\Tags\Tag', 'discussions_tags')
|
||||
|
||||
// Hide discussions which have tags that the user is not allowed
|
||||
// to see.
|
||||
->scopeVisible(function ($query, User $user) {
|
||||
$query->whereNotExists(function ($query) use ($user) {
|
||||
return $query->select(app('db')->raw(1))
|
||||
->from('discussions_tags')
|
||||
->whereIn('tag_id', Tag::getNotVisibleTo($user))
|
||||
->whereRaw('discussion_id = discussions.id');
|
||||
});
|
||||
})
|
||||
|
||||
// Wrap all discussion permission checks with some logic
|
||||
// pertaining to the discussion's tags. If the discussion has a
|
||||
// tag that has been restricted, and the user has this
|
||||
// permission for that tag, then they are allowed. If the
|
||||
// discussion only has tags that have been restricted, then the
|
||||
// user *must* have permission for at least one of them.
|
||||
->allow('*', function (Discussion $discussion, User $user, $action) {
|
||||
$tags = $discussion->getRelation('tags');
|
||||
|
||||
if (count($tags)) {
|
||||
$restricted = true;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag->is_restricted) {
|
||||
if ($user->hasPermission('tag'.$tag->id.'.discussion.'.$action)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$restricted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($restricted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
(new Extend\ApiSerializer('Flarum\Api\Serializers\DiscussionBasicSerializer'))
|
||||
->hasMany('tags', 'Flarum\Tags\TagSerializer')
|
||||
->attributes(function (&$attributes, $discussion, $user) {
|
||||
$attributes['canTag'] = $discussion->can($user, 'tag');
|
||||
}),
|
||||
|
||||
(new Extend\ApiAction([
|
||||
'Flarum\Api\Actions\Discussions\IndexAction',
|
||||
'Flarum\Api\Actions\Discussions\ShowAction'
|
||||
]))
|
||||
->addInclude('tags'),
|
||||
|
||||
// Add an event subscriber so that tags data is persisted when
|
||||
// saving a discussion.
|
||||
new Extend\EventSubscriber('Flarum\Tags\Handlers\TagSaver'),
|
||||
new Extend\EventSubscriber('Flarum\Tags\Handlers\TagMetadataUpdater'),
|
||||
|
||||
// Add a gambit that allows filtering discussions by tag(s).
|
||||
new Extend\DiscussionGambit('Flarum\Tags\TagGambit'),
|
||||
|
||||
// Add a new post type which indicates when a discussion's tags were
|
||||
// changed.
|
||||
new Extend\PostType('Flarum\Tags\DiscussionTaggedPost'),
|
||||
new Extend\EventSubscriber('Flarum\Tags\Handlers\DiscussionTaggedNotifier')
|
||||
]);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user