mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Enable spellcheck for EasyMDE, use contenteditable mode (#19776)
Enable spellcheck for EasyMDE, use contenteditable mode. Rewrite and refactor the ImagePaste code.
This commit is contained in:
		| @@ -38,6 +38,8 @@ export async function createCommentEasyMDE(textarea, easyMDEOptions = {}) { | |||||||
|     indentWithTabs: false, |     indentWithTabs: false, | ||||||
|     tabSize: 4, |     tabSize: 4, | ||||||
|     spellChecker: false, |     spellChecker: false, | ||||||
|  |     inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable | ||||||
|  |     nativeSpellcheck: true, | ||||||
|     toolbar: ['bold', 'italic', 'strikethrough', '|', |     toolbar: ['bold', 'italic', 'strikethrough', '|', | ||||||
|       'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', |       'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', | ||||||
|       'code', 'quote', '|', { |       'code', 'quote', '|', { | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
|  |  | ||||||
| const {csrfToken} = window.config; | const {csrfToken} = window.config; | ||||||
|  |  | ||||||
| async function uploadFile(file, uploadUrl) { | async function uploadFile(file, uploadUrl) { | ||||||
| @@ -21,72 +22,104 @@ function clipboardPastedImages(e) { | |||||||
|     if (!item.type || !item.type.startsWith('image/')) continue; |     if (!item.type || !item.type.startsWith('image/')) continue; | ||||||
|     files.push(item.getAsFile()); |     files.push(item.getAsFile()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (files.length) { |  | ||||||
|     e.preventDefault(); |  | ||||||
|     e.stopPropagation(); |  | ||||||
|   } |  | ||||||
|   return files; |   return files; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class TextareaEditor { | ||||||
|  |   constructor(editor) { | ||||||
|  |     this.editor = editor; | ||||||
|  |   } | ||||||
|  |  | ||||||
| function insertAtCursor(field, value) { |   insertPlaceholder(value) { | ||||||
|   if (field.selectionStart || field.selectionStart === 0) { |     const editor = this.editor; | ||||||
|     const startPos = field.selectionStart; |     const startPos = editor.selectionStart; | ||||||
|     const endPos = field.selectionEnd; |     const endPos = editor.selectionEnd; | ||||||
|     field.value = field.value.substring(0, startPos) + value + field.value.substring(endPos, field.value.length); |     editor.value = editor.value.substring(0, startPos) + value + editor.value.substring(endPos); | ||||||
|     field.selectionStart = startPos + value.length; |     editor.selectionStart = startPos; | ||||||
|     field.selectionEnd = startPos + value.length; |     editor.selectionEnd = startPos + value.length; | ||||||
|   } else { |     editor.focus(); | ||||||
|     field.value += value; |   } | ||||||
|  |  | ||||||
|  |   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) { | class CodeMirrorEditor { | ||||||
|   if (field.selectionStart || field.selectionStart === 0) { |   constructor(editor) { | ||||||
|     const startPos = field.selectionStart; |     this.editor = editor; | ||||||
|     const endPos = field.selectionEnd; |   } | ||||||
|     field.value = field.value.replace(oldval, newval); |  | ||||||
|     field.selectionStart = startPos + newval.length - oldval.length; |   insertPlaceholder(value) { | ||||||
|     field.selectionEnd = endPos + newval.length - oldval.length; |     const editor = this.editor; | ||||||
|   } else { |     const startPoint = editor.getCursor('start'); | ||||||
|     field.value = field.value.replace(oldval, newval); |     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 () { | export function initEasyMDEImagePaste(easyMDE, $dropzone) { | ||||||
|     const dropzone = this.querySelector('.dropzone'); |   const uploadUrl = $dropzone.attr('data-upload-url'); | ||||||
|     if (!dropzone) { |   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; |       return; | ||||||
|     } |     } | ||||||
|     const uploadUrl = dropzone.getAttribute('data-upload-url'); |     e.preventDefault(); | ||||||
|     const dropzoneFiles = dropzone.querySelector('.files'); |     e.stopPropagation(); | ||||||
|     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}]()`, ``); |  | ||||||
|           const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); |  | ||||||
|           dropzoneFiles.appendChild(input[0]); |  | ||||||
|         } |  | ||||||
|       }, false); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function initEasyMDEImagePaste(easyMDE, dropzone, files) { |     for (const img of pastedImages) { | ||||||
|   const uploadUrl = dropzone.getAttribute('data-upload-url'); |  | ||||||
|   easyMDE.codemirror.on('paste', async (_, e) => { |  | ||||||
|     for (const img of clipboardPastedImages(e)) { |  | ||||||
|       const name = img.name.slice(0, img.name.lastIndexOf('.')); |       const name = img.name.slice(0, img.name.lastIndexOf('.')); | ||||||
|  |  | ||||||
|  |       const placeholder = ``; | ||||||
|  |       editor.insertPlaceholder(placeholder); | ||||||
|       const data = await uploadFile(img, uploadUrl); |       const data = await uploadFile(img, uploadUrl); | ||||||
|       const pos = easyMDE.codemirror.getCursor(); |       editor.replacePlaceholder(placeholder, ``); | ||||||
|       easyMDE.codemirror.replaceRange(``, pos); |  | ||||||
|       const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); |       const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid); | ||||||
|       files.append(input); |       $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); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import $ from 'jquery'; | |||||||
| import {htmlEscape} from 'escape-goat'; | import {htmlEscape} from 'escape-goat'; | ||||||
| import attachTribute from './tribute.js'; | import attachTribute from './tribute.js'; | ||||||
| import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.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'; | import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js'; | ||||||
|  |  | ||||||
| const {appSubUrl, csrfToken} = window.config; | 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 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 |       // 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. |       // 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'}); |       const $reviewTextarea = $reviewBox.find('textarea'); | ||||||
|       initCompImagePaste($reviewBox); |       const easyMDE = await createCommentEasyMDE($reviewTextarea, {minHeight: '80px'}); | ||||||
|  |       initEasyMDEImagePaste(easyMDE, $reviewBox.find('.dropzone')); | ||||||
|     })(); |     })(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
| import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js'; | import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js'; | ||||||
| import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js'; | import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js'; | ||||||
| import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js'; | import {initEasyMDEImagePaste} from './comp/ImagePaste.js'; | ||||||
| import { | import { | ||||||
|   initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, |   initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, | ||||||
|   initRepoIssueCommentDelete, |   initRepoIssueCommentDelete, | ||||||
| @@ -33,7 +33,8 @@ import initRepoPullRequestMergeForm from './repo-issue-pr-form.js'; | |||||||
| const {csrfToken} = window.config; | const {csrfToken} = window.config; | ||||||
|  |  | ||||||
| export function initRepoCommentForm() { | export function initRepoCommentForm() { | ||||||
|   if ($('.comment.form').length === 0) { |   const $commentForm = $('.comment.form'); | ||||||
|  |   if ($commentForm.length === 0) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -67,12 +68,13 @@ export function initRepoCommentForm() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   (async () => { |   (async () => { | ||||||
|     await createCommentEasyMDE($('.comment.form textarea:not(.review-textarea)')); |     const $textarea = $commentForm.find('textarea:not(.review-textarea)'); | ||||||
|     initCompImagePaste($('.comment.form')); |     const easyMDE = await createCommentEasyMDE($textarea); | ||||||
|  |     initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone')); | ||||||
|   })(); |   })(); | ||||||
|  |  | ||||||
|   initBranchSelector(); |   initBranchSelector(); | ||||||
|   initCompMarkupContentPreviewTab($('.comment.form')); |   initCompMarkupContentPreviewTab($commentForm); | ||||||
|  |  | ||||||
|   // List submits |   // List submits | ||||||
|   function initListSubmits(selector, outerSelector) { |   function initListSubmits(selector, outerSelector) { | ||||||
| @@ -352,9 +354,7 @@ async function onEditContent(event) { | |||||||
|     easyMDE = await createCommentEasyMDE($textarea); |     easyMDE = await createCommentEasyMDE($textarea); | ||||||
|  |  | ||||||
|     initCompMarkupContentPreviewTab($editContentForm); |     initCompMarkupContentPreviewTab($editContentForm); | ||||||
|     if ($dropzone.length === 1) { |     initEasyMDEImagePaste(easyMDE, $dropzone); | ||||||
|       initEasyMDEImagePaste(easyMDE, $dropzone[0], $dropzone.find('.files')); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const $saveButton = $editContentZone.find('.save.button'); |     const $saveButton = $editContentZone.find('.save.button'); | ||||||
|     $textarea.on('ce-quick-submit', () => { |     $textarea.on('ce-quick-submit', () => { | ||||||
|   | |||||||
| @@ -23,10 +23,9 @@ export function initRepoReleaseEditor() { | |||||||
|   (async () => { |   (async () => { | ||||||
|     const $textarea = $editor.find('textarea'); |     const $textarea = $editor.find('textarea'); | ||||||
|     await attachTribute($textarea.get(), {mentions: false, emoji: true}); |     await attachTribute($textarea.get(), {mentions: false, emoji: true}); | ||||||
|     const $files = $editor.parent().find('.files'); |  | ||||||
|     const easyMDE = await createCommentEasyMDE($textarea); |     const easyMDE = await createCommentEasyMDE($textarea); | ||||||
|     initCompMarkupContentPreviewTab($editor); |     initCompMarkupContentPreviewTab($editor); | ||||||
|     const dropzone = $editor.parent().find('.dropzone')[0]; |     const $dropzone = $editor.parent().find('.dropzone'); | ||||||
|     initEasyMDEImagePaste(easyMDE, dropzone, $files); |     initEasyMDEImagePaste(easyMDE, $dropzone); | ||||||
|   })(); |   })(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -67,6 +67,8 @@ async function initRepoWikiFormEditor() { | |||||||
|     indentWithTabs: false, |     indentWithTabs: false, | ||||||
|     tabSize: 4, |     tabSize: 4, | ||||||
|     spellChecker: false, |     spellChecker: false, | ||||||
|  |     inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable | ||||||
|  |     nativeSpellcheck: true, | ||||||
|     toolbar: ['bold', 'italic', 'strikethrough', '|', |     toolbar: ['bold', 'italic', 'strikethrough', '|', | ||||||
|       'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', |       'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', | ||||||
|       { |       { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user