mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
Initial commit
This commit is contained in:
4
extensions/tags/.gitignore
vendored
Normal file
4
extensions/tags/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/vendor
|
||||||
|
composer.phar
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
5
extensions/tags/bootstrap.php
Normal file
5
extensions/tags/bootstrap.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
|
||||||
|
$this->app->register('Flarum\Categories\CategoriesServiceProvider');
|
11
extensions/tags/composer.json
Normal file
11
extensions/tags/composer.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.4.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Flarum\\Categories\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev"
|
||||||
|
}
|
15
extensions/tags/extension.json
Normal file
15
extensions/tags/extension.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "categories",
|
||||||
|
"description": "Organise discussions into a heirarchy of categories.",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Toby Zerner",
|
||||||
|
"email": "toby@flarum.org",
|
||||||
|
"website": "http://tobyzerner.com"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.4.0",
|
||||||
|
"flarum": ">1.0.0"
|
||||||
|
}
|
||||||
|
}
|
3
extensions/tags/js/.gitignore
vendored
Normal file
3
extensions/tags/js/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
bower_components
|
||||||
|
node_modules
|
||||||
|
dist
|
45
extensions/tags/js/Gulpfile.js
Normal file
45
extensions/tags/js/Gulpfile.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
var gulp = require('gulp');
|
||||||
|
var livereload = require('gulp-livereload');
|
||||||
|
var concat = require('gulp-concat');
|
||||||
|
var argv = require('yargs').argv;
|
||||||
|
var uglify = require('gulp-uglify');
|
||||||
|
var gulpif = require('gulp-if');
|
||||||
|
var babel = require('gulp-babel');
|
||||||
|
var cached = require('gulp-cached');
|
||||||
|
var remember = require('gulp-remember');
|
||||||
|
var merge = require('merge-stream');
|
||||||
|
var streamqueue = require('streamqueue');
|
||||||
|
|
||||||
|
var staticFiles = [
|
||||||
|
'bootstrap.js'
|
||||||
|
];
|
||||||
|
var moduleFiles = [
|
||||||
|
'src/**/*.js'
|
||||||
|
];
|
||||||
|
var modulePrefix = 'categories';
|
||||||
|
|
||||||
|
gulp.task('default', function() {
|
||||||
|
return streamqueue({objectMode: true},
|
||||||
|
gulp.src(moduleFiles)
|
||||||
|
.pipe(cached('scripts'))
|
||||||
|
.pipe(babel({ modules: 'amd', moduleIds: true, moduleRoot: modulePrefix }))
|
||||||
|
.pipe(remember('scripts')),
|
||||||
|
gulp.src(staticFiles)
|
||||||
|
.pipe(babel())
|
||||||
|
)
|
||||||
|
.pipe(concat('extension.js'))
|
||||||
|
.pipe(gulpif(argv.production, uglify()))
|
||||||
|
.pipe(gulp.dest('dist'))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('watch', ['default'], function () {
|
||||||
|
livereload.listen();
|
||||||
|
var watcher = gulp.watch(moduleFiles.concat(staticFiles), ['default']);
|
||||||
|
watcher.on('change', function (event) {
|
||||||
|
if (event.type === 'deleted') {
|
||||||
|
delete cached.caches.scripts[event.path];
|
||||||
|
remember.forget('scripts', event.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
274
extensions/tags/js/bootstrap.js
vendored
Normal file
274
extensions/tags/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { extend, override } from 'flarum/extension-utils';
|
||||||
|
import Model from 'flarum/model';
|
||||||
|
import Component from 'flarum/component';
|
||||||
|
import Discussion from 'flarum/models/discussion';
|
||||||
|
import IndexPage from 'flarum/components/index-page';
|
||||||
|
import DiscussionPage from 'flarum/components/discussion-page';
|
||||||
|
import DiscussionList from 'flarum/components/discussion-list';
|
||||||
|
import DiscussionHero from 'flarum/components/discussion-hero';
|
||||||
|
import Separator from 'flarum/components/separator';
|
||||||
|
import NavItem from 'flarum/components/nav-item';
|
||||||
|
import ActionButton from 'flarum/components/action-button';
|
||||||
|
import ComposerDiscussion from 'flarum/components/composer-discussion';
|
||||||
|
import ActivityPost from 'flarum/components/activity-post';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
|
import CategoriesPage from 'categories/components/categories-page';
|
||||||
|
import Category from 'categories/category';
|
||||||
|
import PostDiscussionMoved from 'categories/components/post-discussion-moved';
|
||||||
|
|
||||||
|
import app from 'flarum/app';
|
||||||
|
|
||||||
|
Discussion.prototype.category = Model.one('category');
|
||||||
|
|
||||||
|
app.initializers.add('categories', function() {
|
||||||
|
app.routes['categories'] = ['/categories', CategoriesPage.component()];
|
||||||
|
|
||||||
|
app.routes['category'] = ['/c/:categories', IndexPage.component({category: true})];
|
||||||
|
|
||||||
|
|
||||||
|
// @todo support combination with filters
|
||||||
|
// app.routes['category.filter'] = ['/c/:slug/:filter', IndexPage.component({category: true})];
|
||||||
|
|
||||||
|
app.postComponentRegistry['discussionMoved'] = PostDiscussionMoved;
|
||||||
|
app.store.model('categories', Category);
|
||||||
|
|
||||||
|
extend(DiscussionList.prototype, 'infoItems', function(items, discussion) {
|
||||||
|
var category = discussion.category();
|
||||||
|
if (category && category.slug() !== this.props.params.categories) {
|
||||||
|
items.add('category', m('span.category', {style: 'color: '+category.color()}, category.title()), {first: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
class CategoryNavItem extends NavItem {
|
||||||
|
view() {
|
||||||
|
var category = this.props.category;
|
||||||
|
var active = this.constructor.active(this.props);
|
||||||
|
return m('li'+(active ? '.active' : ''), m('a', {href: this.props.href, config: m.route, onclick: () => {app.cache.discussionList = null; m.redraw.strategy('none')}, style: active ? 'color: '+category.color() : ''}, [
|
||||||
|
m('span.icon.category-icon', {style: 'background-color: '+category.color()}),
|
||||||
|
category.title()
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static props(props) {
|
||||||
|
var category = props.category;
|
||||||
|
props.params.categories = category.slug();
|
||||||
|
props.href = app.route('category', props.params);
|
||||||
|
props.label = category.title();
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(IndexPage.prototype, 'navItems', function(items) {
|
||||||
|
items.add('categories', NavItem.component({
|
||||||
|
icon: 'reorder',
|
||||||
|
label: 'Categories',
|
||||||
|
href: app.route('categories'),
|
||||||
|
config: m.route
|
||||||
|
}), {last: true});
|
||||||
|
|
||||||
|
items.add('separator', Separator.component(), {last: true});
|
||||||
|
|
||||||
|
app.store.all('categories').forEach(category => {
|
||||||
|
items.add('category'+category.id(), CategoryNavItem.component({category, params: this.stickyParams()}), {last: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(IndexPage.prototype, 'params', function(params) {
|
||||||
|
params.categories = this.props.category ? m.route.param('categories') : undefined;
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
|
||||||
|
class CategoryHero extends Component {
|
||||||
|
view() {
|
||||||
|
var category = this.props.category;
|
||||||
|
|
||||||
|
return m('header.hero.category-hero', {style: 'background-color: '+category.color()}, [
|
||||||
|
m('div.container', [
|
||||||
|
m('div.container-narrow', [
|
||||||
|
m('h2', category.title()),
|
||||||
|
m('div.subtitle', category.description())
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(IndexPage.prototype, 'view', function(view) {
|
||||||
|
if (this.props.category) {
|
||||||
|
var slug = this.params().categories;
|
||||||
|
var category = app.store.all('categories').filter(category => category.slug() == slug)[0];
|
||||||
|
view.children[0] = CategoryHero.component({category});
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(IndexPage.prototype, 'sidebarItems', function(items) {
|
||||||
|
var slug = this.params().categories;
|
||||||
|
var category = app.store.all('categories').filter(category => category.slug() == slug)[0];
|
||||||
|
if (category) {
|
||||||
|
items.newDiscussion.content.props.style = 'background-color: '+category.color();
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(DiscussionList.prototype, 'params', function(params) {
|
||||||
|
if (params.categories) {
|
||||||
|
params.q = (params.q || '')+' category:'+params.categories;
|
||||||
|
delete params.categories;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(DiscussionPage.prototype, 'params', function(params) {
|
||||||
|
params.include += ',category';
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(DiscussionHero.prototype, 'view', function(view) {
|
||||||
|
var category = this.props.discussion.category();
|
||||||
|
if (category) {
|
||||||
|
view.attrs.style = 'background-color: '+category.color();
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(DiscussionHero.prototype, 'items', function(items) {
|
||||||
|
var category = this.props.discussion.category();
|
||||||
|
if (category) {
|
||||||
|
items.add('category', m('span.category', category.title()), {before: 'title'});
|
||||||
|
items.title.content.wrapperClass = 'block-item';
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
class MoveDiscussionModal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.categories = m.prop(app.store.all('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
var discussion = this.props.discussion;
|
||||||
|
|
||||||
|
return m('div.modal-dialog.modal-move-discussion', [
|
||||||
|
m('div.modal-content', [
|
||||||
|
m('button.btn.btn-icon.btn-link.close.back-control', {onclick: app.modal.close.bind(app.modal)}, icon('times')),
|
||||||
|
m('div.modal-header', m('h3.title-control', discussion
|
||||||
|
? ['Move ', m('em', discussion.title()), ' from ', m('span.category', {style: 'color: '+discussion.category().color()}, discussion.category().title()), ' to...']
|
||||||
|
: ['Start a Discussion In...'])),
|
||||||
|
m('div', [
|
||||||
|
m('ul.category-list', [
|
||||||
|
this.categories().map(category =>
|
||||||
|
(discussion && category.id() === discussion.category().id()) ? '' : m('li.category-tile', {style: 'background-color: '+category.color()}, [
|
||||||
|
m('a[href=javascript:;]', {onclick: this.save.bind(this, category)}, [
|
||||||
|
m('h3.title', category.title()),
|
||||||
|
m('p.description', category.description()),
|
||||||
|
m('span.count', category.discussionsCount()+' discussions'),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
save(category) {
|
||||||
|
var discussion = this.props.discussion;
|
||||||
|
|
||||||
|
if (discussion) {
|
||||||
|
discussion.save({links: {category}}).then(discussion => {
|
||||||
|
if (app.current instanceof DiscussionPage) {
|
||||||
|
app.current.stream().sync();
|
||||||
|
}
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onchange && this.props.onchange(category);
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
m.redraw.strategy('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function move() {
|
||||||
|
app.modal.show(new MoveDiscussionModal({discussion: this}));
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(Discussion.prototype, 'controls', function(items) {
|
||||||
|
if (this.canEdit()) {
|
||||||
|
items.add('move', ActionButton.component({
|
||||||
|
label: 'Move',
|
||||||
|
icon: 'arrow-right',
|
||||||
|
onclick: move.bind(this)
|
||||||
|
}), {after: 'rename'});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
override(IndexPage.prototype, 'newDiscussion', function(parent) {
|
||||||
|
var categories = app.store.all('categories');
|
||||||
|
var slug = this.params().categories;
|
||||||
|
var category;
|
||||||
|
if (slug || !app.session.user()) {
|
||||||
|
parent();
|
||||||
|
if (app.composer.component) {
|
||||||
|
category = categories.filter(category => category.slug() == slug)[0];
|
||||||
|
app.composer.component.category(category);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var modal = new MoveDiscussionModal({onchange: category => {
|
||||||
|
parent();
|
||||||
|
app.composer.component.category(category);
|
||||||
|
}});
|
||||||
|
app.modal.show(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ComposerDiscussion.prototype.chooseCategory = function() {
|
||||||
|
var modal = new MoveDiscussionModal({onchange: category => {
|
||||||
|
this.category(category);
|
||||||
|
this.$('textarea').focus();
|
||||||
|
}});
|
||||||
|
app.modal.show(modal);
|
||||||
|
};
|
||||||
|
|
||||||
|
ComposerDiscussion.prototype.category = m.prop();
|
||||||
|
extend(ComposerDiscussion.prototype, 'headerItems', function(items) {
|
||||||
|
var category = this.category();
|
||||||
|
|
||||||
|
items.add('category', m('a[href=javascript:;][tabindex=-1].btn.btn-link.control-change-category', {
|
||||||
|
onclick: this.chooseCategory.bind(this)
|
||||||
|
}, [
|
||||||
|
category ? m('span.category-icon', {style: 'background-color: '+category.color()}) : '', ' ',
|
||||||
|
m('span.label', category ? category.title() : 'Uncategorized'),
|
||||||
|
icon('sort')
|
||||||
|
]));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
extend(ComposerDiscussion.prototype, 'data', function(data) {
|
||||||
|
data.links = data.links || {};
|
||||||
|
data.links.category = this.category();
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
|
||||||
|
extend(ActivityPost.prototype, 'headerItems', function(items) {
|
||||||
|
var category = this.props.activity.post().discussion().category();
|
||||||
|
if (category) {
|
||||||
|
items.add('category', m('span.category', {style: {color: category.color()}}, category.title()));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
})
|
||||||
|
});
|
18
extensions/tags/js/package.json
Normal file
18
extensions/tags/js/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "flarum-sticky",
|
||||||
|
"devDependencies": {
|
||||||
|
"gulp": "^3.8.11",
|
||||||
|
"gulp-babel": "^5.1.0",
|
||||||
|
"gulp-cached": "^1.0.4",
|
||||||
|
"gulp-concat": "^2.5.2",
|
||||||
|
"gulp-if": "^1.2.5",
|
||||||
|
"gulp-livereload": "^3.8.0",
|
||||||
|
"gulp-remember": "^0.3.0",
|
||||||
|
"gulp-uglify": "^1.2.0",
|
||||||
|
"merge-stream": "^0.1.7",
|
||||||
|
"yargs": "^3.7.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"streamqueue": "^0.1.3"
|
||||||
|
}
|
||||||
|
}
|
12
extensions/tags/js/src/category.js
Normal file
12
extensions/tags/js/src/category.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Model from 'flarum/model';
|
||||||
|
|
||||||
|
class Category extends Model {}
|
||||||
|
|
||||||
|
Category.prototype.id = Model.prop('id');
|
||||||
|
Category.prototype.title = Model.prop('title');
|
||||||
|
Category.prototype.slug = Model.prop('slug');
|
||||||
|
Category.prototype.description = Model.prop('description');
|
||||||
|
Category.prototype.color = Model.prop('color');
|
||||||
|
Category.prototype.discussionsCount = Model.prop('discussionsCount');
|
||||||
|
|
||||||
|
export default Category;
|
38
extensions/tags/js/src/components/categories-page.js
Normal file
38
extensions/tags/js/src/components/categories-page.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import Component from 'flarum/component';
|
||||||
|
import WelcomeHero from 'flarum/components/welcome-hero';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
|
export default class CategoriesPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.categories = m.prop(app.store.all('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return m('div.categories-area', [
|
||||||
|
m('div.title-control.categories-forum-title', app.config.forum_title),
|
||||||
|
WelcomeHero.component(),
|
||||||
|
m('div.container', [
|
||||||
|
m('ul.category-list.category-list-tiles', [
|
||||||
|
m('li.filter-tile', [
|
||||||
|
m('a', {href: app.route('index'), config: m.route}, 'All Discussions'),
|
||||||
|
// m('ul.filter-list', [
|
||||||
|
// m('li', m('a', {href: app.route('index'), config: m.route}, m('span', [icon('star'), ' Following']))),
|
||||||
|
// m('li', m('a', {href: app.route('index'), config: m.route}, m('span', [icon('envelope-o'), ' Inbox'])))
|
||||||
|
// ])
|
||||||
|
]),
|
||||||
|
this.categories().map(category =>
|
||||||
|
m('li.category-tile', {style: 'background-color: '+category.color()}, [
|
||||||
|
m('a', {href: app.route('category', {categories: category.slug()}), config: m.route}, [
|
||||||
|
m('h3.title', category.title()),
|
||||||
|
m('p.description', category.description()),
|
||||||
|
m('span.count', category.discussionsCount()+' discussions'),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
59
extensions/tags/js/src/components/post-discussion-moved.js
Normal file
59
extensions/tags/js/src/components/post-discussion-moved.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Component from 'flarum/component';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
import username from 'flarum/helpers/username';
|
||||||
|
import humanTime from 'flarum/utils/human-time';
|
||||||
|
import SubtreeRetainer from 'flarum/utils/subtree-retainer';
|
||||||
|
import ItemList from 'flarum/utils/item-list';
|
||||||
|
import ActionButton from 'flarum/components/action-button';
|
||||||
|
import DropdownButton from 'flarum/components/dropdown-button';
|
||||||
|
|
||||||
|
export default class PostDiscussionMoved extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.subtree = new SubtreeRetainer(
|
||||||
|
() => this.props.post.freshness,
|
||||||
|
() => this.props.post.user().freshness
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view(ctrl) {
|
||||||
|
var controls = this.controlItems().toArray();
|
||||||
|
|
||||||
|
var post = this.props.post;
|
||||||
|
var oldCategory = app.store.getById('categories', post.content()[0]);
|
||||||
|
var newCategory = app.store.getById('categories', post.content()[1]);
|
||||||
|
|
||||||
|
return m('article.post.post-activity.post-discussion-moved', this.subtree.retain() || m('div', [
|
||||||
|
controls.length ? DropdownButton.component({
|
||||||
|
items: controls,
|
||||||
|
className: 'contextual-controls',
|
||||||
|
buttonClass: 'btn btn-default btn-icon btn-sm btn-naked',
|
||||||
|
menuClass: 'pull-right'
|
||||||
|
}) : '',
|
||||||
|
icon('arrow-right post-icon'),
|
||||||
|
m('div.post-activity-info', [
|
||||||
|
m('a.post-user', {href: app.route('user', {username: post.user().username()}), config: m.route}, username(post.user())),
|
||||||
|
' moved the discussion from ', m('span.category', {style: {color: oldCategory.color()}}, oldCategory.title()), ' to ', m('span.category', {style: {color: newCategory.color()}}, newCategory.title()), '.'
|
||||||
|
]),
|
||||||
|
m('div.post-activity-time', humanTime(post.time()))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
controlItems() {
|
||||||
|
var items = new ItemList();
|
||||||
|
var post = this.props.post;
|
||||||
|
|
||||||
|
if (post.canDelete()) {
|
||||||
|
items.add('delete', ActionButton.component({ icon: 'times', label: 'Delete', onclick: this.delete.bind(this) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
var post = this.props.post;
|
||||||
|
post.delete();
|
||||||
|
this.props.ondelete && this.props.ondelete(post);
|
||||||
|
}
|
||||||
|
}
|
206
extensions/tags/less/categories.less
Normal file
206
extensions/tags/less/categories.less
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
.category {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 80%;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.discussion-summary & {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-hero & {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-discussion-moved & {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& .modal-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
& .category-tile .title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
& .category-tile .description {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
& .count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
& .category-tile > a {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddCategoryToDiscussions extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('discussions', function (Blueprint $table) {
|
||||||
|
$table->integer('category_id')->unsigned();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('discussions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('category_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateCategoriesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('categories', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('description');
|
||||||
|
$table->string('color');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::drop('categories');
|
||||||
|
}
|
||||||
|
}
|
36
extensions/tags/src/CategoriesServiceProvider.php
Normal file
36
extensions/tags/src/CategoriesServiceProvider.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php namespace Flarum\Categories;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Flarum\Api\Actions\Discussions\IndexAction;
|
||||||
|
use Flarum\Api\Actions\Discussions\ShowAction;
|
||||||
|
use Flarum\Core\Models\Post;
|
||||||
|
use Flarum\Core\Models\Discussion;
|
||||||
|
|
||||||
|
class CategoriesServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap the application events.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->subscribe('Flarum\Categories\Handlers\Handler');
|
||||||
|
|
||||||
|
IndexAction::$include['category'] = true;
|
||||||
|
|
||||||
|
ShowAction::$include['category'] = true;
|
||||||
|
|
||||||
|
Post::addType('discussionMoved', 'Flarum\Categories\DiscussionMovedPost');
|
||||||
|
|
||||||
|
Discussion::addRelationship('category', function ($model) {
|
||||||
|
return $model->belongsTo('Flarum\Categories\Category', null, null, 'category');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
8
extensions/tags/src/Category.php
Normal file
8
extensions/tags/src/Category.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php namespace Flarum\Categories;
|
||||||
|
|
||||||
|
use Flarum\Core\Models\Model;
|
||||||
|
|
||||||
|
class Category extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'categories';
|
||||||
|
}
|
25
extensions/tags/src/CategoryGambit.php
Normal file
25
extensions/tags/src/CategoryGambit.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php namespace Flarum\Categories;
|
||||||
|
|
||||||
|
use Flarum\Core\Repositories\UserRepositoryInterface as UserRepository;
|
||||||
|
use Flarum\Core\Search\SearcherInterface;
|
||||||
|
use Flarum\Core\Search\GambitAbstract;
|
||||||
|
|
||||||
|
class CategoryGambit extends GambitAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The gambit's regex pattern.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pattern = 'category:(.+)';
|
||||||
|
|
||||||
|
public function conditions($matches, SearcherInterface $searcher)
|
||||||
|
{
|
||||||
|
$slug = trim($matches[1], '"');
|
||||||
|
|
||||||
|
// @todo implement categories repository
|
||||||
|
// $id = $this->categories->getIdForSlug($slug);
|
||||||
|
$id = Category::whereSlug($slug)->pluck('id');
|
||||||
|
|
||||||
|
$searcher->query()->where('category_id', $id);
|
||||||
|
}
|
||||||
|
}
|
26
extensions/tags/src/CategorySerializer.php
Normal file
26
extensions/tags/src/CategorySerializer.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php namespace Flarum\Categories;
|
||||||
|
|
||||||
|
use Flarum\Api\Serializers\BaseSerializer;
|
||||||
|
|
||||||
|
class CategorySerializer extends BaseSerializer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The resource type.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $type = 'categories';
|
||||||
|
|
||||||
|
protected function attributes($category)
|
||||||
|
{
|
||||||
|
$attributes = [
|
||||||
|
'title' => $category->title,
|
||||||
|
'description' => $category->description,
|
||||||
|
'slug' => $category->slug,
|
||||||
|
'color' => $category->color,
|
||||||
|
'discussionsCount' => (int) $category->discussions_count
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->extendAttributes($category, $attributes);
|
||||||
|
}
|
||||||
|
}
|
49
extensions/tags/src/DiscussionMovedPost.php
Executable file
49
extensions/tags/src/DiscussionMovedPost.php
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php namespace Flarum\Categories;
|
||||||
|
|
||||||
|
use Flarum\Core\Models\Post;
|
||||||
|
|
||||||
|
class DiscussionMovedPost extends Post
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new instance in reply to a discussion.
|
||||||
|
*
|
||||||
|
* @param int $discussionId
|
||||||
|
* @param int $userId
|
||||||
|
* @param string $oldTitle
|
||||||
|
* @param string $newTitle
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function reply($discussionId, $userId, $oldCategoryId, $newCategoryId)
|
||||||
|
{
|
||||||
|
$post = new static;
|
||||||
|
|
||||||
|
$post->content = [$oldCategoryId, $newCategoryId];
|
||||||
|
$post->time = time();
|
||||||
|
$post->discussion_id = $discussionId;
|
||||||
|
$post->user_id = $userId;
|
||||||
|
$post->type = 'discussionMoved';
|
||||||
|
|
||||||
|
return $post;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unserialize the content attribute.
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getContentAttribute($value)
|
||||||
|
{
|
||||||
|
return json_decode($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the content attribute.
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
*/
|
||||||
|
public function setContentAttribute($value)
|
||||||
|
{
|
||||||
|
$this->attributes['content'] = json_encode($value);
|
||||||
|
}
|
||||||
|
}
|
100
extensions/tags/src/Handlers/Handler.php
Executable file
100
extensions/tags/src/Handlers/Handler.php
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php namespace Flarum\Categories\Handlers;
|
||||||
|
|
||||||
|
use Flarum\Api\Events\SerializeRelationship;
|
||||||
|
use Flarum\Api\Serializers\DiscussionSerializer;
|
||||||
|
use Flarum\Support\Actor;
|
||||||
|
use Flarum\Forum\Events\RenderView;
|
||||||
|
use Flarum\Core\Events\ModelCall;
|
||||||
|
use Flarum\Core\Events\RegisterDiscussionGambits;
|
||||||
|
use Flarum\Core\Models\Discussion;
|
||||||
|
use Flarum\Categories\Category;
|
||||||
|
use Flarum\Categories\CategorySerializer;
|
||||||
|
use Flarum\Categories\DiscussionMovedPost;
|
||||||
|
use Flarum\Core\Events\DiscussionWillBeSaved;
|
||||||
|
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
public function __construct(Actor $actor)
|
||||||
|
{
|
||||||
|
$this->actor = $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subscribe($events)
|
||||||
|
{
|
||||||
|
$events->listen('Flarum\Forum\Events\RenderView', __CLASS__.'@renderForum');
|
||||||
|
$events->listen('Flarum\Api\Events\SerializeRelationship', __CLASS__.'@serializeRelationship');
|
||||||
|
$events->listen('Flarum\Core\Events\RegisterDiscussionGambits', __CLASS__.'@registerGambits');
|
||||||
|
$events->listen('Flarum\Core\Events\DiscussionWillBeSaved', __CLASS__.'@saveCategoryToDiscussion');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderForum(RenderView $event)
|
||||||
|
{
|
||||||
|
$root = __DIR__.'/../..';
|
||||||
|
|
||||||
|
$event->assets->addFile([
|
||||||
|
$root.'/js/dist/extension.js',
|
||||||
|
$root.'/less/categories.less'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serializer = new CategorySerializer($event->action->actor);
|
||||||
|
$event->view->data = array_merge($event->view->data, $serializer->collection(Category::orderBy('position')->get())->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveCategoryToDiscussion(DiscussionWillBeSaved $event)
|
||||||
|
{
|
||||||
|
if (isset($event->command->data['links']['category']['linkage'])) {
|
||||||
|
$linkage = $event->command->data['links']['category']['linkage'];
|
||||||
|
|
||||||
|
$categoryId = (int) $linkage['id'];
|
||||||
|
$discussion = $event->discussion;
|
||||||
|
$user = $event->command->user;
|
||||||
|
|
||||||
|
$oldCategoryId = (int) $discussion->category_id;
|
||||||
|
|
||||||
|
if ($oldCategoryId === $categoryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discussion->category_id = $categoryId;
|
||||||
|
|
||||||
|
if ($discussion->exists) {
|
||||||
|
$lastPost = $discussion->posts()->orderBy('time', 'desc')->first();
|
||||||
|
if ($lastPost instanceof DiscussionMovedPost) {
|
||||||
|
if ($lastPost->content[0] == $categoryId) {
|
||||||
|
$lastPost->delete();
|
||||||
|
$discussion->postWasRemoved($lastPost);
|
||||||
|
} else {
|
||||||
|
$newContent = $lastPost->content;
|
||||||
|
$newContent[1] = $categoryId;
|
||||||
|
$lastPost->content = $newContent;
|
||||||
|
$lastPost->save();
|
||||||
|
$discussion->postWasAdded($lastPost);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$post = DiscussionMovedPost::reply(
|
||||||
|
$discussion->id,
|
||||||
|
$user->id,
|
||||||
|
$oldCategoryId,
|
||||||
|
$categoryId
|
||||||
|
);
|
||||||
|
|
||||||
|
$post->save();
|
||||||
|
|
||||||
|
$discussion->postWasAdded($post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerGambits(RegisterDiscussionGambits $event)
|
||||||
|
{
|
||||||
|
$event->gambits->add('Flarum\Categories\CategoryGambit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serializeRelationship(SerializeRelationship $event)
|
||||||
|
{
|
||||||
|
if ($event->serializer instanceof DiscussionSerializer && $event->name === 'category') {
|
||||||
|
return $event->serializer->hasOne('Flarum\Categories\CategorySerializer', 'category');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user