style(front/js): apply types on all objects

- reduce amount of warnings in IDE
    - maximize API type mapping with static/js/core/types.js
This commit is contained in:
Edouard Vanbelle
2026-05-07 23:40:02 +02:00
parent a38475bd2c
commit fac184ccfe
43 changed files with 1614 additions and 574 deletions
+100 -51
View File
@@ -25,10 +25,17 @@ import { app } from './state.js';
import { uiFileTypes } from './uiFileTypes.js';
import { uiNotifications } from './uiNotifications.js';
/**
* @import {FileItem, FolderItem} from '../core/types.js'
* @import {BatchResult} from '../features/files/fileOperations.js'
*/
// UI Module
const ui = {
/** @type {HTMLDListElement | null} */
//dragPreview,
/** @type {HTMLDivElement | null} */
dragPreview: null,
/** @type {HTMLDivElement | null} */
draggedItems: null,
/**
* Initialize context menus and dialogs
@@ -338,21 +345,31 @@ const ui = {
const dropzone = document.getElementById('dropzone');
/**
*
* @param {DataTransfer} dataTransfer
* @returns {Promise<any[]|null>}
*/
const collectDroppedEntries = async (dataTransfer) => {
const items = Array.from(dataTransfer?.items || []);
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
if (rootEntries.length === 0) return null;
/** @type {Array<{file: File, relativePath: string}>} */
const out = [];
/**
* @param {FileSystemEntry} entry
* @param {string} prefix
*/
const walkEntry = async (entry, prefix = '') => {
if (!entry) return;
if (entry.isFile) {
await new Promise((resolve) => {
entry.file(
(file) => {
/** @type {FileSystemFileEntry} */ (entry).file(
(/** @type {File} */ file) => {
out.push({ file, relativePath: `${prefix}${file.name}` });
resolve(undefined);
},
@@ -364,7 +381,7 @@ const ui = {
if (entry.isDirectory) {
const dirPrefix = `${prefix}${entry.name}/`;
const reader = entry.createReader();
const reader = /** @type {FileSystemDirectoryEntry} */ (entry).createReader();
while (true) {
const children = await new Promise((resolve) => {
@@ -636,7 +653,7 @@ const ui = {
/**
* Check if a file can be previewed in the viewer
* @param {Object} file - File object with mime_type property
* @param {FileItem} file
* @returns {boolean}
*/
isViewableFile(file) {
@@ -647,6 +664,7 @@ const ui = {
* Get FontAwesome icon class for a filename based on its extension.
* Used as fallback when the backend DTO doesn't include icon_class
* (e.g. trash items).
* @param {string} fileName
*/
getIconClass(fileName) {
return uiFileTypes.getIconClass(fileName);
@@ -655,6 +673,7 @@ const ui = {
/**
* Get CSS special class for icon styling based on filename extension.
* Used as fallback when the backend DTO doesn't include icon_special_class.
* @param {string} fileName
*/
getIconSpecialClass(fileName) {
return uiFileTypes.getIconSpecialClass(fileName);
@@ -695,13 +714,13 @@ const ui = {
* Data store + event delegation (replaces per-item listeners)
* ================================================================ */
/** @type {Map<string, Object>} item data keyed by id */
/** @type {Map<string, FolderItem | FileItem>} item data keyed by id */
_items: new Map(),
/** @type {Array<Object>} last rendered folder dataset */
/** @type {FolderItem[]} last rendered folder dataset */
_lastFolders: [],
/** @type {Array<Object>} last rendered file dataset */
/** @type {FileItem[]} last rendered file dataset */
_lastFiles: [],
/** @type {boolean} */
@@ -716,9 +735,7 @@ const ui = {
},
/**
*
* @param {Object[]} folders
* @returns
* @param {FolderItem[]} folders
*/
_renderFoldersToView(folders) {
if (!Array.isArray(folders) || folders.length === 0) return;
@@ -733,9 +750,7 @@ const ui = {
},
/**
*
* @param {Object[]} files
* @returns
* @param {FileItem[]} files
*/
_renderFilesToView(files) {
if (!Array.isArray(files) || files.length === 0) return;
@@ -749,6 +764,10 @@ const ui = {
target.appendChild(frag);
},
/**
* @param {any[]} arr
* @param {any} item
*/
_upsertById(arr, item) {
if (!Array.isArray(arr) || !item?.id) return;
const idx = arr.findIndex((x) => x && x.id === item.id);
@@ -796,6 +815,7 @@ const ui = {
await fileOps.moveFile(sourceId, targetFolderId);
*/
/** @type {BatchResult} */
let result;
switch (action) {
case 'copy':
@@ -843,6 +863,7 @@ const ui = {
this._delegationReady = true;
// ── helpers ────────────────────────────────────────────────
/** @param {HTMLDivElement} card */
const itemInfo = (card) => {
if (!card) return null;
const fileId = card.dataset.fileId;
@@ -864,6 +885,7 @@ const ui = {
return null;
};
/** @param {FileItem} file */
const openFile = async (file) => {
if (!file) return;
if (recent) {
@@ -897,6 +919,7 @@ const ui = {
}
};
/** @param {HTMLElement} card */
const navigateFolder = (card) => {
const folderId = card.dataset.folderId;
const folderName = card.dataset.folderName;
@@ -912,27 +935,31 @@ const ui = {
loadFiles();
};
/**
* @param {HTMLElement} card
* @param {{ type: string, id: string, name: string | undefined, data: FolderItem | FileItem | undefined }} info
*/
const setContextTarget = (card, info) => {
if (info.type === 'folder') {
app.contextMenuTargetFolder = {
app.contextMenuTargetFolder = /** @type {FolderItem} */ ({
id: info.id,
name: card.dataset.folderName,
parent_id: card.dataset.parentId || ''
};
});
} else {
const fileData = info.data || this._items.get(info.id);
app.contextMenuTargetFile = {
const fileData = /** @type {FileItem | undefined} */ (info.data || this._items.get(info.id));
app.contextMenuTargetFile = /** @type {FileItem} */ ({
id: info.id,
name: card.dataset.fileName,
folder_id: card.dataset.folderId || '',
mime_type: fileData?.mime_type || null
};
});
}
};
// ── click (open / navigate; select only via checkbox) ──
filesList.addEventListener('click', (e) => {
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
if (!card) return;
if (/** @type {HTMLElement} */ (e.target).closest('.file-actions')) {
@@ -975,7 +1002,7 @@ const ui = {
if (info.type === 'folder') {
navigateFolder(card);
} else {
openFile(info.data);
openFile(/** @type {FileItem} */ (info.data));
}
});
@@ -989,7 +1016,7 @@ const ui = {
// ── shared events ──────────────────────
filesList.addEventListener('contextmenu', (e) => {
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
if (!card) return;
e.preventDefault();
const info = itemInfo(card);
@@ -1008,7 +1035,7 @@ const ui = {
// dragstart
filesList.addEventListener('dragstart', (e) => {
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
if (!card) {
e.preventDefault();
return;
@@ -1088,9 +1115,11 @@ const ui = {
// TODO better naming like ("selection in ${parent.name}") modulo i18n ? ...
const now = new Date().toISOString().replace(/T/, ' ').replace(/\.*/, '').replaceAll(/:/g, '-');
nameEncoded = `oxicloud ${now}.zip`;
/** @type {string[]} */
const folders = [];
/** @type {string[]} */
const files = [];
filesList.querySelectorAll(`div.selected`).forEach((e) => {
/** @type {NodeListOf<HTMLDivElement>} */ (filesList.querySelectorAll(`div.selected`)).forEach((e) => {
const item = itemInfo(e);
if (item.type === 'file') {
files.push(item.id);
@@ -1164,6 +1193,7 @@ const ui = {
* Favorite star helper – attaches a direct click handler to a
* star <button> so the event never bubbles to the card.
* ================================================================ */
/** @param {HTMLElement} el */
_bindStarClick(el) {
const star = el.querySelector('.favorite-star');
star?.addEventListener('click', (e) => {
@@ -1174,7 +1204,8 @@ const ui = {
if (!favorites) return;
// FIXME: make a function
const itemElement = shared?.closest('.file-item');
const itemElement = /** @type {HTMLElement | null} */ (shared?.closest('.file-item'));
if (!itemElement) return;
const itemId = itemElement.dataset.fileId ? itemElement.dataset.fileId : itemElement.dataset.folderId;
const itemType = itemElement.dataset.fileId ? 'file' : 'folder';
@@ -1187,7 +1218,7 @@ const ui = {
favorites.removeFromFavorites(itemId, itemType);
} else {
this.setFavoriteVisualState(itemId, itemType, true);
favorites.addToFavorites(itemId, itemName, itemType);
favorites.addToFavorites(itemId, itemName, itemType, null);
}
// Keep context-menu label in sync if available
@@ -1201,26 +1232,30 @@ const ui = {
e.preventDefault();
// FIXME: make a function
const itemElement = shared?.closest('.file-item');
const itemElement = /** @type {HTMLElement | null} */ (shared?.closest('.file-item'));
if (!itemElement) return;
const itemId = itemElement.dataset.fileId ? itemElement.dataset.fileId : itemElement.dataset.folderId;
const itemType = itemElement.dataset.fileId ? 'file' : 'folder';
const itemName = itemElement.dataset.fileId ? itemElement.dataset.fileName : itemElement.dataset.folderName;
// TODO corrently dirty
const item = {
const item = /** @type {unknown} */ ({
id: itemId,
item_id: itemId,
item_type: itemType,
item_name: itemName
};
});
contextMenus.showShareDialog(item, itemType);
contextMenus.showShareDialog(/** @type {FileItem} */ (item), itemType);
});
},
/**
* Sync favorite visuals for a file/folder across grid and list views.
* @param {string} itemId
* @param {string} itemType
* @param {boolean} isFavorite
*/
setFavoriteVisualState(itemId, itemType, isFavorite) {
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
@@ -1258,6 +1293,11 @@ const ui = {
}
},
/**
* @param {string} itemId
* @param {string} itemType
* @param {boolean} isShared
*/
setSharedVisualState(itemId, itemType, isShared) {
console.log(`setSharedVisual call for ${itemId} ${itemType} to ${isShared}`);
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
@@ -1273,7 +1313,10 @@ const ui = {
* Element-creation helpers
* ================================================================ */
/** Create a list row for a folder */
/**
* Create a list row for a folder
* @param {FolderItem} folder
*/
_createFolderItem(folder) {
const el = document.createElement('div');
el.className = 'file-item';
@@ -1314,7 +1357,10 @@ const ui = {
return el;
},
/** Create a grid card for a file */
/**
* Create a grid card for a file
* @param {FileItem} file
*/
_createFileItem(file) {
const iconClass = file.icon_class || this.getIconClass(file.name);
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
@@ -1425,7 +1471,7 @@ const ui = {
* Render an array of folders into both grid and list views
* using DocumentFragment for minimal reflows.
*
* @param {FolderInfo[]} folders
* @param {FolderItem[]} folders
*/
renderFolders(folders) {
if (!this._delegationReady) this.initDelegation();
@@ -1442,6 +1488,7 @@ const ui = {
/**
* Render an array of files into both grid and list views
* using DocumentFragment for minimal reflows.
* @param {FileItem[]} files
*/
renderFiles(files) {
if (!this._delegationReady) this.initDelegation();
@@ -1461,7 +1508,7 @@ const ui = {
/**
* Add a single folder to the active view.
* @param {Object} folder - Folder object
* @param {FolderItem} folder
*/
addFolderToView(folder) {
if (!this._delegationReady) this.initDelegation();
@@ -1479,7 +1526,7 @@ const ui = {
/**
* Add a single file to the active view.
* @param {Object} file - File object
* @param {FileItem} file
*/
addFileToView(file) {
if (!this._delegationReady) this.initDelegation();
@@ -1501,6 +1548,8 @@ const ui = {
/**
* Toggle selection state of a file/folder card.
* Routes through the multiSelect module so batch actions know about selected items.
* @param {HTMLDivElement} card
* @param {MouseEvent} event
*/
function toggleCardSelection(card, event) {
if (multiSelect) {
@@ -1512,6 +1561,8 @@ function toggleCardSelection(card, event) {
/**
* Show the context menu anchored next to a trigger element (the 3-dot button).
* @param {HTMLElement} triggerElement
* @param {string} menuId
*/
function showContextMenuAtElement(triggerElement, menuId) {
// Hide any open menus first
@@ -1542,8 +1593,6 @@ function showContextMenuAtElement(triggerElement, menuId) {
menu.classList.remove('hidden');
}
let __rubberBandJustFinished = false;
/**
* Rubber band (lasso) selection — click + drag on empty grid area
* to draw a rectangle and select all cards it touches.
@@ -1567,16 +1616,18 @@ function initRubberBandSelection() {
if (!container) return;
container.addEventListener('mousedown', (e) => {
if (!(e instanceof MouseEvent)) return;
// Only start if clicking empty area (not on a card, button, menu, input…)
if (e.button !== 0) return; // left click only
const target = /** @type {Element} */ (e.target);
if (
e.target.closest('.file-item') ||
e.target.closest('.context-menu') ||
e.target.closest('.upload-dropdown') ||
e.target.closest('button') ||
e.target.closest('input') ||
e.target.closest('.breadcrumb') ||
e.target.closest('.list-header')
target.closest('.file-item') ||
target.closest('.context-menu') ||
target.closest('.upload-dropdown') ||
target.closest('button') ||
target.closest('input') ||
target.closest('.breadcrumb') ||
target.closest('.list-header')
)
return;
@@ -1627,14 +1678,14 @@ function initRubberBandSelection() {
// Sync with multiSelect module
if (multiSelect) {
const info = multiSelect._extractInfo(card);
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
if (info) multiSelect.select(info.id, info.name, info.type, info.parentId);
}
} else {
card.classList.remove('selected');
// Deselect from multiSelect module
if (multiSelect) {
const info = multiSelect._extractInfo(card);
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
if (info) multiSelect.deselect(info.id);
}
}
@@ -1651,10 +1702,7 @@ function initRubberBandSelection() {
// Suppress the click event that follows mouseup so the global
// deselect handler doesn't immediately clear the selection.
if (hadSelection) {
__rubberBandJustFinished = true;
requestAnimationFrame(() => {
__rubberBandJustFinished = false;
});
requestAnimationFrame(() => {});
}
});
}
@@ -1709,6 +1757,7 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
overlay.classList.add('active');
});
/** @param {boolean} result */
const cleanup = (result) => {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 200);