diff --git a/app/board/boardLabels.js b/app/board/boardLabels.js index ebc8571..e8dcfda 100644 --- a/app/board/boardLabels.js +++ b/app/board/boardLabels.js @@ -1325,6 +1325,9 @@ function initializeBoardLabelControls() { if (!window.boardRoot) { return; } + if (typeof closeAllModals === 'function') { + await closeAllModals({ key: 'Escape' }); + } await ensureBoardLabelsLoaded(); openBoardSettingsModal(); }); diff --git a/app/board/boardTabs.js b/app/board/boardTabs.js index c18c817..a9632c8 100644 --- a/app/board/boardTabs.js +++ b/app/board/boardTabs.js @@ -8,11 +8,17 @@ function normalizeBoardPath(dir) { return ''; } - return dir.endsWith('/') ? dir : `${dir}/`; + const normalizedDir = dir.replace(/\\/g, '/').trim(); + if (!normalizedDir) { + return ''; + } + + return normalizedDir.endsWith('/') ? normalizedDir : `${normalizedDir}/`; } function getBoardLabelFromPath(boardPath) { - const pathParts = boardPath.split('/').filter(Boolean); + const normalizedPath = normalizeBoardPath(boardPath).replace(/\/+$/, ''); + const pathParts = normalizedPath.split('/').filter(Boolean); return pathParts[pathParts.length - 1] || 'Board'; } @@ -95,15 +101,19 @@ function ensureBoardInTabs(boardPath) { } function clearRenderedBoard() { + if (typeof setBoardChromeState === 'function') { + setBoardChromeState(false); + } + + if (typeof renderBoardEmptyState === 'function') { + renderBoardEmptyState(); + return; + } + const boardEl = document.getElementById('board'); if (boardEl) { boardEl.innerHTML = ''; } - - const boardNameEl = document.getElementById('boardName'); - if (boardNameEl) { - boardNameEl.textContent = 'No Board Open'; - } } async function closeBoardTab(boardPath) { @@ -151,17 +161,13 @@ async function closeBoardTab(boardPath) { await renderBoard(); } -function initializeBoardTabsSortable(tabsEl) { - if (!tabsEl || typeof Sortable !== 'function') { - return; - } - - if (boardTabsSortable && boardTabsSortable.el !== tabsEl) { +function initializeBoardTabsSortable(tabsEl, canSortTabs = true) { + if (boardTabsSortable && (!tabsEl || boardTabsSortable.el !== tabsEl || !canSortTabs)) { boardTabsSortable.destroy(); boardTabsSortable = null; } - if (boardTabsSortable) { + if (!canSortTabs || !tabsEl || typeof Sortable !== 'function' || boardTabsSortable) { return; } @@ -245,6 +251,12 @@ function renderBoardTabs() { const openBoards = getStoredOpenBoards(); tabsEl.innerHTML = ''; + if (openBoards.length === 0) { + tabsWrapper.classList.add('hidden'); + initializeBoardTabsSortable(null, false); + return; + } + tabsWrapper.classList.remove('hidden'); const activeBoard = normalizeBoardPath(window.boardRoot || getStoredActiveBoard()); @@ -329,5 +341,5 @@ function renderBoardTabs() { addBoardTab.appendChild(addBoardButton); tabsEl.appendChild(addBoardTab); - initializeBoardTabsSortable(tabsEl); + initializeBoardTabsSortable(tabsEl, openBoards.length > 1); } diff --git a/app/board/openBoard.js b/app/board/openBoard.js index 59eb67e..8115f25 100644 --- a/app/board/openBoard.js +++ b/app/board/openBoard.js @@ -39,22 +39,28 @@ async function openBoard( dir ) { const directories = await window.board.listDirectories( boardPath ); if ( directories.length == 0 ) { - await window.board.createList( boardPath + '000-To-do-stock'); - await window.board.createList( boardPath + '001-Doing-stock'); - await window.board.createList( boardPath + '002-Done-stock'); - await window.board.createList( boardPath + '003-On-hold-stock'); - await window.board.createList( boardPath + 'XXX-Archive'); + await Promise.all([ + window.board.createList(boardPath + '000-To-do-stock'), + window.board.createList(boardPath + '001-Doing-stock'), + window.board.createList(boardPath + '002-Done-stock'), + window.board.createList(boardPath + 'XXX-Archive'), + ]); await window.board.createCard( boardPath + '000-To-do-stock/000-hello-stock.md', `👋 Hello -Welcome to Signboard! This card is your first task. Tap on it to view more or edit. +Welcome to Signboard! + +Here are some fun things to get you started: - Create new cards by clicking the + button on any list -- Edit the title or notes on any card by tapping on it +- Edit the title or notes on any card by tapping on them - Reorder cards in a list or move them between lists by dragging them -- Archive a card by tapping on it and tapping the archive icon +- Archive a card by tapping the archive icon - Reorder lists by dragging them - Create new lists by clicking the "+ Add List" button +- Add due dates to cards +- Customize your labels in the Board Settings area +- Customize your light and dark color schemes per board ***Keyboard Shortcuts*** diff --git a/app/board/renderBoard.js b/app/board/renderBoard.js index c732ae3..f22a702 100644 --- a/app/board/renderBoard.js +++ b/app/board/renderBoard.js @@ -1,20 +1,94 @@ +function setBoardChromeState(hasOpenBoard) { + const body = document.body; + if (body) { + body.classList.toggle('board-empty', !hasOpenBoard); + } + + if (hasOpenBoard) { + return; + } + + const boardNameEl = document.getElementById('boardName'); + if (boardNameEl) { + boardNameEl.textContent = 'Signboard'; + } +} + +async function handleEmptyBoardCallToActionClick(buttonEl) { + if (!buttonEl || buttonEl.disabled) { + return; + } + + buttonEl.disabled = true; + try { + await promptAndOpenBoardFromTabs(); + } finally { + buttonEl.disabled = false; + } +} + +function createEmptyBoardCallToAction() { + const buttonEl = document.createElement('button'); + buttonEl.type = 'button'; + buttonEl.id = 'emptyBoardCallToAction'; + buttonEl.className = 'empty-board-cta'; + buttonEl.setAttribute('aria-label', 'Select a directory to create a board'); + buttonEl.innerHTML = ` + + Create your first board + Select an empty directory. Signboard will use the directory name as the board name. + `; + buttonEl.addEventListener('click', async () => { + await handleEmptyBoardCallToActionClick(buttonEl); + }); + return buttonEl; +} + +function renderBoardEmptyState() { + const boardEl = document.getElementById('board'); + if (!boardEl) { + return; + } + + boardEl.innerHTML = ''; + boardEl.appendChild(createEmptyBoardCallToAction()); +} + async function renderBoard() { - const boardRoot = window.boardRoot; // set in the drop‑zone handler - await ensureBoardLabelsLoaded(); + const boardRoot = window.boardRoot; // set in the drop-zone handler if (!boardRoot) { + setBoardChromeState(false); renderBoardTabs(); + renderBoardEmptyState(); return; } closeCardLabelPopover(); + setBoardChromeState(true); - const boardName = document.getElementById('boardName'); - boardName.textContent = await window.board.getBoardName( boardRoot ); + const boardNameEl = document.getElementById('boardName'); + const [boardName, lists] = await Promise.all([ + window.board.getBoardName(boardRoot), + window.board.listLists(boardRoot), + ensureBoardLabelsLoaded(), + ]); + + if (boardNameEl) { + boardNameEl.textContent = boardName; + } renderBoardTabs(); - const lists = await window.board.listLists(boardRoot); const boardEl = document.getElementById('board'); + if (!boardEl) { + return; + } boardEl.innerHTML = ''; const listsWithCards = await Promise.all( @@ -25,41 +99,44 @@ async function renderBoard() { }) ); - for (const { listName, listPath, cards } of listsWithCards) { - const listEl = await createListElement(listName, listPath, cards); + const listElements = await Promise.all( + listsWithCards.map(({ listName, listPath, cards }) => createListElement(listName, listPath, cards)) + ); + + for (const listEl of listElements) { boardEl.appendChild(listEl); } - // Enable SortableJS on this column - new Sortable(boardEl, { - group: 'lists', - animation: 150, - onEnd: async (evt) => { - - const finalOrder = [...evt.to.querySelectorAll('.list')].map(list => + if (typeof Sortable === 'function') { + // Enable SortableJS on this column + new Sortable(boardEl, { + group: 'lists', + animation: 150, + onEnd: async (evt) => { + const finalOrder = [...evt.to.querySelectorAll('.list')].map((list) => list.getAttribute('data-path') - ); + ); - let directoryCounter = 0; - for (const directoryPath of finalOrder) { - - let directoryNumber = (directoryCounter).toLocaleString('en-US', { - minimumIntegerDigits: 3, - useGrouping: false + let directoryCounter = 0; + for (const directoryPath of finalOrder) { + const directoryNumber = (directoryCounter).toLocaleString('en-US', { + minimumIntegerDigits: 3, + useGrouping: false }); - let newDirectoryName = window.boardRoot + directoryNumber + await window.board.getListDirectoryName(directoryPath).slice(3); + const newDirectoryName = window.boardRoot + directoryNumber + await window.board.getListDirectoryName(directoryPath).slice(3); await window.board.moveCard(directoryPath, newDirectoryName); directoryCounter++; - } - await renderBoard(); - - } - }); - feather.replace(); - return; + await renderBoard(); + } + }); + } + + if (typeof feather !== 'undefined' && feather && typeof feather.replace === 'function') { + feather.replace(); + } } diff --git a/app/init.js b/app/init.js index b13f378..7ddfe6b 100644 --- a/app/init.js +++ b/app/init.js @@ -1,11 +1,31 @@ -var turndown = new TurndownService(); -const renderMarkdown = (md) => marked.parse(md); - async function init() { - initializeBoardLabelControls(); - initializeBoardSearchControls(); - const restoredBoard = restoreBoardTabs(); + const initializeHeaderControls = () => { + initializeBoardLabelControls(); + initializeBoardSearchControls(); + }; + + if (!restoredBoard) { + window.boardRoot = ''; + if (typeof setBoardChromeState === 'function') { + setBoardChromeState(false); + } + + const emptyBoardCallToAction = document.getElementById('emptyBoardCallToAction'); + if (emptyBoardCallToAction) { + emptyBoardCallToAction.addEventListener('click', async () => { + if (typeof handleEmptyBoardCallToActionClick === 'function') { + await handleEmptyBoardCallToActionClick(emptyBoardCallToAction); + return; + } + await promptAndOpenBoardFromTabs(); + }); + } + + window.setTimeout(initializeHeaderControls, 0); + } else { + initializeHeaderControls(); + } if (restoredBoard) { window.boardRoot = restoredBoard; @@ -38,7 +58,8 @@ async function init() { await closeAllModals(e); }); - document.getElementById('btnAddNewList').addEventListener('click', async () => { + document.getElementById('btnAddNewList').addEventListener('click', async (e) => { + e.stopPropagation(); const listName = document.getElementById('userInputListName'); toggleAddListModal( (window.innerWidth / 2)-200, (window.innerHeight / 2)-100 ); listName.focus(); diff --git a/app/lists/createListElement.js b/app/lists/createListElement.js index 8a31b25..7fb9ea9 100644 --- a/app/lists/createListElement.js +++ b/app/lists/createListElement.js @@ -36,6 +36,7 @@ async function createListElement(name, listPath, cardNames) { addBtn.setAttribute('data-listpath', listPath + '/'); addBtn.setAttribute('class','btnOpenAddCardModal'); addBtn.addEventListener('click', async function (e) { + e.stopPropagation(); toggleAddCardModal( e.x-90, e.y+15 ); const userInput = document.getElementById('userInput'); diff --git a/app/modals/closeAllModals.js b/app/modals/closeAllModals.js index 49aa1ca..e1537e0 100644 --- a/app/modals/closeAllModals.js +++ b/app/modals/closeAllModals.js @@ -34,13 +34,42 @@ function resetCardEditorModalState() { } } +function isCardEditorRelatedClickTarget(target) { + if (!target || typeof target.closest !== 'function') { + return false; + } + + if (target.closest('#modalEditCard')) { + return true; + } + + if (target.closest('.card-label-popover')) { + return true; + } + + if (target.closest('.sb-themed-fdatepicker')) { + return true; + } + + if (target.closest('[data-fdatepicker="due-date-anchor"]')) { + return true; + } + + return false; +} + async function closeAllModals(e, options = {}){ - if (e.target.id != 'board' && e.key !== 'Escape') return; + const eventTarget = e && e.target ? e.target : null; + const isEscape = e && e.key === 'Escape'; + const isClick = e && e.type === 'click'; + const closeAllRequest = Boolean(eventTarget && eventTarget.id === 'board') || isEscape; - closeAllLabelPopovers(); + if (!closeAllRequest && !isClick) { + return; + } const shouldRerender = Boolean(options.rerender); - + const modalAddCard = document.getElementById('modalAddCard'); const modalEditCard = document.getElementById('modalEditCard'); const modalAddCardToList = document.getElementById('modalAddCardToList'); @@ -49,22 +78,25 @@ async function closeAllModals(e, options = {}){ const editModalWasOpen = modalEditCard.style.display === 'block'; const boardSettingsWasOpen = modalBoardSettings && modalBoardSettings.style.display === 'block'; - if (editModalWasOpen && typeof flushEditorSaveIfNeeded === 'function') { - await flushEditorSaveIfNeeded(); - } + let editModalClosed = false; + let boardSettingsClosed = false; - if (boardSettingsWasOpen && typeof flushBoardLabelSettingsSave === 'function') { - await flushBoardLabelSettingsSave(); + if (closeAllRequest && typeof closeAllLabelPopovers === 'function') { + closeAllLabelPopovers(); } - if ( e.target.id == 'board' || e.key == 'Escape' ) { + if ( closeAllRequest ) { if ( modalAddCard.style.display === 'block' ) { modalAddCard.style.display = 'none'; } if ( modalEditCard.style.display === 'block' ) { + if (typeof flushEditorSaveIfNeeded === 'function') { + await flushEditorSaveIfNeeded(); + } modalEditCard.style.display = 'none'; resetCardEditorModalState(); setBoardInteractive(true); + editModalClosed = true; } if ( modalAddCardToList.style.display === 'block' ) { modalAddCardToList.style.display = 'none'; @@ -75,38 +107,58 @@ async function closeAllModals(e, options = {}){ setBoardInteractive(true); } if ( modalBoardSettings && modalBoardSettings.style.display === 'block' ) { + if (typeof flushBoardLabelSettingsSave === 'function') { + await flushBoardLabelSettingsSave(); + } modalBoardSettings.style.display = 'none'; setBoardInteractive(true); + boardSettingsClosed = true; } } else { - if ( modalAddCard.style.display === 'block' && !modalAddCard.contains(e.target) ) { + if ( modalAddCard.style.display === 'block' && eventTarget && !modalAddCard.contains(eventTarget) ) { modalAddCard.style.display = 'none'; } - if ( modalEditCard.style.display === 'block' && !modalEditCard.contains(e.target) ) { + const clickIsInsideCardEditor = isCardEditorRelatedClickTarget(eventTarget); + if ( modalEditCard.style.display === 'block' && !clickIsInsideCardEditor ) { + if (typeof flushEditorSaveIfNeeded === 'function') { + await flushEditorSaveIfNeeded(); + } modalEditCard.style.display = 'none'; resetCardEditorModalState(); setBoardInteractive(true); + editModalClosed = true; } - if ( modalAddCardToList.style.display === 'block' && !modalAddCardToList.contains(e.target) ) { + if ( modalAddCardToList.style.display === 'block' && eventTarget && !modalAddCardToList.contains(eventTarget) ) { modalAddCardToList.style.display = 'none'; setBoardInteractive(true); } - if ( modalBoardSettings && modalBoardSettings.style.display === 'block' && !modalBoardSettings.contains(e.target) ) { + if ( modalAddList.style.display === 'block' && eventTarget && !modalAddList.contains(eventTarget) ) { + modalAddList.style.display = 'none'; + setBoardInteractive(true); + } + + if ( modalBoardSettings && modalBoardSettings.style.display === 'block' && eventTarget && !modalBoardSettings.contains(eventTarget) ) { + if (typeof flushBoardLabelSettingsSave === 'function') { + await flushBoardLabelSettingsSave(); + } modalBoardSettings.style.display = 'none'; setBoardInteractive(true); + boardSettingsClosed = true; } } - FDatepicker.destroyAll(); - OverType.destroyAll(); - if (editModalWasOpen && typeof clearQueuedEditorSave === 'function') { - clearQueuedEditorSave(); + if (editModalClosed) { + FDatepicker.destroyAll(); + OverType.destroyAll(); + if (typeof clearQueuedEditorSave === 'function') { + clearQueuedEditorSave(); + } } - if (shouldRerender || editModalWasOpen || boardSettingsWasOpen) { + if (shouldRerender || editModalClosed || boardSettingsClosed) { await renderBoard(); } } diff --git a/index.html b/index.html index 23f9aa8..4eceeb8 100644 --- a/index.html +++ b/index.html @@ -5,18 +5,16 @@ Signboard - - - +
-

BoardName

+

Signboard

@@ -41,7 +39,19 @@

BoardName

-
+
+ +