mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	Introduce GitHub markdown editor, keep EasyMDE as fallback (#23876)
The first step of the plan * #23290 Thanks to @silverwind for the first try in #15394 . Close #10729 and a lot of related issues. The EasyMDE is not removed, now it works as a fallback, users can switch between these two editors. Editor list: * Issue / PR comment * Issue / PR comment edit * Issue / PR comment quote reply * PR diff view, inline comment * PR diff view, inline comment edit * PR diff view, inline comment quote reply * Release editor * Wiki editor Some editors have attached dropzone Screenshots: <details>     </details> --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
		
							
								
								
									
										277
									
								
								web_src/js/features/comp/ComboMarkdownEditor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								web_src/js/features/comp/ComboMarkdownEditor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| import '@github/markdown-toolbar-element'; | ||||
| import {attachTribute} from '../tribute.js'; | ||||
| import {hideElem, showElem} from '../../utils/dom.js'; | ||||
| import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js'; | ||||
| import $ from 'jquery'; | ||||
| import {initMarkupContent} from '../../markup/content.js'; | ||||
| import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | ||||
| import {attachRefIssueContextPopup} from '../contextpopup.js'; | ||||
|  | ||||
| let elementIdCounter = 0; | ||||
|  | ||||
| /** | ||||
|  * validate if the given textarea is non-empty. | ||||
|  * @param {jQuery} $textarea | ||||
|  * @returns {boolean} returns true if validation succeeded. | ||||
|  */ | ||||
| export function validateTextareaNonEmpty($textarea) { | ||||
|   // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation. | ||||
|   // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert. | ||||
|   if (!$textarea.val()) { | ||||
|     if ($textarea.is(':visible')) { | ||||
|       $textarea.prop('required', true); | ||||
|       const $form = $textarea.parents('form'); | ||||
|       $form[0]?.reportValidity(); | ||||
|     } else { | ||||
|       // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places. | ||||
|       alert('Require non-empty content'); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| class ComboMarkdownEditor { | ||||
|   constructor(container, options = {}) { | ||||
|     container._giteaComboMarkdownEditor = this; | ||||
|     this.options = options; | ||||
|     this.container = container; | ||||
|   } | ||||
|  | ||||
|   async init() { | ||||
|     this.textarea = this.container.querySelector('.markdown-text-editor'); | ||||
|     this.textarea._giteaComboMarkdownEditor = this; | ||||
|     this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter)}`; | ||||
|     this.textarea.addEventListener('input', (e) => {this.options?.onContentChanged?.(this, e)}); | ||||
|     this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar'); | ||||
|     this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id); | ||||
|  | ||||
|     elementIdCounter++; | ||||
|  | ||||
|     this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde'); | ||||
|     this.switchToEasyMDEButton?.addEventListener('click', async (e) => { | ||||
|       e.preventDefault(); | ||||
|       await this.switchToEasyMDE(); | ||||
|     }); | ||||
|  | ||||
|     await attachTribute(this.textarea, {mentions: true, emoji: true}); | ||||
|  | ||||
|     const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); | ||||
|     if (dropzoneParentContainer) { | ||||
|       this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone'); | ||||
|       initTextareaImagePaste(this.textarea, this.dropzone); | ||||
|     } | ||||
|  | ||||
|     this.setupTab(); | ||||
|     this.prepareEasyMDEToolbarActions(); | ||||
|   } | ||||
|  | ||||
|   setupTab() { | ||||
|     const $container = $(this.container); | ||||
|     const $tabMenu = $container.find('.tabular.menu'); | ||||
|     const $tabs = $tabMenu.find('> .item'); | ||||
|  | ||||
|     // Fomantic Tab requires the "data-tab" to be globally unique. | ||||
|     // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. | ||||
|     const $tabEditor = $tabs.filter(`.item[data-tab-for="markdown-writer"]`); | ||||
|     const $tabPreviewer = $tabs.filter(`.item[data-tab-for="markdown-previewer"]`); | ||||
|     $tabEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`); | ||||
|     $tabPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`); | ||||
|     const $panelEditor = $container.find('.ui.tab[data-tab-panel="markdown-writer"]'); | ||||
|     const $panelPreviewer = $container.find('.ui.tab[data-tab-panel="markdown-previewer"]'); | ||||
|     $panelEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`); | ||||
|     $panelPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`); | ||||
|     elementIdCounter++; | ||||
|  | ||||
|     $tabs.tab(); | ||||
|  | ||||
|     this.previewUrl = $tabPreviewer.attr('data-preview-url'); | ||||
|     this.previewContext = $tabPreviewer.attr('data-preview-context'); | ||||
|     this.previewMode = this.options.previewMode ?? 'comment'; | ||||
|     this.previewWiki = this.options.previewWiki ?? false; | ||||
|     $tabPreviewer.on('click', () => { | ||||
|       $.post(this.previewUrl, { | ||||
|         _csrf: window.config.csrfToken, | ||||
|         mode: this.previewMode, | ||||
|         context: this.previewContext, | ||||
|         text: this.value(), | ||||
|         wiki: this.previewWiki, | ||||
|       }, (data) => { | ||||
|         $panelPreviewer.html(data); | ||||
|         initMarkupContent(); | ||||
|  | ||||
|         const refIssues = $panelPreviewer.find('p .ref-issue'); | ||||
|         attachRefIssueContextPopup(refIssues); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   prepareEasyMDEToolbarActions() { | ||||
|     this.easyMDEToolbarDefault = [ | ||||
|       'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', | ||||
|       'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|', | ||||
|       'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', 'horizontal-rule', '|', 'clean-block', '|', | ||||
|       'gitea-switch-to-textarea', | ||||
|     ]; | ||||
|  | ||||
|     this.easyMDEToolbarActions = { | ||||
|       'gitea-checkbox-empty': { | ||||
|         action(e) { | ||||
|           const cm = e.codemirror; | ||||
|           cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`); | ||||
|           cm.focus(); | ||||
|         }, | ||||
|         className: 'fa fa-square-o', | ||||
|         title: 'Add Checkbox (empty)', | ||||
|       }, | ||||
|       'gitea-checkbox-checked': { | ||||
|         action(e) { | ||||
|           const cm = e.codemirror; | ||||
|           cm.replaceSelection(`\n- [x] ${cm.getSelection()}`); | ||||
|           cm.focus(); | ||||
|         }, | ||||
|         className: 'fa fa-check-square-o', | ||||
|         title: 'Add Checkbox (checked)', | ||||
|       }, | ||||
|       'gitea-switch-to-textarea': { | ||||
|         action: this.switchToTextarea.bind(this), | ||||
|         className: 'fa fa-file', | ||||
|         title: 'Revert to simple textarea', | ||||
|       }, | ||||
|       'gitea-code-inline': { | ||||
|         action(e) { | ||||
|           const cm = e.codemirror; | ||||
|           const selection = cm.getSelection(); | ||||
|           cm.replaceSelection(`\`${selection}\``); | ||||
|           if (!selection) { | ||||
|             const cursorPos = cm.getCursor(); | ||||
|             cm.setCursor(cursorPos.line, cursorPos.ch - 1); | ||||
|           } | ||||
|           cm.focus(); | ||||
|         }, | ||||
|         className: 'fa fa-angle-right', | ||||
|         title: 'Add Inline Code', | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   parseEasyMDEToolbar(actions) { | ||||
|     const processed = []; | ||||
|     for (const action of actions) { | ||||
|       if (action.startsWith('gitea-')) { | ||||
|         const giteaAction = this.easyMDEToolbarActions[action]; | ||||
|         if (!giteaAction) throw new Error(`Unknown EasyMDE toolbar action ${action}`); | ||||
|         processed.push(giteaAction); | ||||
|       } else { | ||||
|         processed.push(action); | ||||
|       } | ||||
|     } | ||||
|     return processed; | ||||
|   } | ||||
|  | ||||
|   async switchToTextarea() { | ||||
|     showElem(this.textareaMarkdownToolbar); | ||||
|     if (this.easyMDE) { | ||||
|       this.easyMDE.toTextArea(); | ||||
|       this.easyMDE = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async switchToEasyMDE() { | ||||
|     // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. | ||||
|     const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); | ||||
|     const easyMDEOpt = { | ||||
|       autoDownloadFontAwesome: false, | ||||
|       element: this.textarea, | ||||
|       forceSync: true, | ||||
|       renderingConfig: {singleLineBreaks: false}, | ||||
|       indentWithTabs: false, | ||||
|       tabSize: 4, | ||||
|       spellChecker: false, | ||||
|       inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable | ||||
|       nativeSpellcheck: true, | ||||
|       ...this.options.easyMDEOptions, | ||||
|     }; | ||||
|     easyMDEOpt.toolbar = this.parseEasyMDEToolbar(easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault); | ||||
|  | ||||
|     this.easyMDE = new EasyMDE(easyMDEOpt); | ||||
|     this.easyMDE.codemirror.on('change', (...args) => {this.options?.onContentChanged?.(this, ...args)}); | ||||
|     this.easyMDE.codemirror.setOption('extraKeys', { | ||||
|       'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||
|       'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||
|       Enter: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           cm.execCommand('newlineAndIndent'); | ||||
|         } | ||||
|       }, | ||||
|       Up: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           return cm.execCommand('goLineUp'); | ||||
|         } | ||||
|       }, | ||||
|       Down: (cm) => { | ||||
|         const tributeContainer = document.querySelector('.tribute-container'); | ||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|           return cm.execCommand('goLineDown'); | ||||
|         } | ||||
|       }, | ||||
|     }); | ||||
|     await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true}); | ||||
|     initEasyMDEImagePaste(this.easyMDE, this.dropzone); | ||||
|     hideElem(this.textareaMarkdownToolbar); | ||||
|   } | ||||
|  | ||||
|   value(v = undefined) { | ||||
|     if (v === undefined) { | ||||
|       if (this.easyMDE) { | ||||
|         return this.easyMDE.value(); | ||||
|       } | ||||
|       return this.textarea.value; | ||||
|     } | ||||
|  | ||||
|     if (this.easyMDE) { | ||||
|       this.easyMDE.value(v); | ||||
|     } else { | ||||
|       this.textarea.value = v; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   focus() { | ||||
|     if (this.easyMDE) { | ||||
|       this.easyMDE.codemirror.focus(); | ||||
|     } else { | ||||
|       this.textarea.focus(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   moveCursorToEnd() { | ||||
|     this.textarea.focus(); | ||||
|     this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length); | ||||
|     if (this.easyMDE) { | ||||
|       this.easyMDE.codemirror.focus(); | ||||
|       this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function getComboMarkdownEditor(el) { | ||||
|   if (el instanceof $) el = el[0]; | ||||
|   return el?._giteaComboMarkdownEditor; | ||||
| } | ||||
|  | ||||
| export async function initComboMarkdownEditor(container, options = {}) { | ||||
|   if (container instanceof $) { | ||||
|     if (container.length !== 1) { | ||||
|       throw new Error('initComboMarkdownEditor: container must be a single element'); | ||||
|     } | ||||
|     container = container[0]; | ||||
|   } | ||||
|   if (!container) { | ||||
|     throw new Error('initComboMarkdownEditor: container is null'); | ||||
|   } | ||||
|   const editor = new ComboMarkdownEditor(container, options); | ||||
|   await editor.init(); | ||||
|   return editor; | ||||
| } | ||||
| @@ -1,181 +0,0 @@ | ||||
| import $ from 'jquery'; | ||||
| import {attachTribute} from '../tribute.js'; | ||||
| import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | ||||
|  | ||||
| /** | ||||
|  * @returns {EasyMDE} | ||||
|  */ | ||||
| export async function importEasyMDE() { | ||||
|   // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can | ||||
|   // not overwrite the default styles. | ||||
|   const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); | ||||
|   return EasyMDE; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * create an EasyMDE editor for comment | ||||
|  * @param textarea jQuery or HTMLElement | ||||
|  * @param easyMDEOptions the options for EasyMDE | ||||
|  * @returns {null|EasyMDE} | ||||
|  */ | ||||
| export async function createCommentEasyMDE(textarea, easyMDEOptions = {}) { | ||||
|   if (textarea instanceof $) { | ||||
|     textarea = textarea[0]; | ||||
|   } | ||||
|   if (!textarea) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   const EasyMDE = await importEasyMDE(); | ||||
|  | ||||
|   const easyMDE = new EasyMDE({ | ||||
|     autoDownloadFontAwesome: false, | ||||
|     element: textarea, | ||||
|     forceSync: true, | ||||
|     renderingConfig: { | ||||
|       singleLineBreaks: false, | ||||
|     }, | ||||
|     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', '|', { | ||||
|         name: 'checkbox-empty', | ||||
|         action(e) { | ||||
|           const cm = e.codemirror; | ||||
|           cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`); | ||||
|           cm.focus(); | ||||
|         }, | ||||
|         className: 'fa fa-square-o', | ||||
|         title: 'Add Checkbox (empty)', | ||||
|       }, | ||||
|       { | ||||
|         name: 'checkbox-checked', | ||||
|         action(e) { | ||||
|           const cm = e.codemirror; | ||||
|           cm.replaceSelection(`\n- [x] ${cm.getSelection()}`); | ||||
|           cm.focus(); | ||||
|         }, | ||||
|         className: 'fa fa-check-square-o', | ||||
|         title: 'Add Checkbox (checked)', | ||||
|       }, '|', | ||||
|       'unordered-list', 'ordered-list', '|', | ||||
|       'link', 'image', 'table', 'horizontal-rule', '|', | ||||
|       'clean-block', '|', | ||||
|       { | ||||
|         name: 'revert-to-textarea', | ||||
|         action(e) { | ||||
|           e.toTextArea(); | ||||
|         }, | ||||
|         className: 'fa fa-file', | ||||
|         title: 'Revert to simple textarea', | ||||
|       }, | ||||
|     ], ...easyMDEOptions}); | ||||
|  | ||||
|   const inputField = easyMDE.codemirror.getInputField(); | ||||
|  | ||||
|   easyMDE.codemirror.on('change', (...args) => { | ||||
|     easyMDEOptions?.onChange?.(...args); | ||||
|   }); | ||||
|   easyMDE.codemirror.setOption('extraKeys', { | ||||
|     'Cmd-Enter': codeMirrorQuickSubmit, | ||||
|     'Ctrl-Enter': codeMirrorQuickSubmit, | ||||
|     Enter: (cm) => { | ||||
|       const tributeContainer = document.querySelector('.tribute-container'); | ||||
|       if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|         cm.execCommand('newlineAndIndent'); | ||||
|       } | ||||
|     }, | ||||
|     Backspace: (cm) => { | ||||
|       if (cm.getInputField().trigger) { | ||||
|         cm.getInputField().trigger('input'); | ||||
|       } | ||||
|       cm.execCommand('delCharBefore'); | ||||
|     }, | ||||
|     Up: (cm) => { | ||||
|       const tributeContainer = document.querySelector('.tribute-container'); | ||||
|       if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|         return cm.execCommand('goLineUp'); | ||||
|       } | ||||
|     }, | ||||
|     Down: (cm) => { | ||||
|       const tributeContainer = document.querySelector('.tribute-container'); | ||||
|       if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||
|         return cm.execCommand('goLineDown'); | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|   await attachTribute(inputField, {mentions: true, emoji: true}); | ||||
|   attachEasyMDEToElements(easyMDE); | ||||
|   return easyMDE; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * attach the EasyMDE object to its input elements (InputField, TextArea) | ||||
|  * @param {EasyMDE} easyMDE | ||||
|  */ | ||||
| export function attachEasyMDEToElements(easyMDE) { | ||||
|   // TODO: that's the only way we can do now to attach the EasyMDE object to a HTMLElement | ||||
|  | ||||
|   // InputField is used by CodeMirror to accept user input | ||||
|   const inputField = easyMDE.codemirror.getInputField(); | ||||
|   inputField._data_easyMDE = easyMDE; | ||||
|  | ||||
|   // TextArea is the real textarea element in the form | ||||
|   const textArea = easyMDE.codemirror.getTextArea(); | ||||
|   textArea._data_easyMDE = easyMDE; | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * get the attached EasyMDE editor created by createCommentEasyMDE | ||||
|  * @param el jQuery or HTMLElement | ||||
|  * @returns {null|EasyMDE} | ||||
|  */ | ||||
| export function getAttachedEasyMDE(el) { | ||||
|   if (el instanceof $) { | ||||
|     el = el[0]; | ||||
|   } | ||||
|   if (!el) { | ||||
|     return null; | ||||
|   } | ||||
|   return el._data_easyMDE; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * validate if the given EasyMDE textarea is is non-empty. | ||||
|  * @param {jQuery} $textarea | ||||
|  * @returns {boolean} returns true if validation succeeded. | ||||
|  */ | ||||
| export function validateTextareaNonEmpty($textarea) { | ||||
|   const $mdeInputField = $(getAttachedEasyMDE($textarea).codemirror.getInputField()); | ||||
|   // The original edit area HTML element is hidden and replaced by the | ||||
|   // SimpleMDE/EasyMDE editor, breaking HTML5 input validation if the text area is empty. | ||||
|   // This is a workaround for this upstream bug. | ||||
|   // See https://github.com/sparksuite/simplemde-markdown-editor/issues/324 | ||||
|   if (!$textarea.val()) { | ||||
|     $mdeInputField.prop('required', true); | ||||
|     const $form = $textarea.parents('form'); | ||||
|     if (!$form.length) { | ||||
|       // this should never happen. we put a alert here in case the textarea would be forgotten to be put in a form | ||||
|       alert('Require non-empty content'); | ||||
|     } else { | ||||
|       $form[0].reportValidity(); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|   $mdeInputField.prop('required', false); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * there is no guarantee that the CodeMirror object is inside the same form as the textarea, | ||||
|  * so can not call handleGlobalEnterQuickSubmit directly. | ||||
|  * @param {CodeMirror.EditorFromTextArea} codeMirror | ||||
|  */ | ||||
| export function codeMirrorQuickSubmit(codeMirror) { | ||||
|   handleGlobalEnterQuickSubmit(codeMirror.getTextArea()); | ||||
| } | ||||
| @@ -88,38 +88,43 @@ class CodeMirrorEditor { | ||||
| } | ||||
|  | ||||
|  | ||||
| export function initEasyMDEImagePaste(easyMDE, $dropzone) { | ||||
| const uploadClipboardImage = async (editor, dropzone, e) => { | ||||
|   const $dropzone = $(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(); | ||||
|   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('.')); | ||||
|   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); | ||||
|       editor.replacePlaceholder(placeholder, ``); | ||||
|     const placeholder = ``; | ||||
|     editor.insertPlaceholder(placeholder); | ||||
|     const data = await uploadFile(img, uploadUrl); | ||||
|     editor.replacePlaceholder(placeholder, ``); | ||||
|  | ||||
|       const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid); | ||||
|       $files.append($input); | ||||
|     } | ||||
|   }; | ||||
|     const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid); | ||||
|     $files.append($input); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export function initEasyMDEImagePaste(easyMDE, dropzone) { | ||||
|   if (!dropzone) return; | ||||
|   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); | ||||
|     return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function initTextareaImagePaste(textarea, dropzone) { | ||||
|   if (!dropzone) return; | ||||
|   $(textarea).on('paste', async (e) => { | ||||
|     return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import $ from 'jquery'; | ||||
| import {initMarkupContent} from '../../markup/content.js'; | ||||
| import {attachRefIssueContextPopup} from '../contextpopup.js'; | ||||
|  | ||||
| const {csrfToken} = window.config; | ||||
|  | ||||
| export function initCompMarkupContentPreviewTab($form) { | ||||
|   const $tabMenu = $form.find('.tabular.menu'); | ||||
|   $tabMenu.find('.item').tab(); | ||||
|   $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`).on('click', function () { | ||||
|     const $this = $(this); | ||||
|     $.post($this.data('url'), { | ||||
|       _csrf: csrfToken, | ||||
|       mode: 'comment', | ||||
|       context: $this.data('context'), | ||||
|       text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val() | ||||
|     }, (data) => { | ||||
|       const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); | ||||
|       $previewPanel.html(data); | ||||
|       const refIssues = $previewPanel.find('p .ref-issue'); | ||||
|       attachRefIssueContextPopup(refIssues); | ||||
|       initMarkupContent(); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user