From 2d9d4423a98ec25cff8cd8e7d8a688bea4e4b7dc Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 21 May 2022 22:51:52 +0800 Subject: [PATCH 1/3] Enable spellcheck for EasyMDE --- web_src/js/features/comp/EasyMDE.js | 2 ++ web_src/js/features/repo-wiki.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/web_src/js/features/comp/EasyMDE.js b/web_src/js/features/comp/EasyMDE.js index 61aaf23e89582..7c1db9a9988dc 100644 --- a/web_src/js/features/comp/EasyMDE.js +++ b/web_src/js/features/comp/EasyMDE.js @@ -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', '|', { diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js index 27f44f4e22e0e..4555b32e5fbb8 100644 --- a/web_src/js/features/repo-wiki.js +++ b/web_src/js/features/repo-wiki.js @@ -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', '|', { From d80abe75a5f12242f2accb899817d74d78570eeb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 23 Jun 2022 18:01:05 +0800 Subject: [PATCH 2/3] apply ImagePaste patch from @tyroneyeh --- web_src/js/features/comp/ImagePaste.js | 61 ++++++++++++-------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 79aeffa02b4b6..ff8a7f5ee00db 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -31,49 +31,44 @@ function clipboardPastedImages(e) { 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; + if (field.getTextArea().selectionStart || field.getTextArea().selectionStart === 0) { + const startPos = field.getTextArea().selectionStart; + const endPos = field.getTextArea().selectionEnd; + field.setValue(field.getValue().substring(0, startPos) + value + field.getValue().substring(endPos, field.getValue().length)); + field.getTextArea().electionStart = startPos + value.length; + field.getTextArea().selectionEnd = startPos + value.length; } else { - field.value += value; + field.setValue(field.getValue() + value); } } 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; + if (field.getTextArea().selectionStart || field.getTextArea().selectionStart === 0) { + const startPos = field.getTextArea().selectionStart; + const endPos = field.getTextArea().selectionEnd; + field.setValue(field.getValue().replace(oldval, newval)); + field.getTextArea().selectionStart = startPos + newval.length - oldval.length; + field.getTextArea().selectionEnd = endPos + newval.length - oldval.length; } else { - field.value = field.value.replace(oldval, newval); + field.setValue(field.getValue().replace(oldval, newval)); } } export function initCompImagePaste($target) { - $target.each(function () { - const dropzone = this.querySelector('.dropzone'); - if (!dropzone) { - 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 = $(``).val(data.uuid); - dropzoneFiles.appendChild(input[0]); - } - }, false); - } + const dropzone = $target[0].querySelector('.dropzone'); + if (!dropzone) { + return; + } + const uploadUrl = dropzone.getAttribute('data-upload-url'); + const dropzoneFiles = dropzone.querySelector('.files'); + $(document).on('paste', '.CodeMirror', async function (e) { + const img = clipboardPastedImages(e.originalEvent); + const name = img[0].name.substring(0, img[0].name.lastIndexOf('.')); + insertAtCursor(this.CodeMirror, `![${name}]()`); + const data = await uploadFile(img[0], uploadUrl); + replaceAndKeepCursor(this.CodeMirror, `![${name}]()`, `![${name}](/attachments/${data.uuid})`); + const input = $(``).val(data.uuid); + dropzoneFiles.appendChild(input[0]); }); } From 7b34b4f378166a94ad65b93e46e1c35b2b8f4b67 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 23 Jun 2022 20:33:19 +0800 Subject: [PATCH 3/3] fix image paste --- web_src/js/features/comp/ImagePaste.js | 132 ++++++++++++++++--------- web_src/js/features/repo-issue.js | 7 +- web_src/js/features/repo-legacy.js | 16 +-- web_src/js/features/repo-release.js | 5 +- 4 files changed, 99 insertions(+), 61 deletions(-) diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index ff8a7f5ee00db..da41e7611a620 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,4 +1,5 @@ import $ from 'jquery'; + const {csrfToken} = window.config; async function uploadFile(file, uploadUrl) { @@ -21,67 +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.getTextArea().selectionStart || field.getTextArea().selectionStart === 0) { - const startPos = field.getTextArea().selectionStart; - const endPos = field.getTextArea().selectionEnd; - field.setValue(field.getValue().substring(0, startPos) + value + field.getValue().substring(endPos, field.getValue().length)); - field.getTextArea().electionStart = startPos + value.length; - field.getTextArea().selectionEnd = startPos + value.length; - } else { - field.setValue(field.getValue() + 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(); } -} -function replaceAndKeepCursor(field, oldval, newval) { - if (field.getTextArea().selectionStart || field.getTextArea().selectionStart === 0) { - const startPos = field.getTextArea().selectionStart; - const endPos = field.getTextArea().selectionEnd; - field.setValue(field.getValue().replace(oldval, newval)); - field.getTextArea().selectionStart = startPos + newval.length - oldval.length; - field.getTextArea().selectionEnd = endPos + newval.length - oldval.length; - } else { - field.setValue(field.getValue().replace(oldval, newval)); + 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(); } } -export function initCompImagePaste($target) { - const dropzone = $target[0].querySelector('.dropzone'); - if (!dropzone) { - return; +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(); } - const uploadUrl = dropzone.getAttribute('data-upload-url'); - const dropzoneFiles = dropzone.querySelector('.files'); - $(document).on('paste', '.CodeMirror', async function (e) { - const img = clipboardPastedImages(e.originalEvent); - const name = img[0].name.substring(0, img[0].name.lastIndexOf('.')); - insertAtCursor(this.CodeMirror, `![${name}]()`); - const data = await uploadFile(img[0], uploadUrl); - replaceAndKeepCursor(this.CodeMirror, `![${name}]()`, `![${name}](/attachments/${data.uuid})`); - const input = $(``).val(data.uuid); - dropzoneFiles.appendChild(input[0]); - }); } -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)) { + +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; + } + e.preventDefault(); + e.stopPropagation(); + + 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 = $(``).val(data.uuid); - files.append(input); + editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`); + + const $input = $(``).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); }); } diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index bdd616f071838..12900c2455d85 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -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; @@ -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')); })(); } diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 6cdde6a1e4c27..0a04d0ceabb0e 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -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, @@ -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; } @@ -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) { @@ -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', () => { diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js index a44b91f35b0a9..b68a7a6cd5303 100644 --- a/web_src/js/features/repo-release.js +++ b/web_src/js/features/repo-release.js @@ -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); })(); }