feat: Add partial quoting
- If you select a portion of the comment, `Quote reply` will not only quote that portion and not copy paste the whole text as it previously did. This is achieved by using the `@github/quote-selection` package. - There's preprocessing to ensure Forgejo-flavored markdown syntax is preserved. - e2e test added. - Resolves #1342
This commit is contained in:
parent
8b7410f35c
commit
2c2ac80030
21 changed files with 303 additions and 68 deletions
|
@ -27,6 +27,8 @@ import {hideElem, showElem} from '../utils/dom.js';
|
|||
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
|
||||
import {attachRefIssueContextPopup} from './contextpopup.js';
|
||||
import {POST, GET} from '../modules/fetch.js';
|
||||
import {MarkdownQuote} from '@github/quote-selection';
|
||||
import {toAbsoluteUrl} from '../utils.js';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
|
@ -579,32 +581,105 @@ export function initRepository() {
|
|||
initUnicodeEscapeButton();
|
||||
}
|
||||
|
||||
const filters = {
|
||||
A(el) {
|
||||
if (el.classList.contains('mention') || el.classList.contains('ref-issue')) {
|
||||
return el.textContent;
|
||||
}
|
||||
return el;
|
||||
},
|
||||
PRE(el) {
|
||||
const firstChild = el.children[0];
|
||||
if (firstChild && el.classList.contains('code-block')) {
|
||||
// Get the language of the codeblock.
|
||||
const language = firstChild.className.match(/language-(\S+)/);
|
||||
// Remove trailing newlines.
|
||||
const text = el.textContent.replace(/\n+$/, '');
|
||||
el.textContent = `\`\`\`${language[1]}\n${text}\n\`\`\`\n\n`;
|
||||
}
|
||||
return el;
|
||||
},
|
||||
SPAN(el) {
|
||||
const emojiAlias = el.getAttribute('data-alias');
|
||||
if (emojiAlias && el.classList.contains('emoji')) {
|
||||
return `:${emojiAlias}:`;
|
||||
}
|
||||
if (el.classList.contains('katex')) {
|
||||
const texCode = el.querySelector('annotation[encoding="application/x-tex"]').textContent;
|
||||
if (el.parentElement.classList.contains('katex-display')) {
|
||||
el.textContent = `\\[${texCode}\\]\n\n`;
|
||||
} else {
|
||||
el.textContent = `\\(${texCode}\\)\n\n`;
|
||||
}
|
||||
}
|
||||
return el;
|
||||
},
|
||||
};
|
||||
|
||||
function hasContent(node) {
|
||||
return node.nodeName === 'IMG' || node.firstChild !== null;
|
||||
}
|
||||
|
||||
// This code matches that of what is done by @github/quote-selection
|
||||
function preprocessFragment(fragment) {
|
||||
const nodeIterator = document.createNodeIterator(fragment, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
if (node.nodeName in filters && hasContent(node)) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
|
||||
return NodeFilter.FILTER_SKIP;
|
||||
},
|
||||
});
|
||||
const results = [];
|
||||
let node = nodeIterator.nextNode();
|
||||
|
||||
while (node) {
|
||||
if (node instanceof HTMLElement) {
|
||||
results.push(node);
|
||||
}
|
||||
node = nodeIterator.nextNode();
|
||||
}
|
||||
|
||||
// process deepest matches first
|
||||
results.reverse();
|
||||
|
||||
for (const el of results) {
|
||||
el.replaceWith(filters[el.nodeName](el));
|
||||
}
|
||||
}
|
||||
|
||||
function initRepoIssueCommentEdit() {
|
||||
// Edit issue or comment content
|
||||
$(document).on('click', '.edit-content', onEditContent);
|
||||
|
||||
// Quote reply
|
||||
$(document).on('click', '.quote-reply', async function (event) {
|
||||
$(document).on('click', '.quote-reply', async (event) => {
|
||||
event.preventDefault();
|
||||
const target = $(this).data('target');
|
||||
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
|
||||
const content = `> ${quote}\n\n`;
|
||||
let editor;
|
||||
if (this.classList.contains('quote-reply-diff')) {
|
||||
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
|
||||
editor = await handleReply($replyBtn);
|
||||
const quote = new MarkdownQuote('', preprocessFragment);
|
||||
|
||||
let editorTextArea;
|
||||
if (event.target.classList.contains('quote-reply-diff')) {
|
||||
// Temporarily store the range so it doesn't get lost (likely caused by async code).
|
||||
const currentRange = quote.range;
|
||||
|
||||
const replyButton = event.target.closest('.comment-code-cloud').querySelector('button.comment-form-reply');
|
||||
editorTextArea = (await handleReply($(replyButton))).textarea;
|
||||
|
||||
quote.range = currentRange;
|
||||
} else {
|
||||
// for normal issue/comment page
|
||||
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
|
||||
editorTextArea = document.querySelector('#comment-form .combo-markdown-editor textarea');
|
||||
}
|
||||
if (editor) {
|
||||
if (editor.value()) {
|
||||
editor.value(`${editor.value()}\n\n${content}`);
|
||||
} else {
|
||||
editor.value(content);
|
||||
}
|
||||
editor.focus();
|
||||
editor.moveCursorToEnd();
|
||||
|
||||
// Select the whole comment body if there's no selection.
|
||||
if (quote.range.collapsed) {
|
||||
quote.select(document.querySelector(`#${event.target.getAttribute('data-target')}`));
|
||||
}
|
||||
|
||||
// If the selection is in the comment body, then insert the quote.
|
||||
if (quote.closest(`#${event.target.getAttribute('data-target')}`)) {
|
||||
editorTextArea.value += `@${event.target.getAttribute('data-author')} wrote in ${toAbsoluteUrl(event.target.getAttribute('data-reference-url'))}:`;
|
||||
quote.insert(editorTextArea);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue