gitea源码

EditorUpload.ts 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import {imageInfo} from '../../utils/image.ts';
  2. import {textareaInsertText, triggerEditorContentChanged} from './EditorMarkdown.ts';
  3. import {
  4. DropzoneCustomEventRemovedFile,
  5. DropzoneCustomEventUploadDone,
  6. generateMarkdownLinkForAttachment,
  7. } from '../dropzone.ts';
  8. import {subscribe} from '@github/paste-markdown';
  9. import type CodeMirror from 'codemirror';
  10. import type EasyMDE from 'easymde';
  11. import type {DropzoneFile} from 'dropzone';
  12. let uploadIdCounter = 0;
  13. export const EventUploadStateChanged = 'ce-upload-state-changed';
  14. export function triggerUploadStateChanged(target: HTMLElement) {
  15. target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
  16. }
  17. function uploadFile(dropzoneEl: HTMLElement, file: File) {
  18. return new Promise((resolve) => {
  19. const curUploadId = uploadIdCounter++;
  20. (file as any)._giteaUploadId = curUploadId;
  21. const dropzoneInst = dropzoneEl.dropzone;
  22. const onUploadDone = ({file}: {file: any}) => {
  23. if (file._giteaUploadId === curUploadId) {
  24. dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
  25. resolve(file);
  26. }
  27. };
  28. dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone);
  29. // FIXME: this is not entirely correct because `file` does not satisfy DropzoneFile (we have abused the Dropzone for long time)
  30. dropzoneInst.addFile(file as DropzoneFile);
  31. });
  32. }
  33. class TextareaEditor {
  34. editor: HTMLTextAreaElement;
  35. constructor(editor: HTMLTextAreaElement) {
  36. this.editor = editor;
  37. }
  38. insertPlaceholder(value: string) {
  39. textareaInsertText(this.editor, value);
  40. }
  41. replacePlaceholder(oldVal: string, newVal: string) {
  42. const editor = this.editor;
  43. const startPos = editor.selectionStart;
  44. const endPos = editor.selectionEnd;
  45. if (editor.value.substring(startPos, endPos) === oldVal) {
  46. editor.value = editor.value.substring(0, startPos) + newVal + editor.value.substring(endPos);
  47. editor.selectionEnd = startPos + newVal.length;
  48. } else {
  49. editor.value = editor.value.replace(oldVal, newVal);
  50. editor.selectionEnd -= oldVal.length;
  51. editor.selectionEnd += newVal.length;
  52. }
  53. editor.selectionStart = editor.selectionEnd;
  54. editor.focus();
  55. triggerEditorContentChanged(editor);
  56. }
  57. }
  58. class CodeMirrorEditor {
  59. editor: CodeMirror.EditorFromTextArea;
  60. constructor(editor: CodeMirror.EditorFromTextArea) {
  61. this.editor = editor;
  62. }
  63. insertPlaceholder(value: string) {
  64. const editor = this.editor;
  65. const startPoint = editor.getCursor('start');
  66. const endPoint = editor.getCursor('end');
  67. editor.replaceSelection(value);
  68. endPoint.ch = startPoint.ch + value.length;
  69. editor.setSelection(startPoint, endPoint);
  70. editor.focus();
  71. triggerEditorContentChanged(editor.getTextArea());
  72. }
  73. replacePlaceholder(oldVal: string, newVal: string) {
  74. const editor = this.editor;
  75. const endPoint = editor.getCursor('end');
  76. if (editor.getSelection() === oldVal) {
  77. editor.replaceSelection(newVal);
  78. } else {
  79. editor.setValue(editor.getValue().replace(oldVal, newVal));
  80. }
  81. endPoint.ch -= oldVal.length;
  82. endPoint.ch += newVal.length;
  83. editor.setSelection(endPoint, endPoint);
  84. editor.focus();
  85. triggerEditorContentChanged(editor.getTextArea());
  86. }
  87. }
  88. async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, dropzoneEl: HTMLElement, files: Array<File> | FileList, e: Event) {
  89. e.preventDefault();
  90. for (const file of files) {
  91. const name = file.name.slice(0, file.name.lastIndexOf('.'));
  92. const {width, dppx} = await imageInfo(file);
  93. const placeholder = `[${name}](uploading ...)`;
  94. editor.insertPlaceholder(placeholder);
  95. await uploadFile(dropzoneEl, file); // the "file" will get its "uuid" during the upload
  96. editor.replacePlaceholder(placeholder, generateMarkdownLinkForAttachment(file, {width, dppx}));
  97. }
  98. }
  99. export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) {
  100. text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), '');
  101. text = text.replace(new RegExp(`[<]img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), '');
  102. return text;
  103. }
  104. function getPastedImages(e: ClipboardEvent) {
  105. const images: Array<File> = [];
  106. for (const item of e.clipboardData?.items ?? []) {
  107. if (item.type?.startsWith('image/')) {
  108. images.push(item.getAsFile());
  109. }
  110. }
  111. return images;
  112. }
  113. export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
  114. const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
  115. easyMDE.codemirror.on('paste', (_, e) => {
  116. const images = getPastedImages(e);
  117. if (!images.length) return;
  118. handleUploadFiles(editor, dropzoneEl, images, e);
  119. });
  120. easyMDE.codemirror.on('drop', (_, e) => {
  121. if (!e.dataTransfer.files.length) return;
  122. handleUploadFiles(editor, dropzoneEl, e.dataTransfer.files, e);
  123. });
  124. dropzoneEl.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => {
  125. const oldText = easyMDE.codemirror.getValue();
  126. const newText = removeAttachmentLinksFromMarkdown(oldText, fileUuid);
  127. if (oldText !== newText) easyMDE.codemirror.setValue(newText);
  128. });
  129. }
  130. export function initTextareaEvents(textarea: HTMLTextAreaElement, dropzoneEl: HTMLElement) {
  131. subscribe(textarea); // enable paste features
  132. textarea.addEventListener('paste', (e: ClipboardEvent) => {
  133. const images = getPastedImages(e);
  134. if (images.length && dropzoneEl) {
  135. handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e);
  136. }
  137. });
  138. textarea.addEventListener('drop', (e: DragEvent) => {
  139. if (!e.dataTransfer.files.length) return;
  140. if (!dropzoneEl) return;
  141. handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e);
  142. });
  143. dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}: {fileUuid: string}) => {
  144. const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid);
  145. if (textarea.value !== newText) textarea.value = newText;
  146. });
  147. }