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:
		
							parent
							
								
									cdd6371ad4
								
							
						
					
					
						commit
						76910f213f
					
				| @ -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', '|', { | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import $ from 'jquery'; | ||||
| 
 | ||||
| const {csrfToken} = window.config; | ||||
| 
 | ||||
| async function uploadFile(file, uploadUrl) { | ||||
| @ -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}]()`, ``); | ||||
|           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 = ``; | ||||
|       editor.insertPlaceholder(placeholder); | ||||
|       const data = await uploadFile(img, uploadUrl); | ||||
|       const pos = easyMDE.codemirror.getCursor(); | ||||
|       easyMDE.codemirror.replaceRange(``, pos); | ||||
|       const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); | ||||
|       files.append(input); | ||||
|       editor.replacePlaceholder(placeholder, ``); | ||||
| 
 | ||||
|       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); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @ -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')); | ||||
|     })(); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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) { | ||||
| @ -352,9 +354,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', () => { | ||||
|  | ||||
| @ -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); | ||||
|   })(); | ||||
| } | ||||
|  | ||||
| @ -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', '|', | ||||
|       { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user