You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Paste.js 5.0KB

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