Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable spellcheck for EasyMDE, use contenteditable mode #19776

Merged
merged 6 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web_src/js/features/comp/EasyMDE.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export async function createCommentEasyMDE(textarea, easyMDEOptions = {}) {
indentWithTabs: false,
tabSize: 4,
spellChecker: false,
inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
nativeSpellcheck: true,
toolbar: ['bold', 'italic', 'strikethrough', '|',
'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
'code', 'quote', '|', {
Expand Down
135 changes: 84 additions & 51 deletions web_src/js/features/comp/ImagePaste.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery';

const {csrfToken} = window.config;

async function uploadFile(file, uploadUrl) {
Expand All @@ -21,72 +22,104 @@ function clipboardPastedImages(e) {
if (!item.type || !item.type.startsWith('image/')) continue;
files.push(item.getAsFile());
}

if (files.length) {
e.preventDefault();
e.stopPropagation();
}
return files;
}

class TextareaEditor {
constructor(editor) {
this.editor = editor;
}

function insertAtCursor(field, value) {
if (field.selectionStart || field.selectionStart === 0) {
const startPos = field.selectionStart;
const endPos = field.selectionEnd;
field.value = field.value.substring(0, startPos) + value + field.value.substring(endPos, field.value.length);
field.selectionStart = startPos + value.length;
field.selectionEnd = startPos + value.length;
} else {
field.value += value;
insertPlaceholder(value) {
const editor = this.editor;
const startPos = editor.selectionStart;
const endPos = editor.selectionEnd;
editor.value = editor.value.substring(0, startPos) + value + editor.value.substring(endPos);
editor.selectionStart = startPos;
editor.selectionEnd = startPos + value.length;
editor.focus();
}

replacePlaceholder(oldVal, newVal) {
const editor = this.editor;
const startPos = editor.selectionStart;
const endPos = editor.selectionEnd;
if (editor.value.substring(startPos, endPos) === oldVal) {
editor.value = editor.value.substring(0, startPos) + newVal + editor.value.substring(endPos);
editor.selectionEnd = startPos + newVal.length;
} else {
editor.value = editor.value.replace(oldVal, newVal);
editor.selectionEnd -= oldVal.length;
editor.selectionEnd += newVal.length;
}
editor.selectionStart = editor.selectionEnd;
editor.focus();
}
}

function replaceAndKeepCursor(field, oldval, newval) {
if (field.selectionStart || field.selectionStart === 0) {
const startPos = field.selectionStart;
const endPos = field.selectionEnd;
field.value = field.value.replace(oldval, newval);
field.selectionStart = startPos + newval.length - oldval.length;
field.selectionEnd = endPos + newval.length - oldval.length;
} else {
field.value = field.value.replace(oldval, newval);
class CodeMirrorEditor {
constructor(editor) {
this.editor = editor;
}

insertPlaceholder(value) {
const editor = this.editor;
const startPoint = editor.getCursor('start');
const endPoint = editor.getCursor('end');
editor.replaceSelection(value);
endPoint.ch = startPoint.ch + value.length;
editor.setSelection(startPoint, endPoint);
editor.focus();
}

replacePlaceholder(oldVal, newVal) {
const editor = this.editor;
const endPoint = editor.getCursor('end');
if (editor.getSelection() === oldVal) {
editor.replaceSelection(newVal);
} else {
editor.setValue(editor.getValue().replace(oldVal, newVal));
}
endPoint.ch -= oldVal.length;
endPoint.ch += newVal.length;
editor.setSelection(endPoint, endPoint);
editor.focus();
}
}

export function initCompImagePaste($target) {
$target.each(function () {
const dropzone = this.querySelector('.dropzone');
if (!dropzone) {

export function initEasyMDEImagePaste(easyMDE, $dropzone) {
const uploadUrl = $dropzone.attr('data-upload-url');
const $files = $dropzone.find('.files');

if (!uploadUrl || !$files.length) return;

const uploadClipboardImage = async (editor, e) => {
const pastedImages = clipboardPastedImages(e);
if (!pastedImages || pastedImages.length === 0) {
return;
}
const uploadUrl = dropzone.getAttribute('data-upload-url');
const dropzoneFiles = dropzone.querySelector('.files');
for (const textarea of this.querySelectorAll('textarea')) {
textarea.addEventListener('paste', async (e) => {
for (const img of clipboardPastedImages(e)) {
const name = img.name.slice(0, img.name.lastIndexOf('.'));
insertAtCursor(textarea, `![${name}]()`);
const data = await uploadFile(img, uploadUrl);
replaceAndKeepCursor(textarea, `![${name}]()`, `![${name}](/attachments/${data.uuid})`);
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
dropzoneFiles.appendChild(input[0]);
}
}, false);
}
});
}
e.preventDefault();
e.stopPropagation();

export function initEasyMDEImagePaste(easyMDE, dropzone, files) {
const uploadUrl = dropzone.getAttribute('data-upload-url');
easyMDE.codemirror.on('paste', async (_, e) => {
for (const img of clipboardPastedImages(e)) {
for (const img of pastedImages) {
const name = img.name.slice(0, img.name.lastIndexOf('.'));

const placeholder = `![${name}](uploading ...)`;
editor.insertPlaceholder(placeholder);
const data = await uploadFile(img, uploadUrl);
const pos = easyMDE.codemirror.getCursor();
easyMDE.codemirror.replaceRange(`![${name}](/attachments/${data.uuid})`, pos);
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
files.append(input);
editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);

const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
$files.append($input);
}
};

easyMDE.codemirror.on('paste', async (_, e) => {
return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), e);
});

$(easyMDE.element).on('paste', async (e) => {
return uploadClipboardImage(new TextareaEditor(easyMDE.element), e.originalEvent);
});
}
7 changes: 4 additions & 3 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import $ from 'jquery';
import {htmlEscape} from 'escape-goat';
import attachTribute from './tribute.js';
import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
import {initCompImagePaste} from './comp/ImagePaste.js';
import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';

const {appSubUrl, csrfToken} = window.config;
Expand Down Expand Up @@ -480,8 +480,9 @@ export function initRepoPullRequestReview() {
// the editor's height is too large in some cases, and the panel cannot be scrolled with page now because there is `.repository .diff-detail-box.sticky { position: sticky; }`
// the temporary solution is to make the editor's height smaller (about 4 lines). GitHub also only show 4 lines for default. We can improve the UI (including Dropzone area) in future
// EasyMDE's options can not handle minHeight & maxHeight together correctly, we have to set max-height for .CodeMirror-scroll in CSS.
await createCommentEasyMDE($reviewBox.find('textarea'), {minHeight: '80px'});
initCompImagePaste($reviewBox);
const $reviewTextarea = $reviewBox.find('textarea');
const easyMDE = await createCommentEasyMDE($reviewTextarea, {minHeight: '80px'});
initEasyMDEImagePaste(easyMDE, $reviewBox.find('.dropzone'));
})();
}

Expand Down
16 changes: 8 additions & 8 deletions web_src/js/features/repo-legacy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import $ from 'jquery';
import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete,
Expand Down Expand Up @@ -33,7 +33,8 @@ import initRepoPullRequestMergeForm from './repo-issue-pr-form.js';
const {csrfToken} = window.config;

export function initRepoCommentForm() {
if ($('.comment.form').length === 0) {
const $commentForm = $('.comment.form');
if ($commentForm.length === 0) {
return;
}

Expand Down Expand Up @@ -67,12 +68,13 @@ export function initRepoCommentForm() {
}

(async () => {
await createCommentEasyMDE($('.comment.form textarea:not(.review-textarea)'));
initCompImagePaste($('.comment.form'));
const $textarea = $commentForm.find('textarea:not(.review-textarea)');
const easyMDE = await createCommentEasyMDE($textarea);
initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
})();

initBranchSelector();
initCompMarkupContentPreviewTab($('.comment.form'));
initCompMarkupContentPreviewTab($commentForm);

// List submits
function initListSubmits(selector, outerSelector) {
Expand Down Expand Up @@ -351,9 +353,7 @@ async function onEditContent(event) {
easyMDE = await createCommentEasyMDE($textarea);

initCompMarkupContentPreviewTab($editContentForm);
if ($dropzone.length === 1) {
initEasyMDEImagePaste(easyMDE, $dropzone[0], $dropzone.find('.files'));
}
initEasyMDEImagePaste(easyMDE, $dropzone);

const $saveButton = $editContentZone.find('.save.button');
$textarea.on('ce-quick-submit', () => {
Expand Down
5 changes: 2 additions & 3 deletions web_src/js/features/repo-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ export function initRepoReleaseEditor() {
(async () => {
const $textarea = $editor.find('textarea');
await attachTribute($textarea.get(), {mentions: false, emoji: true});
const $files = $editor.parent().find('.files');
const easyMDE = await createCommentEasyMDE($textarea);
initCompMarkupContentPreviewTab($editor);
const dropzone = $editor.parent().find('.dropzone')[0];
initEasyMDEImagePaste(easyMDE, dropzone, $files);
const $dropzone = $editor.parent().find('.dropzone');
initEasyMDEImagePaste(easyMDE, $dropzone);
})();
}
2 changes: 2 additions & 0 deletions web_src/js/features/repo-wiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ async function initRepoWikiFormEditor() {
indentWithTabs: false,
tabSize: 4,
spellChecker: false,
inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
nativeSpellcheck: true,
toolbar: ['bold', 'italic', 'strikethrough', '|',
'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
{
Expand Down