1
0
mirror of https://github.com/flarum/core.git synced 2025-08-16 21:34:08 +02:00

Update for new API + TextFormatter

This commit is contained in:
Toby Zerner
2015-07-23 14:28:39 +09:30
parent 985ccc096e
commit da25882280
45 changed files with 1224 additions and 925 deletions

View File

@@ -1,89 +0,0 @@
import Component from 'flarum/component';
export default class AutocompleteDropdown extends Component {
constructor(props) {
super(props);
this.active = m.prop(false);
this.index = m.prop(0);
this.keyWasJustPressed = false;
}
view() {
return m('ul.dropdown-menu.mentions-dropdown', this.props.items.map(item => m('li', item)));
}
show(left, top) {
this.$().show().css({
left: left+'px',
top: top+'px'
});
this.active(true);
}
hide() {
this.$().hide();
this.active(false);
}
navigate(e) {
if (!this.active()) return;
switch (e.which) {
case 40: case 38: // Down/Up
this.keyWasJustPressed = true;
this.setIndex(this.index() + (e.which === 40 ? 1 : -1), true);
clearTimeout(this.keyWasJustPressedTimeout);
this.keyWasJustPressedTimeout = setTimeout(() => this.keyWasJustPressed = false, 500);
e.preventDefault();
break;
case 13: case 9: // Enter/Tab
this.$('li').eq(this.index()).find('a').click();
e.preventDefault();
break;
case 27: // Escape
this.hide();
e.stopPropagation();
e.preventDefault();
break;
}
}
setIndex(index, scrollToItem) {
if (this.keyWasJustPressed && !scrollToItem) return;
var $dropdown = this.$();
var $items = $dropdown.find('li');
if (index < 0) {
index = $items.length - 1;
} else if (index >= $items.length) {
index = 0;
}
this.index(index);
var $item = $items.removeClass('active').eq(index).addClass('active');
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);
}
}
}
}

View File

@@ -1,17 +0,0 @@
import Notification from 'flarum/components/notification';
import username from 'flarum/helpers/username';
export default class PostMentionedNotification extends Notification {
view() {
var notification = this.props.notification;
var post = notification.subject();
var auc = notification.additionalUnreadCount();
var content = notification.content();
return super.view({
href: app.route.discussion(post.discussion(), auc ? post.number() : (content && content.replyNumber)),
icon: 'reply',
content: [username(notification.sender()), (auc ? ' and '+auc+' others' : '')+' replied to your post #'+post.number()]
});
}
}

View File

@@ -1,15 +0,0 @@
import Notification from 'flarum/components/notification';
import username from 'flarum/helpers/username';
export default class UserMentionedNotification extends Notification {
view() {
var notification = this.props.notification;
var post = notification.subject();
return super.view({
href: app.route.discussion(post.discussion(), post.number()),
icon: 'at',
content: [username(notification.sender()), ' mentioned you']
});
}
}

View File

