new functions and fixes
This commit is contained in:
+131
-21
@@ -30,9 +30,18 @@ function formatDate(value) {
|
||||
return Number.isNaN(date.getTime()) ? "" : date.toLocaleString("pl-PL");
|
||||
}
|
||||
|
||||
function aliasCode(filename, label, mimeType) {
|
||||
const kind = String(mimeType || "").startsWith("image/") ? "image" : "file";
|
||||
function isVideo(mimeType) {
|
||||
return String(mimeType || "").startsWith("video/");
|
||||
}
|
||||
|
||||
function isLikelyVideoFile(file) {
|
||||
return isVideo(file?.type) || /\.(?:mp4|m4v|mov|webm|ogv)$/i.test(String(file?.name || ""));
|
||||
}
|
||||
|
||||
function aliasCode(filename, label, mimeType, mode = "auto") {
|
||||
const safeLabel = String(label || filename).replace(/\]/g, ")").replace(/[\r\n]+/g, " ").trim() || filename;
|
||||
let kind = String(mimeType || "").startsWith("image/") ? "image" : "file";
|
||||
if (isVideo(mimeType) && mode === "player") kind = "video";
|
||||
return `[${kind}=${filename},${safeLabel}]`;
|
||||
}
|
||||
|
||||
@@ -70,12 +79,84 @@ function safeAttachmentUrl(value) {
|
||||
: safePublicUrl(raw, { allowMailto: false });
|
||||
}
|
||||
|
||||
function createVideoInsertDialog() {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.className = "app-dialog video-insert-dialog";
|
||||
dialog.innerHTML = `
|
||||
<div class="video-insert-dialog__panel">
|
||||
<div class="dialog-heading-row">
|
||||
<div><p class="eyebrow">Video file</p><h2>How should it be added?</h2></div>
|
||||
<button type="button" class="icon-button" data-video-choice="cancel" aria-label="Cancel">×</button>
|
||||
</div>
|
||||
<p class="dialog-copy" data-video-file-name></p>
|
||||
<div class="video-choice-actions">
|
||||
<button type="button" class="action-button action-button--primary" data-video-choice="player">
|
||||
<strong>Embedded player</strong><span>Play the video directly in the note.</span>
|
||||
</button>
|
||||
<button type="button" class="action-button action-button--secondary" data-video-choice="download">
|
||||
<strong>Download link</strong><span>Insert a link that downloads the original file.</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.append(dialog);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxSize = () => 0, canDelete, canUpload, canEdit = () => true, toast, onFilesChanged = () => { } }) {
|
||||
const dialog = document.querySelector("#files-dialog");
|
||||
const list = document.querySelector("#files-list");
|
||||
const input = document.querySelector("#file-input");
|
||||
const footer = document.querySelector("#footer-files");
|
||||
const retryableStatuses = new Set([408, 425, 429, 500, 502, 503, 504]);
|
||||
let videoInsertDialog;
|
||||
|
||||
function chooseVideoInsertMode(filename) {
|
||||
videoInsertDialog ||= createVideoInsertDialog();
|
||||
videoInsertDialog.querySelector("[data-video-file-name]").textContent = filename;
|
||||
if (!videoInsertDialog.open) videoInsertDialog.showModal();
|
||||
|
||||
return new Promise(resolve => {
|
||||
let settled = false;
|
||||
const finish = value => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
videoInsertDialog.removeEventListener("click", handleClick);
|
||||
videoInsertDialog.removeEventListener("cancel", handleCancel);
|
||||
if (videoInsertDialog.open) videoInsertDialog.close();
|
||||
resolve(value);
|
||||
};
|
||||
const handleClick = event => {
|
||||
const choice = event.target.closest("[data-video-choice]")?.dataset.videoChoice;
|
||||
if (!choice) return;
|
||||
finish(choice === "player" || choice === "download" ? choice : null);
|
||||
};
|
||||
const handleCancel = event => {
|
||||
event.preventDefault();
|
||||
finish(null);
|
||||
};
|
||||
videoInsertDialog.addEventListener("click", handleClick);
|
||||
videoInsertDialog.addEventListener("cancel", handleCancel);
|
||||
});
|
||||
}
|
||||
|
||||
function fileActionButtons(file) {
|
||||
const attributes = `data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}"`;
|
||||
const insertButtons = isVideo(file.mime_type)
|
||||
? `<button class="action-button action-button--primary compact-button" data-add-file-to-note data-insert-mode="player" ${attributes}>Add player</button>
|
||||
<button class="action-button action-button--secondary compact-button" data-add-file-to-note data-insert-mode="download" ${attributes}>Add download</button>`
|
||||
: `<button class="action-button action-button--primary compact-button" data-add-file-to-note data-insert-mode="auto" ${attributes}>Add to note</button>`;
|
||||
const codeButtons = isVideo(file.mime_type)
|
||||
? `<button class="action-button action-button--secondary compact-button" data-show-file-code="link" ${attributes}>Link</button>
|
||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="player" ${attributes}>Player code</button>
|
||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="download" ${attributes}>Download code</button>`
|
||||
: `<button class="action-button action-button--secondary compact-button" data-show-file-code="link" ${attributes}>Link</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="alias" ${attributes}>Alias</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" ${attributes}>Markdown</button>`;
|
||||
const deleteButton = canDelete()
|
||||
? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>`
|
||||
: "";
|
||||
return `${codeButtons}${insertButtons}${deleteButton}`;
|
||||
}
|
||||
|
||||
async function loadFiles({ open = false } = {}) {
|
||||
try {
|
||||
@@ -88,13 +169,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
<div class="file-name">${escapeHtml(file.filename)}</div>
|
||||
<div class="file-meta">${formatBytes(file.size_bytes)} · ${escapeHtml(file.mime_type)}${file.created_at ? ` · ${formatDate(file.created_at)}` : ""} · <span class="file-flag${file.is_attached ? "" : " detached"}">${file.is_attached ? "in note" : "removed from content"}</span></div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="alias" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Alias</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>
|
||||
<button class="action-button action-button--primary compact-button" data-add-file-to-note data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Add to note</button>
|
||||
${canDelete() ? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>` : ""}
|
||||
</div>
|
||||
<div class="file-actions">${fileActionButtons(file)}</div>
|
||||
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
|
||||
</div>`).join("") : '<p class="dialog-copy">No files uploaded.</p>';
|
||||
onFilesChanged(files);
|
||||
@@ -112,7 +187,15 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
return { start: start + text.length, end: start + text.length };
|
||||
}
|
||||
|
||||
async function uploadFile(file, onUploaded) {
|
||||
function insertVideoPlayer(text, range = null, inputType = "insertText") {
|
||||
const start = Math.max(0, Math.min(range?.start ?? editor.selectionStart, editor.value.length));
|
||||
const end = Math.max(start, Math.min(range?.end ?? editor.selectionEnd, editor.value.length));
|
||||
const prefix = start > 0 && editor.value[start - 1] !== "\n" ? "\n" : "";
|
||||
const suffix = end < editor.value.length && editor.value[end] !== "\n" ? "\n" : "";
|
||||
return insertText(`${prefix}${text}${suffix}`, { start, end }, inputType);
|
||||
}
|
||||
|
||||
async function uploadFile(file, onUploaded, insertMode = "auto") {
|
||||
const uploadToast = createUploadToast(file.name);
|
||||
let completed = false;
|
||||
|
||||
@@ -131,7 +214,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
});
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
const text = aliasCode(result.name, file.name, result.mime_type || file.type);
|
||||
const text = aliasCode(result.name, file.name, result.mime_type || file.type, insertMode);
|
||||
await onUploaded(text);
|
||||
uploadToast.success();
|
||||
await loadFiles();
|
||||
@@ -158,20 +241,21 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
input.addEventListener("change", async event => {
|
||||
let file = event.target.files[0];
|
||||
if (!file) return;
|
||||
input.value = "";
|
||||
if (file.type.startsWith("image/")) {
|
||||
try {
|
||||
file = await prepareImageFile(file);
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
if (!file) { input.value = ""; return; }
|
||||
if (!file) return;
|
||||
}
|
||||
|
||||
const insertMode = isLikelyVideoFile(file) ? await chooseVideoInsertMode(file.name) : "auto";
|
||||
if (!insertMode) return;
|
||||
const range = { start: editor.selectionStart, end: editor.selectionEnd };
|
||||
input.value = "";
|
||||
await uploadFile(file, text => insertText(text, range));
|
||||
await uploadFile(file, text => insertMode === "player" ? insertVideoPlayer(text, range) : insertText(text, range), insertMode);
|
||||
});
|
||||
|
||||
document.querySelector("#editor-workspace")?.addEventListener("paste", async event => {
|
||||
@@ -202,7 +286,10 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
state.replaceEnd = state.cursor;
|
||||
};
|
||||
|
||||
for (const file of files) await uploadFile(file, insertPastedAlias);
|
||||
for (const file of files) {
|
||||
const insertMode = isLikelyVideoFile(file) ? await chooseVideoInsertMode(file.name) : "auto";
|
||||
if (insertMode) await uploadFile(file, insertPastedAlias, insertMode);
|
||||
}
|
||||
editor.setSelectionRange(state.cursor, state.cursor);
|
||||
});
|
||||
|
||||
@@ -212,11 +299,14 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
list.addEventListener("click", async event => {
|
||||
const addButton = event.target.closest("[data-add-file-to-note]");
|
||||
if (addButton) {
|
||||
const text = aliasCode(addButton.dataset.name, addButton.dataset.name, addButton.dataset.mime);
|
||||
insertText(text);
|
||||
const mode = addButton.dataset.insertMode;
|
||||
const text = aliasCode(addButton.dataset.name, addButton.dataset.name, addButton.dataset.mime, mode);
|
||||
if (mode === "player") insertVideoPlayer(text);
|
||||
else insertText(text);
|
||||
toast("Added to note");
|
||||
return;
|
||||
}
|
||||
|
||||
const showButton = event.target.closest("[data-show-file-code]");
|
||||
if (showButton) {
|
||||
const panel = showButton.closest(".file-row").querySelector(".file-code");
|
||||
@@ -228,11 +318,29 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime);
|
||||
} else if (showButton.dataset.showFileCode === "markdown") {
|
||||
text = markdownCode(safeUrl, showButton.dataset.name, showButton.dataset.mime);
|
||||
} else if (showButton.dataset.showFileCode === "player") {
|
||||
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime, "player");
|
||||
} else if (showButton.dataset.showFileCode === "download") {
|
||||
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime, "download");
|
||||
}
|
||||
output.value = text; panel.hidden = false; output.focus(); output.select(); return;
|
||||
output.value = text;
|
||||
panel.hidden = false;
|
||||
output.focus();
|
||||
output.select();
|
||||
return;
|
||||
}
|
||||
|
||||
const copyButton = event.target.closest("[data-copy-generated]");
|
||||
if (copyButton) { try { await copyText(copyButton.closest(".file-code").querySelector("textarea").value); toast("Copied"); } catch (error) { toast(error.message); } return; }
|
||||
if (copyButton) {
|
||||
try {
|
||||
await copyText(copyButton.closest(".file-code").querySelector("textarea").value);
|
||||
toast("Copied");
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteButton = event.target.closest("[data-delete-file]");
|
||||
if (!deleteButton) return;
|
||||
if (!await askConfirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`, { title: "Delete file", confirmText: "Delete", danger: true })) return;
|
||||
@@ -240,7 +348,9 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
|
||||
await api(endpoints.remove(deleteButton.dataset.deleteFile), { method: "DELETE", headers: {}, body: JSON.stringify({ access_token: getAccessToken() || null }) });
|
||||
toast("File deleted");
|
||||
await loadFiles();
|
||||
} catch (error) { toast(error.message); }
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
return { loadFiles };
|
||||
|
||||
Reference in New Issue
Block a user