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:
David Wheatley 2021-09-20 18:42:38 +01:00 committed by GitHub
parent 6a5d7e9864
commit 6173c3d612
11 changed files with 4346 additions and 2725 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,20 @@
{
"private": true,
"name": "@flarum/mentions",
"prettier": "@flarum/prettier-config",
"dependencies": {
"flarum-webpack-config": "0.1.0-beta.10",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
"flarum-webpack-config": "^1.0.0",
"webpack": "^4.46.0",
"webpack-cli": "^4.7.2"
},
"scripts": {
"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"
}
}

View File

@ -1,22 +1,42 @@
import { extend } from 'flarum/extend';
import TextEditor from 'flarum/components/TextEditor';
import TextEditorButton from 'flarum/components/TextEditorButton';
import ReplyComposer from 'flarum/components/ReplyComposer';
import EditPostComposer from 'flarum/components/EditPostComposer';
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 { extend } from 'flarum/common/extend';
import TextEditor from 'flarum/common/components/TextEditor';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
import avatar from 'flarum/common/helpers/avatar';
import usernameHelper from 'flarum/common/helpers/username';
import highlight from 'flarum/common/helpers/highlight';
import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable';
import { truncate } from 'flarum/common/utils/string';
import { throttle } from 'flarum/common/utils/throttleDebounce';
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() {
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
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>');
this.navigator = new KeyboardNavigatable();
@ -37,13 +57,12 @@ export default function addComposerAutocomplete() {
let absMentionStart;
let typed;
let matchTyped;
let searchTimeout;
// 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.
// 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 returnedUserIds = new Set(returnedUsers.map(u => u.id()));
const returnedUserIds = new Set(returnedUsers.map((u) => u.id()));
const applySuggestion = (replacement) => {
app.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
@ -51,177 +70,172 @@ export default function addComposerAutocomplete() {
dropdown.hide();
};
params.inputListeners.push(function(e) {
const selection = app.composer.editor.getSelectionRange();
params.inputListeners.push(function () {
const selection = app.composer.editor.getSelectionRange();
const cursor = selection[0];
const cursor = selection[0];
if (selection[1] - cursor > 0) return;
if (selection[1] - cursor > 0) return;
// Search backwards from the cursor for an '@' symbol. If we find one,
// we will want to show the autocomplete dropdown!
const lastChunk = app.composer.editor.getLastNChars(30);
absMentionStart = 0;
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
if (character === '@' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relMentionStart = i + 1;
absMentionStart = cursor - lastChunk.length + i + 1;
break;
}
// Search backwards from the cursor for an '@' symbol. If we find one,
// we will want to show the autocomplete dropdown!
const lastChunk = app.composer.editor.getLastNChars(30);
absMentionStart = 0;
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
if (character === '@' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relMentionStart = i + 1;
absMentionStart = cursor - lastChunk.length + i + 1;
break;
}
}
dropdown.hide();
dropdown.active = false;
dropdown.hide();
dropdown.active = false;
if (absMentionStart) {
typed = lastChunk.substring(relMentionStart).toLowerCase();
matchTyped = typed.match(/^["|“]((?:(?!"#).)+)$/);
typed = (matchTyped && matchTyped[1]) || typed;
if (absMentionStart) {
typed = lastChunk.substring(relMentionStart).toLowerCase();
matchTyped = typed.match(/^["|“]((?:(?!"#).)+)$/);
typed = (matchTyped && matchTyped[1]) || typed;
const makeSuggestion = function(user, replacement, content, className = '') {
const username = usernameHelper(user);
if (typed) {
username.children = [highlight(username.text, typed)];
delete username.text;
}
const makeSuggestion = function (user, replacement, content, className = '') {
const username = usernameHelper(user);
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>
);
};
if (typed) {
username.children = [highlight(username.text, typed)];
delete username.text;
}
const userMatches = function(user) {
const names = [
user.username(),
user.displayName()
];
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>
);
};
return names.some(name => name.toLowerCase().substr(0, typed.length) === typed);
};
const userMatches = function (user) {
const names = [user.username(), user.displayName()];
const buildSuggestions = () => {
const suggestions = [];
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
};
// If the user has started to type a username, then suggest users
// matching that username.
if (typed) {
returnedUsers.forEach(user => {
if (!userMatches(user)) return;
const buildSuggestions = () => {
const suggestions = [];
suggestions.push(
makeSuggestion(user, `@"${cleanDisplayName(user)}"#${user.id()}`, '', 'MentionsDropdown-user')
);
});
}
// If the user has started to type a username, then suggest users
// matching that username.
if (typed) {
returnedUsers.forEach((user) => {
if (!userMatches(user)) return;
// 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.
if (app.composer.bodyMatches(ReplyComposer) || app.composer.bodyMatches(EditPostComposer)) {
const composerAttrs = app.composer.body.attrs;
const composerPost = composerAttrs.post;
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
suggestions.push(makeSuggestion(user, getMentionText(user), '', 'MentionsDropdown-user'));
});
}
if (discussion) {
discussion.posts()
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
.sort((a, b) => b.createdAt() - a.createdAt())
.filter(post => {
const user = post.user();
return user && userMatches(user);
})
.splice(0, 5)
.forEach(post => {
const user = post.user();
suggestions.push(
makeSuggestion(user, `@"${cleanDisplayName(user)}"#p${post.id()}`, [
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', {number: post.number()}), ' — ',
truncate(post.contentPlain(), 200)
], 'MentionsDropdown-post')
);
});
}
}
// 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.
if (app.composer.bodyMatches(ReplyComposer) || app.composer.bodyMatches(EditPostComposer)) {
const composerAttrs = app.composer.body.attrs;
const composerPost = composerAttrs.post;
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
if (suggestions.length) {
dropdown.items = suggestions;
m.render($container[0], dropdown.render());
dropdown.show();
const coordinates = app.composer.editor.getCaretCoordinates(absMentionStart);
const width = dropdown.$().outerWidth();
const height = dropdown.$().outerHeight();
const parent = dropdown.$().offsetParent();
let left = coordinates.left;
let top = coordinates.top + 15;
// Keep the dropdown inside the editor.
if (top + height > parent.height()) {
top = coordinates.top - height - 15;
}
if (left + width > parent.width()) {
left = parent.width() - width;
}
// Prevent the dropdown from going off screen on mobile
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
left = Math.max(-parent.offset().left, left);
dropdown.show(left, top);
} else {
dropdown.active = false;
dropdown.hide();
}
};
dropdown.active = true;
buildSuggestions();
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
clearTimeout(searchTimeout);
// 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.
if (typed.length > 1) {
searchTimeout = setTimeout(function() {
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();
if (discussion) {
discussion
.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())
// Filter to where the user matches what is being typed
.filter((post) => {
const user = post.user();
return user && userMatches(user);
})
// Get the first 5
.splice(0, 5)
// Make the suggestions
.forEach((post) => {
const user = post.user();
suggestions.push(
makeSuggestion(
user,
getMentionText(user, post.id()),
[
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }),
' — ',
truncate(post.contentPlain(), 200),
],
'MentionsDropdown-post'
)
);
});
searched.push(typedLower);
}
}, 250);
}
}
if (suggestions.length) {
dropdown.items = suggestions;
m.render($container[0], dropdown.render());
dropdown.show();
const coordinates = app.composer.editor.getCaretCoordinates(absMentionStart);
const width = dropdown.$().outerWidth();
const height = dropdown.$().outerHeight();
const parent = dropdown.$().offsetParent();
let left = coordinates.left;
let top = coordinates.top + 15;
// Keep the dropdown inside the editor.
if (top + height > parent.height()) {
top = coordinates.top - height - 15;
}
if (left + width > parent.width()) {
left = parent.width() - width;
}
// Prevent the dropdown from going off screen on mobile
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
left = Math.max(-parent.offset().left, left);
dropdown.show(left, top);
} else {
dropdown.active = false;
dropdown.hide();
}
};
dropdown.active = true;
buildSuggestions();
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
// 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.
if (typed.length > 1) {
throttledSearch(typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions);
}
});
}
});
});
extend(TextEditor.prototype, 'toolbarItems', function(items) {
items.add('mention', (
extend(TextEditor.prototype, 'toolbarItems', function (items) {
items.add(
'mention',
<TextEditorButton onclick={() => this.attrs.composer.editor.insertAtCursor(' @')} icon="fas fa-at">
{app.translator.trans('flarum-mentions.forum.composer.mention_tooltip')}
</TextEditorButton>
));
);
});
}

View File

@ -1,12 +1,12 @@
import { extend } from 'flarum/extend';
import Model from 'flarum/Model';
import Post from 'flarum/models/Post';
import CommentPost from 'flarum/components/CommentPost';
import Link from 'flarum/components/Link';
import PostPreview from 'flarum/components/PostPreview';
import punctuateSeries from 'flarum/helpers/punctuateSeries';
import username from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon';
import { extend } from 'flarum/common/extend';
import Model from 'flarum/common/Model';
import Post from 'flarum/common/models/Post';
import CommentPost from 'flarum/forum/components/CommentPost';
import Link from 'flarum/common/components/Link';
import PostPreview from 'flarum/forum/components/PostPreview';
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
import username from 'flarum/common/helpers/username';
import icon from 'flarum/common/helpers/icon';
export default function addMentionedByList() {
Post.prototype.mentionedBy = Model.hasMany('mentionedBy');
@ -14,10 +14,12 @@ export default function addMentionedByList() {
function hidePreview() {
this.$('.Post-mentionedBy-preview')
.removeClass('in')
.one('transitionend', function() { $(this).hide(); });
.one('transitionend', function () {
$(this).hide();
});
}
extend(CommentPost.prototype, 'oncreate', function() {
extend(CommentPost.prototype, 'oncreate', function () {
let timeout;
const post = this.attrs.post;
const replies = post.mentionedBy();
@ -35,22 +37,26 @@ export default function addMentionedByList() {
// 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(reply => (
<li data-number={reply.number()}>
{PostPreview.component({
post: reply,
onclick: hidePreview.bind(this)
})}
</li>
)));
m.render(
$preview[0],
replies.map((reply) => (
<li data-number={reply.number()}>
{PostPreview.component({
post: reply,
onclick: hidePreview.bind(this),
})}
</li>
))
);
$preview.show()
$preview
.show()
.css('top', $this.offset().top - $parentPost.offset().top + $this.outerHeight(true))
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
.css('max-width', $parentPost.width());
setTimeout(() => $preview.off('transitionend').addClass('in'));
}
};
$this.add($preview).hover(
() => {
@ -66,23 +72,28 @@ export default function addMentionedByList() {
// 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('.Post-mentionedBy-summary a').hover(function() {
$preview.find('[data-number="' + $(this).data('number') + '"]').addClass('active');
}, function() {
$preview.find('[data-number]').removeClass('active');
});
this.$()
.find('.Post-mentionedBy-summary a')
.hover(
function () {
$preview.find('[data-number="' + $(this).data('number') + '"]').addClass('active');
},
function () {
$preview.find('[data-number]').removeClass('active');
}
);
}
});
extend(CommentPost.prototype, 'footerItems', function(items) {
extend(CommentPost.prototype, 'footerItems', function (items) {
const post = this.attrs.post;
const replies = post.mentionedBy();
if (replies && replies.length) {
const users = [];
const repliers = replies
.sort(reply => reply.user() === app.session.user ? -1 : 0)
.filter(reply => {
.sort((reply) => (reply.user() === app.session.user ? -1 : 0))
.filter((reply) => {
const user = reply.user();
if (users.indexOf(user) === -1) {
users.push(user);
@ -95,19 +106,15 @@ export default function addMentionedByList() {
// 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.
const names = repliers
.slice(0, overLimit ? limit - 1 : limit)
.map(reply => {
const user = reply.user();
const names = repliers.slice(0, overLimit ? limit - 1 : limit).map((reply) => {
const user = reply.user();
return (
<Link href={app.route.post(reply)}
onclick={hidePreview.bind(this)}
data-number={reply.number()}>
{app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}
</Link>
);
});
return (
<Link href={app.route.post(reply)} onclick={hidePreview.bind(this)} data-number={reply.number()}>
{app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}
</Link>
);
});
// If there are more users that we've run out of room to display, add a "x
// others" name to the end of the list. Clicking on it will display a modal
@ -115,18 +122,17 @@ export default function addMentionedByList() {
if (overLimit) {
const count = repliers.length - names.length;
names.push(
app.translator.trans('flarum-mentions.forum.post.others_text', {count})
);
names.push(app.translator.trans('flarum-mentions.forum.post.others_text', { count }));
}
items.add('replies',
items.add(
'replies',
<div className="Post-mentionedBy">
<span className="Post-mentionedBy-summary">
{icon('fas fa-reply')}
{app.translator.trans('flarum-mentions.forum.post.mentioned_by' + (repliers[0].user() === app.session.user ? '_self' : '') + '_text', {
count: names.length,
users: punctuateSeries(names)
users: punctuateSeries(names),
})}
</span>
</div>

View File

@ -1,7 +1,7 @@
import { extend } from 'flarum/extend';
import CommentPost from 'flarum/components/CommentPost';
import PostPreview from 'flarum/components/PostPreview';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import { extend } from 'flarum/common/extend';
import CommentPost from 'flarum/forum/components/CommentPost';
import PostPreview from 'flarum/forum/components/PostPreview';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
export default function addPostMentionPreviews() {
function addPreviews() {
@ -19,7 +19,7 @@ export default function addPostMentionPreviews() {
e.preventDefault();
});
this.$('.PostMention:not(.PostMention--deleted)').each(function() {
this.$('.PostMention:not(.PostMention--deleted)').each(function () {
const $this = $(this);
const id = $this.data('id');
let timeout;
@ -65,20 +65,25 @@ export default function addPostMentionPreviews() {
offset -= previewHeight;
}
$preview.show()
$preview
.show()
.css('top', $this.offset().top - $parentPost.offset().top + offset)
.css('left', $this.offsetParent().offset().left - $parentPost.offset().left)
.css('max-width', $this.offsetParent().width());
};
const showPost = post => {
const showPost = (post) => {
const discussion = post.discussion();
m.render($preview[0], [
discussion !== parentPost.discussion()
? <li><span className="PostMention-preview-discussion">{discussion.title()}</span></li>
: '',
<li>{PostPreview.component({post})}</li>
discussion !== parentPost.discussion() ? (
<li>
<span className="PostMention-preview-discussion">{discussion.title()}</span>
</li>
) : (
''
),
<li>{PostPreview.component({ post })}</li>,
]);
positionPreview();
};
@ -106,24 +111,26 @@ export default function addPostMentionPreviews() {
// 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.
// 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) {
e.preventDefault();
}
});
$this.add($preview).hover(
() => {
clearTimeout(timeout);
timeout = setTimeout(showPreview, 250);
},
() => {
clearTimeout(timeout);
getPostElement().removeClass('pulsate');
timeout = setTimeout(hidePreview, 250);
}
)
.on('touchend', e => {
$this
.add($preview)
.hover(
() => {
clearTimeout(timeout);
timeout = setTimeout(showPreview, 250);
},
() => {
clearTimeout(timeout);
getPostElement().removeClass('pulsate');
timeout = setTimeout(hidePreview, 250);
}
)
.on('touchend', (e) => {
showPreview();
e.stopPropagation();
});

View File

@ -1,3 +0,0 @@
export default function cleanDisplayName(user) {
return user.displayName().replace(/"#[a-z]{0,3}[0-9]+/, '_');
};

View File

@ -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]+/, '_');
}

View 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}`;
}
}

View File

@ -1,10 +1,10 @@
import DiscussionControls from 'flarum/utils/DiscussionControls';
import EditPostComposer from 'flarum/components/EditPostComposer';
import cleanDisplayName from './cleanDisplayName';
import DiscussionControls from 'flarum/forum/utils/DiscussionControls';
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
import getMentionText from './getMentionText';
function insertMention(post, composer, quote) {
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.
// 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(
Array(precedingNewlines).join('\n') + // Insert up to two newlines, depending on preceding whitespace
(quote
? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n'
: mention),
(quote ? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n' : mention),
false
);
}
@ -35,7 +33,6 @@ export default function reply(post, quote) {
// The default "Reply" action behavior will only open a new composer if
// necessary, but it will always be a ReplyComposer, hence the exceptional
// case above.
DiscussionControls.replyAction.call(post.discussion())
.then(composer => insertMention(post, composer, quote));
DiscussionControls.replyAction.call(post.discussion()).then((composer) => insertMention(post, composer, quote));
}
}

View File

@ -1,28 +1,33 @@
/**
* Finds the selected text in the provided composer body.
*/
export default function selectedText(body) {
const selection = window.getSelection();
if (selection.rangeCount) {
if (selection?.rangeCount) {
const range = selection.getRangeAt(0);
const parent = range.commonAncestorContainer;
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)
clone.find('img.emoji').replaceWith(function() {
clone.find('img.emoji').replaceWith(function () {
return this.alt;
});
// Replace all other images with a Markdown image
clone.find('img').replaceWith(function() {
return '![](' + this.src + ')';
clone.find('img').replaceWith(function () {
return `![](${this.src})`;
});
// Replace all links with a Markdown link
clone.find('a').replaceWith(function() {
return '[' + this.innerText + '](' + this.href + ')';
clone.find('a').replaceWith(function () {
return `[${this.innerText}](${this.href})`;
});
return clone.text();
}
}
return "";
return '';
}

View 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/*"]
}
}
}