@@ -1,169 +0,0 @@
import { extend } from 'flarum/extension-utils';
import ComposerBody from 'flarum/components/composer-body';
import ReplyComposer from 'flarum/components/reply-composer';
import EditComposer from 'flarum/components/edit-composer';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import highlight from 'flarum/helpers/highlight';
import truncate from 'flarum/utils/truncate';
import AutocompleteDropdown from 'flarum-mentions/components/autocomplete-dropdown';
export default function() {
extend(ComposerBody.prototype, 'config', function(original, element, isInitialized, context) {
if (isInitialized) return;
var composer = this;
var $container = $('<div class="mentions-dropdown-container"></div>');
var dropdown = new AutocompleteDropdown({items: []});
var typed;
var mentionStart;
var $textarea = this.$('textarea');
var searched = [];
var searchTimeout;
var applySuggestion = function(replacement) {
replacement += ' ';
var content = composer.content();
composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr($textarea[0].selectionStart));
var index = mentionStart - 1 + replacement.length;
composer.editor.setSelectionRange(index, index);
dropdown.hide();
};
$textarea
.after($container)
.on('keydown', dropdown.navigate.bind(dropdown))
.on('click keyup', function(e) {
// Up, down, enter, tab, escape, left, right.
if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;
var cursor = this.selectionStart;
if (this.selectionEnd - cursor > 0) return;
// Search backwards from the cursor for an '@' symbol, without any
// intervening whitespace. If we find one, we will want to show the
// autocomplete dropdown!
var value = this.value;
mentionStart = 0;
for (var i = cursor - 1; i >= 0; i--) {
var character = value.substr(i, 1);
if (/\s/.test(character)) break;
if (character == '@') {
mentionStart = i + 1;
break;
}
}
dropdown.hide();
dropdown.active(false);
if (mentionStart) {
typed = value.substring(mentionStart, cursor).toLowerCase();
var makeSuggestion = function(user, replacement, content, className) {
return m('a[href=javascript:;].post-preview', {
className,
onclick: () => applySuggestion(replacement),
onmouseenter: function() { dropdown.setIndex($(this).parent().index()); }
}, m('div.post-preview-content', [
avatar(user),
(function() {
var vdom = username(user);
if (typed) {
vdom.children[0] = highlight(vdom.children[0], typed);
}
return vdom;
})(), ' ',
content
]));
};
var buildSuggestions = () => {
var suggestions = [];
// If the user is replying to a discussion, or if they are editing a
// post, then we can suggest other posts in the discussion to mention.
// We will add the 5 most recent comments in the discussion which
// match any username characters that have been typed.
var composerPost = composer.props.post;
var discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
if (discussion) {
discussion.posts()
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
.sort((a, b) => b.time() - a.time())
.filter(post => {
var user = post.user();
return user && user.username().toLowerCase().substr(0, typed.length) === typed;
})
.splice(0, 5)
.forEach(post => {
var user = post.user();
suggestions.push(
makeSuggestion(user, '@'+user.username()+'#'+post.number(), [
'Reply to #', post.number(), ' — ',
truncate(post.contentPlain(), 200)
], 'suggestion-post')
);
});
}
// If the user has started to type a username, then suggest users
// matching that username.
if (typed) {
app.store.all('users').forEach(user => {
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;
suggestions.push(
makeSuggestion(user, '@'+user.username(), '', 'suggestion-user')
);
});
}
if (suggestions.length) {
dropdown.props.items = suggestions;
m.render($container[0], dropdown.render());
dropdown.show();
var coordinates = getCaretCoordinates(this, mentionStart);
var left = coordinates.left;
var top = coordinates.top + 15;
var width = dropdown.$().outerWidth();
var height = dropdown.$().outerHeight();
var parent = dropdown.$().offsetParent();
if (top + height > parent.height()) {
top = coordinates.top - height - 15;
}
if (left + width > parent.width()) {
left = parent.width() - width;
}
dropdown.show(left, top);
}
};
buildSuggestions();
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
dropdown.active(true);
clearTimeout(searchTimeout);
if (typed) {
searchTimeout = setTimeout(function() {
var typedLower = typed.toLowerCase();
if (searched.indexOf(typedLower) === -1) {
app.store.find('users', {q: typed, page: {limit: 5}}).then(users => {
if (dropdown.active()) buildSuggestions();
});
searched.push(typedLower);
}
}, 250);
}
}
});
});
}

View File

@@ -1,102 +0,0 @@
import { extend } from 'flarum/extension-utils';
import Model from 'flarum/model';
import Post from 'flarum/models/post';
import DiscussionPage from 'flarum/components/discussion-page';
import CommentPost from 'flarum/components/comment-post';
import PostPreview from 'flarum/components/post-preview';
import punctuate from 'flarum/helpers/punctuate';
import username from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon';
export default function mentionedByList() {
Post.prototype.mentionedBy = Model.many('mentionedBy');
extend(DiscussionPage.prototype, 'params', function(params) {
params.include.push('posts.mentionedBy', 'posts.mentionedBy.user');
});
extend(CommentPost.prototype, 'footerItems', function(items) {
var post = this.props.post;
var replies = post.mentionedBy();
if (replies && replies.length) {
// If there is only one reply, and it's adjacent to this post, we don't
// really need to show the list.
if (replies.length === 1 && replies[0].number() == post.number() + 1) {
return;
}
var hidePreview = () => {
this.$('.mentioned-by-preview').removeClass('in').one('transitionend', function() { $(this).hide(); });
};
var config = function(element, isInitialized) {
if (isInitialized) return;
var $this = $(element);
var timeout;
var $preview = $('<ul class="dropdown-menu mentioned-by-preview fade"/>');
$this.append($preview);
$this.children().hover(function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
if (!$preview.hasClass('in') && $preview.is(':visible')) return;
// When the user hovers their mouse over the list of people who have
// replied to the post, render a list of reply previews into a
// popup.
m.render($preview[0], replies.map(post => {
return m('li', {'data-number': post.number()}, PostPreview.component({post, onclick: hidePreview}));
}));
$preview.show();
setTimeout(() => $preview.off('transitionend').addClass('in'));
}, 500);
}, function() {
clearTimeout(timeout);
timeout = setTimeout(hidePreview, 250);
});
// Whenever the user hovers their mouse over a particular name in the
// list of repliers, highlight the corresponding post in the preview
// popup.
$this.find('.summary a').hover(function() {
$preview.find('[data-number='+$(this).data('number')+']').addClass('active');
}, function() {
$preview.find('[data-number]').removeClass('active');
});
};
// Create a list of unique users who have replied. So even if a user has
// replied twice, they will only be in this array once.
var used = [];
var repliers = replies.filter(reply => {
var user = reply.user();
var id = user && user.id();
if (used.indexOf(id) === -1) {
used.push(id);
return true;
}
});
items.add('replies',
m('div.mentioned-by', {config}, [
m('span.summary', [
icon('reply icon'),
punctuate(repliers.map(reply => {
return m('a', {
href: app.route.post(reply),
config: m.route,
onclick: hidePreview,
'data-number': reply.number()
}, [
app.session.user() && reply.user() === app.session.user() ? 'You' : username(reply.user())
])
})),
' replied to this.'
])
])
);
}
});
}

