mirror of
https://github.com/flarum/core.git
synced 2025-07-29 12:40:40 +02:00
Minor refactors in frontend JS (#69)
* Add Prettier * Update dependencies; add typescript setup * Add tsconfig * Rewrite some mentions frontend into TS * Fix use of username instead of display name * Change back to JS * Remove commented code * Update function name to match filename * Update getMentionText.js * Simplify condition * Bump packages to stable versions; use prettier package * Make functions use camel case * Update js/package.json Co-authored-by: Sami Mazouz <sychocouldy@gmail.com> * Don't access data directly * Update js/src/forum/addComposerAutocomplete.js Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com> * Update tsconfig.json Co-authored-by: Sami Mazouz <sychocouldy@gmail.com> Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com>
This commit is contained in:
6408
extensions/mentions/js/package-lock.json
generated
6408
extensions/mentions/js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,20 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/mentions",
|
"name": "@flarum/mentions",
|
||||||
|
"prettier": "@flarum/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flarum-webpack-config": "0.1.0-beta.10",
|
"flarum-webpack-config": "^1.0.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.11"
|
"webpack-cli": "^4.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"build": "webpack --mode production"
|
"build": "webpack --mode production",
|
||||||
|
"format": "prettier --write src"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flarum/prettier-config": "^1.0.0",
|
||||||
|
"@types/mithril": "^2.0.8",
|
||||||
|
"flarum-tsconfig": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,42 @@
|
|||||||
import { extend } from 'flarum/extend';
|
import { extend } from 'flarum/common/extend';
|
||||||
import TextEditor from 'flarum/components/TextEditor';
|
import TextEditor from 'flarum/common/components/TextEditor';
|
||||||
import TextEditorButton from 'flarum/components/TextEditorButton';
|
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
|
||||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
|
||||||
import avatar from 'flarum/helpers/avatar';
|
import avatar from 'flarum/common/helpers/avatar';
|
||||||
import usernameHelper from 'flarum/helpers/username';
|
import usernameHelper from 'flarum/common/helpers/username';
|
||||||
import highlight from 'flarum/helpers/highlight';
|
import highlight from 'flarum/common/helpers/highlight';
|
||||||
import KeyboardNavigatable from 'flarum/utils/KeyboardNavigatable';
|
import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable';
|
||||||
import { truncate } from 'flarum/utils/string';
|
import { truncate } from 'flarum/common/utils/string';
|
||||||
|
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||||
|
|
||||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||||
import cleanDisplayName from './utils/cleanDisplayName';
|
import getMentionText from './utils/getMentionText';
|
||||||
|
|
||||||
|
const throttledSearch = throttle(
|
||||||
|
250, // 250ms timeout
|
||||||
|
function (typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions) {
|
||||||
|
const typedLower = typed.toLowerCase();
|
||||||
|
if (!searched.includes(typedLower)) {
|
||||||
|
app.store.find('users', { filter: { q: typed }, page: { limit: 5 } }).then((results) => {
|
||||||
|
results.forEach((u) => {
|
||||||
|
if (!returnedUserIds.has(u.id())) {
|
||||||
|
returnedUserIds.add(u.id());
|
||||||
|
returnedUsers.push(u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (dropdown.active) buildSuggestions();
|
||||||
|
});
|
||||||
|
searched.push(typedLower);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default function addComposerAutocomplete() {
|
export default function addComposerAutocomplete() {
|
||||||
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
||||||
const dropdown = new AutocompleteDropdown();
|
const dropdown = new AutocompleteDropdown();
|
||||||
|
|
||||||
extend(TextEditor.prototype, 'oncreate', function (params) {
|
extend(TextEditor.prototype, 'oncreate', function () {
|
||||||
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
|
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
|
||||||
|
|
||||||
this.navigator = new KeyboardNavigatable();
|
this.navigator = new KeyboardNavigatable();
|
||||||
@@ -37,13 +57,12 @@ export default function addComposerAutocomplete() {
|
|||||||
let absMentionStart;
|
let absMentionStart;
|
||||||
let typed;
|
let typed;
|
||||||
let matchTyped;
|
let matchTyped;
|
||||||
let searchTimeout;
|
|
||||||
|
|
||||||
// We store users returned from an API here to preserve order in which they are returned
|
// We store users returned from an API here to preserve order in which they are returned
|
||||||
// This prevents the user list jumping around while users are returned.
|
// This prevents the user list jumping around while users are returned.
|
||||||
// We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
|
// We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
|
||||||
const returnedUsers = Array.from(app.store.all('users'));
|
const returnedUsers = Array.from(app.store.all('users'));
|
||||||
const returnedUserIds = new Set(returnedUsers.map(u => u.id()));
|
const returnedUserIds = new Set(returnedUsers.map((u) => u.id()));
|
||||||
|
|
||||||
const applySuggestion = (replacement) => {
|
const applySuggestion = (replacement) => {
|
||||||
app.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
app.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
||||||
@@ -51,7 +70,7 @@ export default function addComposerAutocomplete() {
|
|||||||
dropdown.hide();
|
dropdown.hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
params.inputListeners.push(function(e) {
|
params.inputListeners.push(function () {
|
||||||
const selection = app.composer.editor.getSelectionRange();
|
const selection = app.composer.editor.getSelectionRange();
|
||||||
|
|
||||||
const cursor = selection[0];
|
const cursor = selection[0];
|
||||||
@@ -81,33 +100,32 @@ export default function addComposerAutocomplete() {
|
|||||||
|
|
||||||
const makeSuggestion = function (user, replacement, content, className = '') {
|
const makeSuggestion = function (user, replacement, content, className = '') {
|
||||||
const username = usernameHelper(user);
|
const username = usernameHelper(user);
|
||||||
|
|
||||||
if (typed) {
|
if (typed) {
|
||||||
username.children = [highlight(username.text, typed)];
|
username.children = [highlight(username.text, typed)];
|
||||||
delete username.text;
|
delete username.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={'PostPreview ' + className}
|
<button
|
||||||
|
className={'PostPreview ' + className}
|
||||||
onclick={() => applySuggestion(replacement)}
|
onclick={() => applySuggestion(replacement)}
|
||||||
onmouseenter={function () {
|
onmouseenter={function () {
|
||||||
dropdown.setIndex($(this).parent().index());
|
dropdown.setIndex($(this).parent().index());
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span className="PostPreview-content">
|
<span className="PostPreview-content">
|
||||||
{avatar(user)}
|
{avatar(user)}
|
||||||
{username} {' '}
|
{username} {content}
|
||||||
{content}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const userMatches = function (user) {
|
const userMatches = function (user) {
|
||||||
const names = [
|
const names = [user.username(), user.displayName()];
|
||||||
user.username(),
|
|
||||||
user.displayName()
|
|
||||||
];
|
|
||||||
|
|
||||||
return names.some(name => name.toLowerCase().substr(0, typed.length) === typed);
|
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSuggestions = () => {
|
const buildSuggestions = () => {
|
||||||
@@ -116,12 +134,10 @@ export default function addComposerAutocomplete() {
|
|||||||
// If the user has started to type a username, then suggest users
|
// If the user has started to type a username, then suggest users
|
||||||
// matching that username.
|
// matching that username.
|
||||||
if (typed) {
|
if (typed) {
|
||||||
returnedUsers.forEach(user => {
|
returnedUsers.forEach((user) => {
|
||||||
if (!userMatches(user)) return;
|
if (!userMatches(user)) return;
|
||||||
|
|
||||||
suggestions.push(
|
suggestions.push(makeSuggestion(user, getMentionText(user), '', 'MentionsDropdown-user'));
|
||||||
makeSuggestion(user, `@"${cleanDisplayName(user)}"#${user.id()}`, '', 'MentionsDropdown-user')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,21 +151,33 @@ export default function addComposerAutocomplete() {
|
|||||||
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
|
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
|
||||||
|
|
||||||
if (discussion) {
|
if (discussion) {
|
||||||
discussion.posts()
|
discussion
|
||||||
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
.posts()
|
||||||
|
// Filter to only comment posts, and replies before this message
|
||||||
|
.filter((post) => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
||||||
|
// Sort by new to old
|
||||||
.sort((a, b) => b.createdAt() - a.createdAt())
|
.sort((a, b) => b.createdAt() - a.createdAt())
|
||||||
.filter(post => {
|
// Filter to where the user matches what is being typed
|
||||||
|
.filter((post) => {
|
||||||
const user = post.user();
|
const user = post.user();
|
||||||
return user && userMatches(user);
|
return user && userMatches(user);
|
||||||
})
|
})
|
||||||
|
// Get the first 5
|
||||||
.splice(0, 5)
|
.splice(0, 5)
|
||||||
.forEach(post => {
|
// Make the suggestions
|
||||||
|
.forEach((post) => {
|
||||||
const user = post.user();
|
const user = post.user();
|
||||||
suggestions.push(
|
suggestions.push(
|
||||||
makeSuggestion(user, `@"${cleanDisplayName(user)}"#p${post.id()}`, [
|
makeSuggestion(
|
||||||
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', {number: post.number()}), ' — ',
|
user,
|
||||||
truncate(post.contentPlain(), 200)
|
getMentionText(user, post.id()),
|
||||||
], 'MentionsDropdown-post')
|
[
|
||||||
|
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }),
|
||||||
|
' — ',
|
||||||
|
truncate(post.contentPlain(), 200),
|
||||||
|
],
|
||||||
|
'MentionsDropdown-post'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,35 +221,21 @@ export default function addComposerAutocomplete() {
|
|||||||
dropdown.setIndex(0);
|
dropdown.setIndex(0);
|
||||||
dropdown.$().scrollTop(0);
|
dropdown.$().scrollTop(0);
|
||||||
|
|
||||||
clearTimeout(searchTimeout);
|
|
||||||
// Don't send API calls searching for users until at least 2 characters have been typed.
|
// Don't send API calls searching for users until at least 2 characters have been typed.
|
||||||
// This focuses the mention results on users and posts in the discussion.
|
// This focuses the mention results on users and posts in the discussion.
|
||||||
if (typed.length > 1) {
|
if (typed.length > 1) {
|
||||||
searchTimeout = setTimeout(function() {
|
throttledSearch(typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions);
|
||||||
const typedLower = typed.toLowerCase();
|
|
||||||
if (searched.indexOf(typedLower) === -1) {
|
|
||||||
app.store.find('users', { filter: { q: typed }, page: { limit: 5 } }).then(results => {
|
|
||||||
results.forEach(u => {
|
|
||||||
if (!returnedUserIds.has(u.id())) {
|
|
||||||
returnedUserIds.add(u.id());
|
|
||||||
returnedUsers.push(u);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (dropdown.active) buildSuggestions();
|
|
||||||
});
|
|
||||||
searched.push(typedLower);
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
||||||
items.add('mention', (
|
items.add(
|
||||||
|
'mention',
|
||||||
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(' @')} icon="fas fa-at">
|
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(' @')} icon="fas fa-at">
|
||||||
{app.translator.trans('flarum-mentions.forum.composer.mention_tooltip')}
|
{app.translator.trans('flarum-mentions.forum.composer.mention_tooltip')}
|
||||||
</TextEditorButton>
|
</TextEditorButton>
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { extend } from 'flarum/extend';
|
import { extend } from 'flarum/common/extend';
|
||||||
import Model from 'flarum/Model';
|
import Model from 'flarum/common/Model';
|
||||||
import Post from 'flarum/models/Post';
|
import Post from 'flarum/common/models/Post';
|
||||||
import CommentPost from 'flarum/components/CommentPost';
|
import CommentPost from 'flarum/forum/components/CommentPost';
|
||||||
import Link from 'flarum/components/Link';
|
import Link from 'flarum/common/components/Link';
|
||||||
import PostPreview from 'flarum/components/PostPreview';
|
import PostPreview from 'flarum/forum/components/PostPreview';
|
||||||
import punctuateSeries from 'flarum/helpers/punctuateSeries';
|
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
|
||||||
import username from 'flarum/helpers/username';
|
import username from 'flarum/common/helpers/username';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from 'flarum/common/helpers/icon';
|
||||||
|
|
||||||
export default function addMentionedByList() {
|
export default function addMentionedByList() {
|
||||||
Post.prototype.mentionedBy = Model.hasMany('mentionedBy');
|
Post.prototype.mentionedBy = Model.hasMany('mentionedBy');
|
||||||
@@ -14,7 +14,9 @@ export default function addMentionedByList() {
|
|||||||
function hidePreview() {
|
function hidePreview() {
|
||||||
this.$('.Post-mentionedBy-preview')
|
this.$('.Post-mentionedBy-preview')
|
||||||
.removeClass('in')
|
.removeClass('in')
|
||||||
.one('transitionend', function() { $(this).hide(); });
|
.one('transitionend', function () {
|
||||||
|
$(this).hide();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extend(CommentPost.prototype, 'oncreate', function () {
|
extend(CommentPost.prototype, 'oncreate', function () {
|
||||||
@@ -35,22 +37,26 @@ export default function addMentionedByList() {
|
|||||||
// When the user hovers their mouse over the list of people who have
|
// 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
|
// replied to the post, render a list of reply previews into a
|
||||||
// popup.
|
// popup.
|
||||||
m.render($preview[0], replies.map(reply => (
|
m.render(
|
||||||
|
$preview[0],
|
||||||
|
replies.map((reply) => (
|
||||||
<li data-number={reply.number()}>
|
<li data-number={reply.number()}>
|
||||||
{PostPreview.component({
|
{PostPreview.component({
|
||||||
post: reply,
|
post: reply,
|
||||||
onclick: hidePreview.bind(this)
|
onclick: hidePreview.bind(this),
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
)));
|
))
|
||||||
|
);
|
||||||
|
|
||||||
$preview.show()
|
$preview
|
||||||
|
.show()
|
||||||
.css('top', $this.offset().top - $parentPost.offset().top + $this.outerHeight(true))
|
.css('top', $this.offset().top - $parentPost.offset().top + $this.outerHeight(true))
|
||||||
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
|
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
|
||||||
.css('max-width', $parentPost.width());
|
.css('max-width', $parentPost.width());
|
||||||
|
|
||||||
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
||||||
}
|
};
|
||||||
|
|
||||||
$this.add($preview).hover(
|
$this.add($preview).hover(
|
||||||
() => {
|
() => {
|
||||||
@@ -66,11 +72,16 @@ export default function addMentionedByList() {
|
|||||||
// Whenever the user hovers their mouse over a particular name in the
|
// Whenever the user hovers their mouse over a particular name in the
|
||||||
// list of repliers, highlight the corresponding post in the preview
|
// list of repliers, highlight the corresponding post in the preview
|
||||||
// popup.
|
// popup.
|
||||||
this.$().find('.Post-mentionedBy-summary a').hover(function() {
|
this.$()
|
||||||
|
.find('.Post-mentionedBy-summary a')
|
||||||
|
.hover(
|
||||||
|
function () {
|
||||||
$preview.find('[data-number="' + $(this).data('number') + '"]').addClass('active');
|
$preview.find('[data-number="' + $(this).data('number') + '"]').addClass('active');
|
||||||
}, function() {
|
},
|
||||||
|
function () {
|
||||||
$preview.find('[data-number]').removeClass('active');
|
$preview.find('[data-number]').removeClass('active');
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,8 +92,8 @@ export default function addMentionedByList() {
|
|||||||
if (replies && replies.length) {
|
if (replies && replies.length) {
|
||||||
const users = [];
|
const users = [];
|
||||||
const repliers = replies
|
const repliers = replies
|
||||||
.sort(reply => reply.user() === app.session.user ? -1 : 0)
|
.sort((reply) => (reply.user() === app.session.user ? -1 : 0))
|
||||||
.filter(reply => {
|
.filter((reply) => {
|
||||||
const user = reply.user();
|
const user = reply.user();
|
||||||
if (users.indexOf(user) === -1) {
|
if (users.indexOf(user) === -1) {
|
||||||
users.push(user);
|
users.push(user);
|
||||||
@@ -95,15 +106,11 @@ export default function addMentionedByList() {
|
|||||||
|
|
||||||
// Create a list of unique users who have replied. So even if a user has
|
// 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.
|
// replied twice, they will only be in this array once.
|
||||||
const names = repliers
|
const names = repliers.slice(0, overLimit ? limit - 1 : limit).map((reply) => {
|
||||||
.slice(0, overLimit ? limit - 1 : limit)
|
|
||||||
.map(reply => {
|
|
||||||
const user = reply.user();
|
const user = reply.user();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={app.route.post(reply)}
|
<Link href={app.route.post(reply)} onclick={hidePreview.bind(this)} data-number={reply.number()}>
|
||||||
onclick={hidePreview.bind(this)}
|
|
||||||
data-number={reply.number()}>
|
|
||||||
{app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}
|
{app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -115,18 +122,17 @@ export default function addMentionedByList() {
|
|||||||
if (overLimit) {
|
if (overLimit) {
|
||||||
const count = repliers.length - names.length;
|
const count = repliers.length - names.length;
|
||||||
|
|
||||||
names.push(
|
names.push(app.translator.trans('flarum-mentions.forum.post.others_text', { count }));
|
||||||
app.translator.trans('flarum-mentions.forum.post.others_text', {count})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add('replies',
|
items.add(
|
||||||
|
'replies',
|
||||||
<div className="Post-mentionedBy">
|
<div className="Post-mentionedBy">
|
||||||
<span className="Post-mentionedBy-summary">
|
<span className="Post-mentionedBy-summary">
|
||||||
{icon('fas fa-reply')}
|
{icon('fas fa-reply')}
|
||||||
{app.translator.trans('flarum-mentions.forum.post.mentioned_by' + (repliers[0].user() === app.session.user ? '_self' : '') + '_text', {
|
{app.translator.trans('flarum-mentions.forum.post.mentioned_by' + (repliers[0].user() === app.session.user ? '_self' : '') + '_text', {
|
||||||
count: names.length,
|
count: names.length,
|
||||||
users: punctuateSeries(names)
|
users: punctuateSeries(names),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { extend } from 'flarum/extend';
|
import { extend } from 'flarum/common/extend';
|
||||||
import CommentPost from 'flarum/components/CommentPost';
|
import CommentPost from 'flarum/forum/components/CommentPost';
|
||||||
import PostPreview from 'flarum/components/PostPreview';
|
import PostPreview from 'flarum/forum/components/PostPreview';
|
||||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||||
|
|
||||||
export default function addPostMentionPreviews() {
|
export default function addPostMentionPreviews() {
|
||||||
function addPreviews() {
|
function addPreviews() {
|
||||||
@@ -65,20 +65,25 @@ export default function addPostMentionPreviews() {
|
|||||||
offset -= previewHeight;
|
offset -= previewHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
$preview.show()
|
$preview
|
||||||
|
.show()
|
||||||
.css('top', $this.offset().top - $parentPost.offset().top + offset)
|
.css('top', $this.offset().top - $parentPost.offset().top + offset)
|
||||||
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
|
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
|
||||||
.css('max-width', $this.offsetParent().width());
|
.css('max-width', $this.offsetParent().width());
|
||||||
};
|
};
|
||||||
|
|
||||||
const showPost = post => {
|
const showPost = (post) => {
|
||||||
const discussion = post.discussion();
|
const discussion = post.discussion();
|
||||||
|
|
||||||
m.render($preview[0], [
|
m.render($preview[0], [
|
||||||
discussion !== parentPost.discussion()
|
discussion !== parentPost.discussion() ? (
|
||||||
? <li><span className="PostMention-preview-discussion">{discussion.title()}</span></li>
|
<li>
|
||||||
: '',
|
<span className="PostMention-preview-discussion">{discussion.title()}</span>
|
||||||
<li>{PostPreview.component({post})}</li>
|
</li>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
<li>{PostPreview.component({ post })}</li>,
|
||||||
]);
|
]);
|
||||||
positionPreview();
|
positionPreview();
|
||||||
};
|
};
|
||||||
@@ -106,13 +111,15 @@ export default function addPostMentionPreviews() {
|
|||||||
// On a touch (mobile) device we cannot hover the link to reveal the preview.
|
// On a touch (mobile) device we cannot hover the link to reveal the preview.
|
||||||
// Instead we cancel the navigation so that a click reveals the preview.
|
// Instead we cancel the navigation so that a click reveals the preview.
|
||||||
// Users can then click on the preview to go to the post if desired.
|
// Users can then click on the preview to go to the post if desired.
|
||||||
$this.on('touchend', e => {
|
$this.on('touchend', (e) => {
|
||||||
if (e.cancelable) {
|
if (e.cancelable) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$this.add($preview).hover(
|
$this
|
||||||
|
.add($preview)
|
||||||
|
.hover(
|
||||||
() => {
|
() => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(showPreview, 250);
|
timeout = setTimeout(showPreview, 250);
|
||||||
@@ -123,7 +130,7 @@ export default function addPostMentionPreviews() {
|
|||||||
timeout = setTimeout(hidePreview, 250);
|
timeout = setTimeout(hidePreview, 250);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.on('touchend', e => {
|
.on('touchend', (e) => {
|
||||||
showPreview();
|
showPreview();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
export default function cleanDisplayName(user) {
|
|
||||||
return user.displayName().replace(/"#[a-z]{0,3}[0-9]+/, '_');
|
|
||||||
};
|
|
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Whether to use the old mentions format.
|
||||||
|
*
|
||||||
|
* `'@username'` or `'@"Display name"'`
|
||||||
|
*/
|
||||||
|
export const shouldUseOldFormat = () => app.forum.attribute('allowUsernameMentionFormat') || false;
|
||||||
|
|
||||||
|
const getDeletedUserText = () => app.translator.trans('core.lib.username.deleted_text');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a user's username or display name.
|
||||||
|
*
|
||||||
|
* Chooses based on the format option set in the admin settings page.
|
||||||
|
*
|
||||||
|
* @param user An instance of the User model to fetch the username for
|
||||||
|
* @param useDisplayName If `true`, uses `user.displayName()`, otherwise, uses `user.username()`
|
||||||
|
*/
|
||||||
|
export default function getCleanDisplayName(user, useDisplayName = true) {
|
||||||
|
if (!user) return getDeletedUserText().replace(/"#[a-z]{0,3}[0-9]+/, '_');
|
||||||
|
|
||||||
|
const text = (useDisplayName ? user.displayName() : user.username()) || getDeletedUserText();
|
||||||
|
|
||||||
|
return text.replace(/"#[a-z]{0,3}[0-9]+/, '_');
|
||||||
|
}
|
36
extensions/mentions/js/src/forum/utils/getMentionText.js
Normal file
36
extensions/mentions/js/src/forum/utils/getMentionText.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import getCleanDisplayName, { ShouldUseOldFormat } from './getCleanDisplayName';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the mention text for a specified user (and optionally a post ID for replies).
|
||||||
|
*
|
||||||
|
* Automatically determines which mention syntax to be used based on the option in the
|
||||||
|
* admin dashboard. Also performs display name clean-up automatically.
|
||||||
|
*
|
||||||
|
* @example <caption>New display name syntax</caption>
|
||||||
|
* // '@"User"#1'
|
||||||
|
* getMentionText(User) // User is ID 1, display name is 'User'
|
||||||
|
*
|
||||||
|
* @example <caption>Replying</caption>
|
||||||
|
* // '@"User"#p13'
|
||||||
|
* getMentionText(User, 13) // User display name is 'User', post ID is 13
|
||||||
|
*
|
||||||
|
* @example <caption>Using old syntax</caption>
|
||||||
|
* // '@username'
|
||||||
|
* getMentionText(User) // User's username is 'username'
|
||||||
|
*/
|
||||||
|
export default function getMentionText(user, postId) {
|
||||||
|
if (postId === undefined) {
|
||||||
|
if (ShouldUseOldFormat()) {
|
||||||
|
// Plain @username
|
||||||
|
const cleanText = getCleanDisplayName(user, false);
|
||||||
|
return `@${cleanText}`;
|
||||||
|
}
|
||||||
|
// @"Display name"#UserID
|
||||||
|
const cleanText = getCleanDisplayName(user);
|
||||||
|
return `@"${cleanText}"${user.id()}`;
|
||||||
|
} else {
|
||||||
|
// @"Display name"#pPostID
|
||||||
|
const cleanText = getCleanDisplayName(user);
|
||||||
|
return `@"${cleanText}"#p${postId}`;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
import DiscussionControls from 'flarum/forum/utils/DiscussionControls';
|
||||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
|
||||||
import cleanDisplayName from './cleanDisplayName';
|
import getMentionText from './getMentionText';
|
||||||
|
|
||||||
function insertMention(post, composer, quote) {
|
function insertMention(post, composer, quote) {
|
||||||
const user = post.user();
|
const user = post.user();
|
||||||
const mention = `@"${(user && cleanDisplayName(user)) || app.translator.trans('core.lib.username.deleted_text')}"#p${post.id()} `;
|
const mention = getMentionText(user, post.id());
|
||||||
|
|
||||||
// If the composer is empty, then assume we're starting a new reply.
|
// 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
|
// In which case we don't want the user to have to confirm if they
|
||||||
@@ -19,9 +19,7 @@ function insertMention(post, composer, quote) {
|
|||||||
|
|
||||||
composer.editor.insertAtCursor(
|
composer.editor.insertAtCursor(
|
||||||
Array(precedingNewlines).join('\n') + // Insert up to two newlines, depending on preceding whitespace
|
Array(precedingNewlines).join('\n') + // Insert up to two newlines, depending on preceding whitespace
|
||||||
(quote
|
(quote ? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n' : mention),
|
||||||
? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n'
|
|
||||||
: mention),
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,7 +33,6 @@ export default function reply(post, quote) {
|
|||||||
// The default "Reply" action behavior will only open a new composer if
|
// The default "Reply" action behavior will only open a new composer if
|
||||||
// necessary, but it will always be a ReplyComposer, hence the exceptional
|
// necessary, but it will always be a ReplyComposer, hence the exceptional
|
||||||
// case above.
|
// case above.
|
||||||
DiscussionControls.replyAction.call(post.discussion())
|
DiscussionControls.replyAction.call(post.discussion()).then((composer) => insertMention(post, composer, quote));
|
||||||
.then(composer => insertMention(post, composer, quote));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Finds the selected text in the provided composer body.
|
||||||
|
*/
|
||||||
export default function selectedText(body) {
|
export default function selectedText(body) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.rangeCount) {
|
|
||||||
|
if (selection?.rangeCount) {
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
const parent = range.commonAncestorContainer;
|
const parent = range.commonAncestorContainer;
|
||||||
|
|
||||||
if (body[0] === parent || $.contains(body[0], parent)) {
|
if (body[0] === parent || $.contains(body[0], parent)) {
|
||||||
const clone = $("<div>").append(range.cloneContents());
|
const clone = $('<div>').append(range.cloneContents());
|
||||||
|
|
||||||
// Replace emoji images with their shortcode (found in alt attribute)
|
// Replace emoji images with their shortcode (found in alt attribute)
|
||||||
clone.find('img.emoji').replaceWith(function () {
|
clone.find('img.emoji').replaceWith(function () {
|
||||||
@@ -13,16 +18,16 @@ export default function selectedText(body) {
|
|||||||
|
|
||||||
// Replace all other images with a Markdown image
|
// Replace all other images with a Markdown image
|
||||||
clone.find('img').replaceWith(function () {
|
clone.find('img').replaceWith(function () {
|
||||||
return '';
|
return ``;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace all links with a Markdown link
|
// Replace all links with a Markdown link
|
||||||
clone.find('a').replaceWith(function () {
|
clone.find('a').replaceWith(function () {
|
||||||
return '[' + this.innerText + '](' + this.href + ')';
|
return `[${this.innerText}](${this.href})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return clone.text();
|
return clone.text();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
|
16
extensions/mentions/js/tsconfig.json
Normal file
16
extensions/mentions/js/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use Flarum's tsconfig as a starting point
|
||||||
|
"extends": "flarum-tsconfig",
|
||||||
|
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||||
|
// and also tells your Typescript server to read core's global typings for
|
||||||
|
// access to `dayjs` and `$` in the global namespace.
|
||||||
|
"include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
// This will output typings to `dist-typings`
|
||||||
|
"declarationDir": "./dist-typings",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user