Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions web_src/js/features/common-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {createDropzone} from './dropzone.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js';
import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter} from '../utils/dom.js';
import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter, getComboMarkdownEditor} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
import {showTemporaryTooltip} from '../modules/tippy.js';
import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js';
import {request, POST, GET} from '../modules/fetch.js';
import {removeLinksInTextarea} from './comp/ComboMarkdownEditor.js';
import '../htmx.js';

const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
Expand Down Expand Up @@ -249,12 +250,13 @@ export function initDropzone(el) {
});
file.previewTemplate.append(copyLinkElement);
});
this.on('removedfile', (file) => {
$(`#${file.uuid}`).remove();
this.on('removedfile', async (file) => {
document.getElementById(file.uuid)?.remove();
if ($dropzone.data('remove-url')) {
POST($dropzone.data('remove-url'), {
await POST($dropzone.data('remove-url'), {
data: new URLSearchParams({file: file.uuid}),
});
removeLinksInTextarea(getComboMarkdownEditor(el.closest('form').querySelector('.combo-markdown-editor')), file);
}
});
this.on('error', function (file, message) {
Expand Down
11 changes: 6 additions & 5 deletions web_src/js/features/comp/ComboMarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,6 @@ class ComboMarkdownEditor {
}
}

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) {
Expand All @@ -315,3 +310,9 @@ export async function initComboMarkdownEditor(container, options = {}) {
await editor.init();
return editor;
}

export function removeLinksInTextarea(editor, file) {
const fileName = file.name.slice(0, file.name.lastIndexOf('.'));
const fileText = `\\[${fileName}\\]\\(/attachments/${file.uuid}\\)`;
editor.value(editor.value().replace(new RegExp(`<img [\\s\\w"=]+ alt="${fileName}" src="/attachments/${file.uuid}">`, 'g'), '').replace(new RegExp(`\\!${fileText}`, 'g'), '').replace(new RegExp(fileText, 'g'), ''));
}
59 changes: 38 additions & 21 deletions web_src/js/features/comp/Paste.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,35 +82,48 @@ class CodeMirrorEditor {
}
}

async function handleClipboardImages(editor, dropzone, images, e) {
async function handleClipboardFiles(editor, dropzone, files, e) {
const uploadUrl = dropzone.getAttribute('data-upload-url');
const filesContainer = dropzone.querySelector('.files');

if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
if (!dropzone || !uploadUrl || !filesContainer || !files.length) return;

e.preventDefault();
e.stopPropagation();

for (const img of images) {
const name = img.name.slice(0, img.name.lastIndexOf('.'));
for (const file of files) {
if (!file) continue;
const name = file.name.slice(0, file.name.lastIndexOf('.'));

const placeholder = `![${name}](uploading ...)`;
editor.insertPlaceholder(placeholder);

const {uuid} = await uploadFile(img, uploadUrl);
const {width, dppx} = await imageInfo(img);
const {uuid} = await uploadFile(file, uploadUrl);
const {width, dppx} = await imageInfo(file);

const url = `/attachments/${uuid}`;
let text;
if (width > 0 && dppx > 1) {
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
// method to change image size in Markdown that is supported by all implementations.
text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
if (file.type?.startsWith('image/')) {
if (width > 0 && dppx > 1) {
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
// method to change image size in Markdown that is supported by all implementations.
text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
} else {
text = `![${name}](${url})`;
}
} else {
text = `![${name}](${url})`;
text = `[${name}](${url})`;
}
editor.replacePlaceholder(placeholder, text);

file.uuid = uuid;
dropzone.dropzone.emit('addedfile', file);
if (file.type?.startsWith('image/')) {
const imgSrc = `/attachments/${file.uuid}`;
dropzone.dropzone.emit('thumbnail', file, imgSrc);
dropzone.querySelector(`img[src='${CSS.escape(imgSrc)}']`).style.maxWidth = '100%';
}
dropzone.dropzone.emit('complete', file);
const input = document.createElement('input');
input.setAttribute('name', 'files');
input.setAttribute('type', 'hidden');
Expand All @@ -134,21 +147,25 @@ function handleClipboardText(textarea, text, e) {
}

export function initEasyMDEPaste(easyMDE, dropzone) {
easyMDE.codemirror.on('paste', (_, e) => {
const {images} = getPastedContent(e);
if (images.length) {
handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
const pasteFunc = (e) => {
const {files} = getPastedContent(e);
if (files.length) {
handleClipboardFiles(new CodeMirrorEditor(easyMDE.codemirror), dropzone, files, e);
}
});
};
easyMDE.codemirror.on('paste', (_, e) => pasteFunc(e));
easyMDE.codemirror.on('drop', (_, e) => pasteFunc(e));
}

export function initTextareaPaste(textarea, dropzone) {
textarea.addEventListener('paste', (e) => {
const {images, text} = getPastedContent(e);
if (images.length) {
handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
const pasteFunc = (e) => {
const {files, text} = getPastedContent(e);
if (files.length) {
handleClipboardFiles(new TextareaEditor(textarea), dropzone, files, e);
} else if (text) {
handleClipboardText(textarea, text, e);
}
});
};
textarea.addEventListener('paste', (e) => pasteFunc(e));
textarea.addEventListener('drop', (e) => pasteFunc(e));
}
26 changes: 10 additions & 16 deletions web_src/js/features/repo-issue-edit.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import $ from 'jquery';
import {handleReply} from './repo-issue.js';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {initComboMarkdownEditor, removeLinksInTextarea} from './comp/ComboMarkdownEditor.js';
import {createDropzone} from './dropzone.js';
import {GET, POST} from '../modules/fetch.js';
import {hideElem, showElem} from '../utils/dom.js';
import {hideElem, showElem, getComboMarkdownEditor} from '../utils/dom.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
import {initCommentContent, initMarkupContent} from '../markup/content.js';

Expand All @@ -26,7 +26,6 @@ async function onEditContent(event) {
if (!dropzone) return null;

let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
const dz = await createDropzone(dropzone, {
url: dropzone.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken},
Expand All @@ -45,7 +44,6 @@ async function onEditContent(event) {
init() {
this.on('success', (file, data) => {
file.uuid = data.uuid;
fileUuidDict[file.uuid] = {submitted: false};
const input = document.createElement('input');
input.id = data.uuid;
input.name = 'files';
Expand All @@ -56,19 +54,15 @@ async function onEditContent(event) {
this.on('removedfile', async (file) => {
document.getElementById(file.uuid)?.remove();
if (disableRemovedfileEvent) return;
if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
if (dropzone.getAttribute('data-remove-url')) {
try {
await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
removeLinksInTextarea(getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')), file);
} catch (error) {
console.error(error);
}
}
});
this.on('submit', () => {
for (const fileUuid of Object.keys(fileUuidDict)) {
fileUuidDict[fileUuid].submitted = true;
}
});
this.on('reload', async () => {
try {
const response = await GET(editContentZone.getAttribute('data-attachment-url'));
Expand All @@ -78,16 +72,16 @@ async function onEditContent(event) {
dz.removeAllFiles(true);
dropzone.querySelector('.files').innerHTML = '';
for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
fileUuidDict = {};
disableRemovedfileEvent = false;

for (const attachment of data) {
const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
dz.emit('addedfile', attachment);
dz.emit('thumbnail', attachment, imgSrc);
if (/\.(jpg|jpeg|png|gif|bmp|svg)$/i.test(attachment.name)) {
const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
dz.emit('thumbnail', attachment, imgSrc);
dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
}
dz.emit('complete', attachment);
fileUuidDict[attachment.uuid] = {submitted: true};
dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
const input = document.createElement('input');
input.id = attachment.uuid;
input.name = 'files';
Expand Down Expand Up @@ -191,7 +185,7 @@ export function initRepoIssueCommentEdit() {
editor = await handleReply($replyBtn);
} else {
// for normal issue/comment page
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
editor = getComboMarkdownEditor(document.querySelector('#comment-form .combo-markdown-editor'));
}
if (editor) {
if (editor.value()) {
Expand Down
6 changes: 3 additions & 3 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import $ from 'jquery';
import {htmlEscape} from 'escape-goat';
import {showTemporaryTooltip, createTippy} from '../modules/tippy.js';
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
import {hideElem, showElem, toggleElem, getComboMarkdownEditor} from '../utils/dom.js';
import {setFileFolding} from './file-fold.js';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {toAbsoluteUrl} from '../utils.js';
import {initDropzone} from './common-global.js';
import {POST, GET} from '../modules/fetch.js';
Expand Down Expand Up @@ -414,7 +414,7 @@ export async function handleReply($el) {
showElem($form);

const $textarea = $form.find('textarea');
let editor = getComboMarkdownEditor($textarea);
let editor = getComboMarkdownEditor($textarea[0]);
if (!editor) {
// FIXME: the initialization of the dropzone is not consistent.
// When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
Expand Down
20 changes: 15 additions & 5 deletions web_src/js/utils/dom.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {debounce} from 'throttle-debounce';
import {extname} from '../utils.js';

function elementsCall(el, func, ...args) {
if (typeof el === 'string' || el instanceof String) {
Expand Down Expand Up @@ -258,16 +259,25 @@ export function isElemVisible(element) {
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
}

export function getComboMarkdownEditor(el) {
return el?._giteaComboMarkdownEditor;
}

// extract text and images from "paste" event
export function getPastedContent(e) {
const images = [];
for (const item of e.clipboardData?.items ?? []) {
if (item.type?.startsWith('image/')) {
images.push(item.getAsFile());
const acceptedFiles = getComboMarkdownEditor(e.currentTarget).dropzone.getAttribute('data-accepts');
const files = [];
const data = e.clipboardData?.items || e.dataTransfer?.items;
for (const item of data ?? []) {
if (item?.kind === 'file') {
const file = item.getAsFile();
if (acceptedFiles.includes(extname(file.name))) {
files.push(file);
}
}
}
const text = e.clipboardData?.getData?.('text') ?? '';
return {text, images};
return {text, files};
}

// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
Expand Down