View File

@@ -1,96 +0,0 @@
import { extend } from 'flarum/extension-utils';
import CommentPost from 'flarum/components/comment-post';
import PostPreview from 'flarum/components/post-preview';
import LoadingIndicator from 'flarum/components/loading-indicator';
export default function postMentionPreviews() {
extend(CommentPost.prototype, 'config', function() {
var contentHtml = this.props.post.contentHtml();
if (contentHtml === this.oldPostContentHtml) return;
this.oldPostContentHtml = contentHtml;
var discussion = this.props.post.discussion();
this.$('.mention-post').each(function() {
var $this = $(this);
var number = $this.data('number');
var timeout;
// Wrap the mention link in a wrapper element so that we can insert a
// preview popup as its sibling and relatively position it.
var $preview = $('<ul class="dropdown-menu mention-post-preview fade"/>');
var $wrapper = $('<span class="mention-post-wrapper"/>');
$this.wrap($wrapper).after($preview);
var getPostElement = function() {
return $('.discussion-posts .item[data-number='+number+']');
};
var showPreview = function() {
// When the user hovers their mouse over the mention, look for the
// post that it's referring to in the stream, and determine if it's
// in the viewport. If it is, we will "pulsate" it.
var $post = getPostElement();
var visible = false;
if ($post.length) {
var top = $post.offset().top;
var scrollTop = window.pageYOffset;
if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {
$post.addClass('pulsate');
visible = true;
}
}
// Otherwise, we will show a popup preview of the post. If the post
// hasn't yet been loaded, we will need to do that.
if (!visible) {
var showPost = function(post) {
m.render($preview[0], m('li', PostPreview.component({post})));
positionPreview();
};
// Position the preview so that it appears above the mention.
// (The offsetParent should be .post-body.)
var positionPreview = function() {
$preview.show().css('top', $this.offset().top - $this.offsetParent().offset().top - $preview.outerHeight(true));
};
var post = discussion.posts().filter(post => post && post.number() == number)[0];
if (post) {
showPost(post);
} else {
m.render($preview[0], LoadingIndicator.component());
app.store.find('posts', {discussions: discussion.id(), number}).then(posts => showPost(posts[0]));
positionPreview();
}
setTimeout(() => $preview.off('transitionend').addClass('in'));
}
};
var hidePreview = () => {
getPostElement().removeClass('pulsate');
if ($preview.hasClass('in')) {
$preview.removeClass('in').one('transitionend', () => $preview.hide());
}
};
$this.parent().hover(
function() {
clearTimeout(timeout);
timeout = setTimeout(showPreview, 500);
},
function() {
clearTimeout(timeout);
getPostElement().removeClass('pulsate');
timeout = setTimeout(hidePreview, 250);
}
)
.on('click', e => e.preventDefault())
.on('touchend', e => {
showPreview();
e.stopPropagation();
});
$(document).on('touchend', hidePreview);
});
});
}

View File

@@ -1,40 +0,0 @@
import { extend } from 'flarum/extension-utils';
import ActionButton from 'flarum/components/action-button';
import CommentPost from 'flarum/components/comment-post';
export default function() {
extend(CommentPost.prototype, 'actionItems', function(items) {
var post = this.props.post;
if (post.isHidden() || (app.session.user() && !post.discussion().canReply())) return;
function insertMention(component, quote) {
var mention = '@'+post.user().username()+'#'+post.number()+' ';
// If the composer is empty, then assume we're starting a new reply.
// In which case we don't want the user to have to confirm if they
// close the composer straight away.
if (!component.content()) {
component.props.originalContent = mention;
}
component.editor.insertAtCursor((component.editor.getSelectionRange()[0] > 0 ? '\n\n' : '')+(quote ? '> '+mention+quote.trim().replace(/\n/g, '\n> ')+'\n\n' : mention));
}
items.add('reply',
ActionButton.component({
icon: 'reply',
label: 'Reply',
onclick: () => {
var quote = window.getSelection().toString();
var component = app.composer.component;
if (component && component.props.post && component.props.post.discussion() === post.discussion()) {
insertMention(component, quote);
} else {
post.discussion().replyAction().then(component => insertMention(component, quote));
}
}
})
);
});
}