mirror of
https://github.com/flarum/core.git
synced 2025-08-01 06:00:24 +02:00
Webpack (#33)
See https://github.com/flarum/core/pull/1367 * Replace gulp with webpack and npm scripts for JS compilation * Set up Travis CI to commit compiled JS * Restructure `js` directory; only one instance of npm, forum/admin are "submodules" * Restructure `less` directory
This commit is contained in:
192
extensions/mentions/js/src/forum/addComposerAutocomplete.js
Normal file
192
extensions/mentions/js/src/forum/addComposerAutocomplete.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import getCaretCoordinates from 'textarea-caret';
|
||||
|
||||
import { extend } from 'flarum/extend';
|
||||
import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import usernameHelper from 'flarum/helpers/username';
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import KeyboardNavigatable from 'flarum/utils/KeyboardNavigatable';
|
||||
import { truncate } from 'flarum/utils/string';
|
||||
|
||||
import AutocompleteDropdown from './components/AutocompleteDropdown';
|
||||
|
||||
export default function addComposerAutocomplete() {
|
||||
extend(ComposerBody.prototype, 'config', function(original, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
const composer = this;
|
||||
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
||||
const dropdown = new AutocompleteDropdown({items: []});
|
||||
const $textarea = this.$('textarea').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
|
||||
const searched = [];
|
||||
let mentionStart;
|
||||
let typed;
|
||||
let searchTimeout;
|
||||
|
||||
const applySuggestion = function(replacement) {
|
||||
const insert = replacement + ' ';
|
||||
|
||||
const content = composer.content();
|
||||
composer.editor.setValue(content.substring(0, mentionStart - 1) + insert + content.substr($textarea[0].selectionStart));
|
||||
|
||||
const index = mentionStart - 1 + insert.length;
|
||||
composer.editor.setSelectionRange(index, index);
|
||||
|
||||
dropdown.hide();
|
||||
};
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.when(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.bindTo($textarea);
|
||||
|
||||
$textarea
|
||||
.after($container)
|
||||
.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;
|
||||
|
||||
const cursor = this.selectionStart;
|
||||
|
||||
if (this.selectionEnd - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for an '@' symbol. If we find one,
|
||||
// we will want to show the autocomplete dropdown!
|
||||
const value = this.value;
|
||||
mentionStart = 0;
|
||||
for (let i = cursor - 1; i >= cursor - 30; i--) {
|
||||
const character = value.substr(i, 1);
|
||||
if (character === '@') {
|
||||
mentionStart = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dropdown.hide();
|
||||
dropdown.active = false;
|
||||
|
||||
if (mentionStart) {
|
||||
typed = value.substring(mentionStart, cursor).toLowerCase();
|
||||
|
||||
const makeSuggestion = function(user, replacement, content, className = '') {
|
||||
const username = usernameHelper(user);
|
||||
if (typed) {
|
||||
username.children[0] = highlight(username.children[0], typed);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={'PostPreview ' + className}
|
||||
onclick={() => applySuggestion(replacement)}
|
||||
onmouseenter={function() {
|
||||
dropdown.setIndex($(this).parent().index());
|
||||
}}>
|
||||
<span className="PostPreview-content">
|
||||
{avatar(user)}
|
||||
{username} {' '}
|
||||
{content}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const userMatches = function(user) {
|
||||
const names = [
|
||||
user.username(),
|
||||
user.displayName()
|
||||
];
|
||||
|
||||
return names.some(value => value.toLowerCase().substr(0, typed.length) === typed);
|
||||
};
|
||||
|
||||
const buildSuggestions = () => {
|
||||
const suggestions = [];
|
||||
|
||||
// If the user has started to type a username, then suggest users
|
||||
// matching that username.
|
||||
if (typed) {
|
||||
app.store.all('users').forEach(user => {
|
||||
if (!userMatches(user)) return;
|
||||
|
||||
suggestions.push(
|
||||
makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
const composerPost = composer.props.post;
|
||||
const 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 => {
|
||||
const user = post.user();
|
||||
return user && userMatches(user);
|
||||
})
|
||||
.splice(0, 5)
|
||||
.forEach(post => {
|
||||
const user = post.user();
|
||||
suggestions.push(
|
||||
makeSuggestion(user, '@' + user.username() + '#' + post.id(), [
|
||||
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', {number: post.number()}), ' — ',
|
||||
truncate(post.contentPlain(), 200)
|
||||
], 'MentionsDropdown-post')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (suggestions.length) {
|
||||
dropdown.props.items = suggestions;
|
||||
m.render($container[0], dropdown.render());
|
||||
|
||||
dropdown.show();
|
||||
const coordinates = getCaretCoordinates(this, mentionStart);
|
||||
const width = dropdown.$().outerWidth();
|
||||
const height = dropdown.$().outerHeight();
|
||||
const parent = dropdown.$().offsetParent();
|
||||
let left = coordinates.left;
|
||||
let top = coordinates.top + 15;
|
||||
if (top + height > parent.height()) {
|
||||
top = coordinates.top - height - 15;
|
||||
}
|
||||
if (left + width > parent.width()) {
|
||||
left = parent.width() - width;
|
||||
}
|
||||
dropdown.show(left, top);
|
||||
} else {
|
||||
dropdown.active = false;
|
||||
dropdown.hide();
|
||||
}
|
||||
};
|
||||
|
||||
dropdown.active = true;
|
||||
|
||||
buildSuggestions();
|
||||
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
if (typed) {
|
||||
searchTimeout = setTimeout(function() {
|
||||
const typedLower = typed.toLowerCase();
|
||||
if (searched.indexOf(typedLower) === -1) {
|
||||
app.store.find('users', {filter: {q: typed}, page: {limit: 5}}).then(() => {
|
||||
if (dropdown.active) buildSuggestions();
|
||||
});
|
||||
searched.push(typedLower);
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user