code.gitea.io/gitea@v1.22.3/web_src/js/features/comp/Paste.js (about)

     1  import {htmlEscape} from 'escape-goat';
     2  import {POST} from '../../modules/fetch.js';
     3  import {imageInfo} from '../../utils/image.js';
     4  import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js';
     5  import {isUrl} from '../../utils/url.js';
     6  
     7  async function uploadFile(file, uploadUrl) {
     8    const formData = new FormData();
     9    formData.append('file', file, file.name);
    10  
    11    const res = await POST(uploadUrl, {data: formData});
    12    return await res.json();
    13  }
    14  
    15  function triggerEditorContentChanged(target) {
    16    target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
    17  }
    18  
    19  class TextareaEditor {
    20    constructor(editor) {
    21      this.editor = editor;
    22    }
    23  
    24    insertPlaceholder(value) {
    25      const editor = this.editor;
    26      const startPos = editor.selectionStart;
    27      const endPos = editor.selectionEnd;
    28      editor.value = editor.value.substring(0, startPos) + value + editor.value.substring(endPos);
    29      editor.selectionStart = startPos;
    30      editor.selectionEnd = startPos + value.length;
    31      editor.focus();
    32      triggerEditorContentChanged(editor);
    33    }
    34  
    35    replacePlaceholder(oldVal, newVal) {
    36      const editor = this.editor;
    37      const startPos = editor.selectionStart;
    38      const endPos = editor.selectionEnd;
    39      if (editor.value.substring(startPos, endPos) === oldVal) {
    40        editor.value = editor.value.substring(0, startPos) + newVal + editor.value.substring(endPos);
    41        editor.selectionEnd = startPos + newVal.length;
    42      } else {
    43        editor.value = editor.value.replace(oldVal, newVal);
    44        editor.selectionEnd -= oldVal.length;
    45        editor.selectionEnd += newVal.length;
    46      }
    47      editor.selectionStart = editor.selectionEnd;
    48      editor.focus();
    49      triggerEditorContentChanged(editor);
    50    }
    51  }
    52  
    53  class CodeMirrorEditor {
    54    constructor(editor) {
    55      this.editor = editor;
    56    }
    57  
    58    insertPlaceholder(value) {
    59      const editor = this.editor;
    60      const startPoint = editor.getCursor('start');
    61      const endPoint = editor.getCursor('end');
    62      editor.replaceSelection(value);
    63      endPoint.ch = startPoint.ch + value.length;
    64      editor.setSelection(startPoint, endPoint);
    65      editor.focus();
    66      triggerEditorContentChanged(editor.getTextArea());
    67    }
    68  
    69    replacePlaceholder(oldVal, newVal) {
    70      const editor = this.editor;
    71      const endPoint = editor.getCursor('end');
    72      if (editor.getSelection() === oldVal) {
    73        editor.replaceSelection(newVal);
    74      } else {
    75        editor.setValue(editor.getValue().replace(oldVal, newVal));
    76      }
    77      endPoint.ch -= oldVal.length;
    78      endPoint.ch += newVal.length;
    79      editor.setSelection(endPoint, endPoint);
    80      editor.focus();
    81      triggerEditorContentChanged(editor.getTextArea());
    82    }
    83  }
    84  
    85  async function handleClipboardImages(editor, dropzone, images, e) {
    86    const uploadUrl = dropzone.getAttribute('data-upload-url');
    87    const filesContainer = dropzone.querySelector('.files');
    88  
    89    if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
    90  
    91    e.preventDefault();
    92    e.stopPropagation();
    93  
    94    for (const img of images) {
    95      const name = img.name.slice(0, img.name.lastIndexOf('.'));
    96  
    97      const placeholder = `![${name}](uploading ...)`;
    98      editor.insertPlaceholder(placeholder);
    99  
   100      const {uuid} = await uploadFile(img, uploadUrl);
   101      const {width, dppx} = await imageInfo(img);
   102  
   103      let text;
   104      if (width > 0 && dppx > 1) {
   105        // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
   106        // method to change image size in Markdown that is supported by all implementations.
   107        // Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
   108        const url = `attachments/${uuid}`;
   109        text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
   110      } else {
   111        // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
   112        // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
   113        const url = `/attachments/${uuid}`;
   114        text = `![${name}](${url})`;
   115      }
   116      editor.replacePlaceholder(placeholder, text);
   117  
   118      const input = document.createElement('input');
   119      input.setAttribute('name', 'files');
   120      input.setAttribute('type', 'hidden');
   121      input.setAttribute('id', uuid);
   122      input.value = uuid;
   123      filesContainer.append(input);
   124    }
   125  }
   126  
   127  function handleClipboardText(textarea, text, e) {
   128    // when pasting links over selected text, turn it into [text](link), except when shift key is held
   129    const {value, selectionStart, selectionEnd, _shiftDown} = textarea;
   130    if (_shiftDown) return;
   131    const selectedText = value.substring(selectionStart, selectionEnd);
   132    const trimmedText = text.trim();
   133    if (selectedText && isUrl(trimmedText)) {
   134      e.stopPropagation();
   135      e.preventDefault();
   136      replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
   137    }
   138  }
   139  
   140  export function initEasyMDEPaste(easyMDE, dropzone) {
   141    easyMDE.codemirror.on('paste', (_, e) => {
   142      const {images} = getPastedContent(e);
   143      if (images.length) {
   144        handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
   145      }
   146    });
   147  }
   148  
   149  export function initTextareaPaste(textarea, dropzone) {
   150    textarea.addEventListener('paste', (e) => {
   151      const {images, text} = getPastedContent(e);
   152      if (images.length) {
   153        handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
   154      } else if (text) {
   155        handleClipboardText(textarea, text, e);
   156      }
   157    });
   158  }