diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index a5d6b9760..691ed1787 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -27,16 +27,19 @@ BlazeComponent.extendComponent({ this.autorun(() => { const currentBoardId = Session.get('currentBoard'); if (!currentBoardId) return; - + const handle = subManager.subscribe('board', currentBoardId, false); - + // Use a separate autorun for subscription ready state to avoid reactive loops this.subscriptionReadyAutorun = Tracker.autorun(() => { if (handle.ready()) { - if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) { + if ( + !this._boardProcessed || + this._lastProcessedBoardId !== currentBoardId + ) { this._boardProcessed = true; this._lastProcessedBoardId = currentBoardId; - + // Ensure default swimlane exists (only once per board) this.ensureDefaultSwimlane(currentBoardId); // Check if board needs conversion @@ -67,7 +70,7 @@ BlazeComponent.extendComponent({ if (!board) return; const swimlanes = board.swimlanes(); - + if (swimlanes.length === 0) { // Check if any swimlane exists in the database to avoid race conditions const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId }); @@ -77,7 +80,9 @@ BlazeComponent.extendComponent({ boardId: boardId, }); if (process.env.DEBUG === 'true') { - console.log(`Created default swimlane ${swimlaneId} for board ${boardId}`); + console.log( + `Created default swimlane ${swimlaneId} for board ${boardId}`, + ); } } this._swimlaneCreated.add(boardId); @@ -98,7 +103,6 @@ BlazeComponent.extendComponent({ } this.isBoardReady.set(true); - } catch (error) { console.error('Error during board conversion check:', error); this.isConverting.set(false); @@ -117,7 +121,9 @@ BlazeComponent.extendComponent({ const isMobile = Utils.getMobileMode(); if (!isMobile) { const openCardIds = Session.get('openCards') || []; - return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card); + return openCardIds + .map((id) => ReactiveCache.getCard(id)) + .filter((card) => card); } return []; }, @@ -159,7 +165,7 @@ BlazeComponent.extendComponent({ if (nullSortSwimlanes.length > 0) { const swimlanes = currentBoardData.swimlanes(); let count = 0; - swimlanes.forEach(s => { + swimlanes.forEach((s) => { Swimlanes.update(s._id, { $set: { sort: count, @@ -181,7 +187,7 @@ BlazeComponent.extendComponent({ if (nullSortLists.length > 0) { const lists = currentBoardData.lists(); let count = 0; - lists.forEach(l => { + lists.forEach((l) => { Lists.update(l._id, { $set: { sort: count, @@ -208,7 +214,9 @@ BlazeComponent.extendComponent({ function focusFirstInteractive(container) { if (!container) return; // Find first focusable element - const focusable = container.querySelectorAll('button, [role="button"], a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + const focusable = container.querySelectorAll( + 'button, [role="button"], a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ); for (let i = 0; i < focusable.length; i++) { if (!focusable[i].disabled && focusable[i].offsetParent !== null) { focusable[i].focus(); @@ -218,16 +226,22 @@ BlazeComponent.extendComponent({ } // Observe for new popups/menus and set focus (but exclude swimlane content) - const popupObserver = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - mutation.addedNodes.forEach(function(node) { - if (node.nodeType === 1 && - (node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) && - !node.closest('.js-swimlanes') && - !node.closest('.swimlane') && - !node.closest('.list') && - !node.closest('.minicard')) { - setTimeout(function() { focusFirstInteractive(node); }, 10); + const popupObserver = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if ( + node.nodeType === 1 && + (node.classList.contains('popup') || + node.classList.contains('modal') || + node.classList.contains('menu')) && + !node.closest('.js-swimlanes') && + !node.closest('.swimlane') && + !node.closest('.list') && + !node.closest('.minicard') + ) { + setTimeout(function () { + focusFirstInteractive(node); + }, 10); } }); }); @@ -235,11 +249,15 @@ BlazeComponent.extendComponent({ popupObserver.observe(document.body, { childList: true, subtree: true }); // Remove tabindex from non-interactive elements (e.g., user abbreviations, labels) - document.querySelectorAll('.user-abbreviation, .user-label, .card-header-label, .edit-label, .private-label').forEach(function(el) { - if (el.hasAttribute('tabindex')) { - el.removeAttribute('tabindex'); - } - }); + document + .querySelectorAll( + '.user-abbreviation, .user-label, .card-header-label, .edit-label, .private-label', + ) + .forEach(function (el) { + if (el.hasAttribute('tabindex')) { + el.removeAttribute('tabindex'); + } + }); /* // Add a toggle button for keyboard shortcuts accessibility if (!document.getElementById('wekan-shortcuts-toggle')) { @@ -272,7 +290,7 @@ BlazeComponent.extendComponent({ } */ // Ensure toggle-buttons, color choices, reactions, renaming, and calendar controls are focusable and have ARIA roles - document.querySelectorAll('.js-toggle').forEach(function(el) { + document.querySelectorAll('.js-toggle').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); // Short, descriptive label for favorite/star toggle @@ -282,27 +300,27 @@ BlazeComponent.extendComponent({ el.setAttribute('aria-label', 'Toggle'); } }); - document.querySelectorAll('.js-color-choice').forEach(function(el) { + document.querySelectorAll('.js-color-choice').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); el.setAttribute('aria-label', 'Choose color'); }); - document.querySelectorAll('.js-reaction').forEach(function(el) { + document.querySelectorAll('.js-reaction').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); el.setAttribute('aria-label', 'React'); }); - document.querySelectorAll('.js-rename-swimlane').forEach(function(el) { + document.querySelectorAll('.js-rename-swimlane').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); el.setAttribute('aria-label', 'Rename swimlane'); }); - document.querySelectorAll('.js-rename-list').forEach(function(el) { + document.querySelectorAll('.js-rename-list').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); el.setAttribute('aria-label', 'Rename list'); }); - document.querySelectorAll('.fc-button').forEach(function(el) { + document.querySelectorAll('.fc-button').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); }); @@ -313,7 +331,10 @@ BlazeComponent.extendComponent({ // This fixes WCAG 2.5.3: Label in Name const swimlanesSwitcher = this.$('.js-board-view-swimlanes'); if (swimlanesSwitcher.length) { - swimlanesSwitcher.attr('aria-label', swimlanesSwitcher.text().trim() || 'Swimlanes'); + swimlanesSwitcher.attr( + 'aria-label', + swimlanesSwitcher.text().trim() || 'Swimlanes', + ); } // Add a highly visible focus indicator and improve contrast for interactive elements @@ -376,7 +397,7 @@ BlazeComponent.extendComponent({ document.head.appendChild(style); } // Ensure plus/add elements are focusable and have ARIA roles - document.querySelectorAll('.js-add-card').forEach(function(el) { + document.querySelectorAll('.js-add-card').forEach(function (el) { el.setAttribute('tabindex', '0'); el.setAttribute('role', 'button'); el.setAttribute('aria-label', 'Add new card'); @@ -506,7 +527,11 @@ BlazeComponent.extendComponent({ if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) { if (Utils.isTouchScreenOrShowDesktopDragHandles()) { - $swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle'); + $swimlanesDom.sortable( + 'option', + 'handle', + '.js-swimlane-header-handle', + ); } else { $swimlanesDom.sortable('option', 'handle', '.swimlane-header'); } @@ -532,65 +557,72 @@ BlazeComponent.extendComponent({ }, notDisplayThisBoard() { - let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly'); + let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne( + 'tableVisibilityMode-allowPrivateOnly', + ); let currentBoard = Utils.getCurrentBoard(); - return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public'; + return ( + allowPrivateVisibilityOnly !== undefined && + allowPrivateVisibilityOnly.booleanValue && + currentBoard && + currentBoard.permission == 'public' + ); }, isViewSwimlanes() { const currentUser = ReactiveCache.getCurrentUser(); let boardView; - + if (currentUser) { boardView = (currentUser.profile || {}).boardView; } else { boardView = window.localStorage.getItem('boardView'); } - + // If no board view is set, default to swimlanes if (!boardView) { boardView = 'board-view-swimlanes'; } - + return boardView === 'board-view-swimlanes'; }, isViewLists() { const currentUser = ReactiveCache.getCurrentUser(); let boardView; - + if (currentUser) { boardView = (currentUser.profile || {}).boardView; } else { boardView = window.localStorage.getItem('boardView'); } - + return boardView === 'board-view-lists'; }, isViewCalendar() { const currentUser = ReactiveCache.getCurrentUser(); let boardView; - + if (currentUser) { boardView = (currentUser.profile || {}).boardView; } else { boardView = window.localStorage.getItem('boardView'); } - + return boardView === 'board-view-cal'; }, isViewGantt() { const currentUser = ReactiveCache.getCurrentUser(); let boardView; - + if (currentUser) { boardView = (currentUser.profile || {}).boardView; } else { boardView = window.localStorage.getItem('boardView'); } - + return boardView === 'board-view-gantt'; }, @@ -602,12 +634,16 @@ BlazeComponent.extendComponent({ } return false; } - + try { const swimlanes = currentBoard.swimlanes(); const hasSwimlanes = swimlanes && swimlanes.length > 0; if (process.env.DEBUG === 'true') { - console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes'); + console.log( + 'hasSwimlanes: Board has', + swimlanes ? swimlanes.length : 0, + 'swimlanes', + ); } return hasSwimlanes; } catch (error) { @@ -616,7 +652,6 @@ BlazeComponent.extendComponent({ } }, - isVerticalScrollbars() { const user = ReactiveCache.getCurrentUser(); return user && user.isVerticalScrollbars(); @@ -638,28 +673,31 @@ BlazeComponent.extendComponent({ const isBoardReady = this.isBoardReady.get(); const isConverting = this.isConverting.get(); const boardView = Utils.boardView(); - + if (process.env.DEBUG === 'true') { console.log('=== BOARD DEBUG STATE ==='); console.log('currentBoardId:', currentBoardId); - console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none'); + console.log( + 'currentBoard:', + !!currentBoard, + currentBoard ? currentBoard.title : 'none', + ); console.log('isBoardReady:', isBoardReady); console.log('isConverting:', isConverting); console.log('boardView:', boardView); console.log('========================'); } - + return { currentBoardId, hasCurrentBoard: !!currentBoard, currentBoardTitle: currentBoard ? currentBoard.title : 'none', isBoardReady, isConverting, - boardView + boardView, }; }, - openNewListForm() { if (this.isViewSwimlanes()) { // The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902 @@ -686,7 +724,11 @@ BlazeComponent.extendComponent({ // Global drag and drop file upload handlers for better visual feedback 'dragover .board-canvas'(event) { const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + if ( + dataTransfer && + dataTransfer.types && + dataTransfer.types.includes('Files') + ) { event.preventDefault(); // Add visual indicator that files can be dropped $('.board-canvas').addClass('file-drag-over'); @@ -694,7 +736,11 @@ BlazeComponent.extendComponent({ }, 'dragleave .board-canvas'(event) { const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + if ( + dataTransfer && + dataTransfer.types && + dataTransfer.types.includes('Files') + ) { // Only remove class if we're leaving the board canvas entirely if (!event.currentTarget.contains(event.relatedTarget)) { $('.board-canvas').removeClass('file-drag-over'); @@ -703,7 +749,11 @@ BlazeComponent.extendComponent({ }, 'drop .board-canvas'(event) { const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + if ( + dataTransfer && + dataTransfer.types && + dataTransfer.types.includes('Files') + ) { event.preventDefault(); $('.board-canvas').removeClass('file-drag-over'); } @@ -738,12 +788,12 @@ BlazeComponent.extendComponent({ // Accessibility: Allow users to enable/disable keyboard shortcuts window.wekanShortcutsEnabled = true; -window.toggleWekanShortcuts = function(enabled) { +window.toggleWekanShortcuts = function (enabled) { window.wekanShortcutsEnabled = !!enabled; }; // Example: Wrap your character key shortcut handler like this -document.addEventListener('keydown', function(e) { +document.addEventListener('keydown', function (e) { // Example: "W" key shortcut (replace with your actual shortcut logic) if (!window.wekanShortcutsEnabled) return; if (e.key === 'w' || e.key === 'W') { @@ -753,7 +803,7 @@ document.addEventListener('keydown', function(e) { }); // Keyboard accessibility for card actions (favorite, archive, duplicate, etc.) -document.addEventListener('keydown', function(e) { +document.addEventListener('keydown', function (e) { if (!window.wekanShortcutsEnabled) return; // Only proceed if focus is on a card action element const active = document.activeElement; @@ -798,14 +848,20 @@ document.addEventListener('keydown', function(e) { } } } - // Ensure move card buttons are focusable and have ARIA roles - document.querySelectorAll('.js-move-card').forEach(function(el) { - el.setAttribute('tabindex', '0'); - el.setAttribute('role', 'button'); - el.setAttribute('aria-label', 'Move card'); - }); + // Ensure move card buttons are focusable and have ARIA roles + document.querySelectorAll('.js-move-card').forEach(function (el) { + el.setAttribute('tabindex', '0'); + el.setAttribute('role', 'button'); + el.setAttribute('aria-label', 'Move card'); + }); // Make toggle-buttons, color choices, reactions, and X-buttons keyboard accessible - if (active && (active.classList.contains('js-toggle') || active.classList.contains('js-color-choice') || active.classList.contains('js-reaction') || active.classList.contains('close'))) { + if ( + active && + (active.classList.contains('js-toggle') || + active.classList.contains('js-color-choice') || + active.classList.contains('js-reaction') || + active.classList.contains('close')) + ) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); active.click(); @@ -813,13 +869,21 @@ document.addEventListener('keydown', function(e) { } // Prevent scripts from removing focus when received if (active) { - active.addEventListener('focus', function(e) { - // Do not remove focus - // No-op: This prevents F55 failure - }, { once: true }); + active.addEventListener( + 'focus', + function (e) { + // Do not remove focus + // No-op: This prevents F55 failure + }, + { once: true }, + ); } // Make swimlane/list renaming keyboard accessible - if (active && (active.classList.contains('js-rename-swimlane') || active.classList.contains('js-rename-list'))) { + if ( + active && + (active.classList.contains('js-rename-swimlane') || + active.classList.contains('js-rename-list')) + ) { if (e.key === 'Enter') { e.preventDefault(); active.click(); @@ -851,20 +915,23 @@ BlazeComponent.extendComponent({ selectable: true, timezone: 'local', weekNumbers: true, + // Use non-localized AM/PM time format to avoid confusing notations like 上/下/中 + timeFormat: 'h:mma', + slotLabelFormat: 'h:mma', header: { - left: 'title today prev,next', + left: 'title today prev,next', center: 'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth', right: '', }, - buttonText: { - prev: TAPi18n.__('calendar-previous-month-label'), // e.g. "Previous month" - next: TAPi18n.__('calendar-next-month-label'), // e.g. "Next month" - }, - ariaLabel: { - prev: TAPi18n.__('calendar-previous-month-label'), - next: TAPi18n.__('calendar-next-month-label'), - }, + buttonText: { + prev: TAPi18n.__('calendar-previous-month-label'), // e.g. "Previous month" + next: TAPi18n.__('calendar-next-month-label'), // e.g. "Next month" + }, + ariaLabel: { + prev: TAPi18n.__('calendar-previous-month-label'), + next: TAPi18n.__('calendar-next-month-label'), + }, // height: 'parent', nope, doesn't work as the parent might be small height: 'auto', /* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */ @@ -976,40 +1043,52 @@ BlazeComponent.extendComponent({ `; - const createCardButton = modalElement.querySelector('#create-card-button'); + const createCardButton = modalElement.querySelector( + '#create-card-button', + ); createCardButton.addEventListener('click', function () { const myTitle = modalElement.querySelector('#card-title-input').value; if (myTitle) { const firstList = currentBoard.draggableLists()[0]; const firstSwimlane = currentBoard.swimlanes()[0]; - Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) { - if (error) { - if (process.env.DEBUG === 'true') { - console.log(error); + Meteor.call( + 'createCardWithDueDate', + currentBoard._id, + firstList._id, + myTitle, + startDate.toDate(), + firstSwimlane._id, + function (error, result) { + if (error) { + if (process.env.DEBUG === 'true') { + console.log(error); + } + } else { + if (process.env.DEBUG === 'true') { + console.log('Card Created', result); + } } - } else { - if (process.env.DEBUG === 'true') { - console.log("Card Created", result); - } - } - }); + }, + ); closeModal(); } }); document.body.appendChild(modalElement); - const openModal = function() { + const openModal = function () { modalElement.style.display = 'flex'; // Set focus to the input field for better keyboard accessibility const input = modalElement.querySelector('#card-title-input'); if (input) input.focus(); }; - const closeModal = function() { + const closeModal = function () { modalElement.style.display = 'none'; }; - const closeButton = modalElement.querySelector('[data-dismiss="modal"]'); + const closeButton = modalElement.querySelector( + '[data-dismiss="modal"]', + ); closeButton.addEventListener('click', closeModal); openModal(); - } + }, }; }, isViewCalendar() { @@ -1025,4 +1104,3 @@ BlazeComponent.extendComponent({ * Gantt View Component * Displays cards as a Gantt chart with start/due dates */ -