diff --git a/src/list.jsx b/src/list.jsx index 5c48abd..49d53b8 100644 --- a/src/list.jsx +++ b/src/list.jsx @@ -1,22 +1,15 @@ import { Filesystem } from '@capacitor/filesystem'; import { getPaths } from './get-data.js'; import React, { useState, useEffect, useCallback } from 'react'; -import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; -import { chevronDown } from '@wordpress/icons'; +import { ToolbarButton, ToolbarGroup, Button } from '@wordpress/components'; +import { addCard, archive } from '@wordpress/icons'; +import { useResizeObserver } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { v4 as uuidv4 } from 'uuid'; -import { Read, Write, getTitleFromBlocks } from './read-write.js'; -import Editor from './editor.jsx'; - -function Title({ item: { path, blocks } }) { - if (blocks) { - return getTitleFromBlocks(blocks) || {__('Untitled')}; - } - - const title = path?.replace(/(?:\.?[0-9]+)?\.html$/, ''); - return title ? decodeURIComponent(title) : {__('Untitled')}; -} +import { Read, Write } from './read-write'; +import Editor from './editor'; +import Sidebar from './sidebar.jsx'; function getInitialSelection({ path, blocks }) { if (path) { @@ -37,6 +30,7 @@ function getInitialSelection({ path, blocks }) { export default function Frame({ selectedFolderURL, setSelectedFolderURL }) { const [currentId, setCurrentId] = useState(); const [items, setItems] = useState([]); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const setItem = useCallback((id, item) => { setItems((_items) => @@ -66,6 +60,8 @@ export default function Frame({ selectedFolderURL, setSelectedFolderURL }) { }); }, [selectedFolderURL, setSelectedFolderURL, setCurrentId]); + const [observer, { width }] = useResizeObserver(); + if (!currentId) { return null; } @@ -74,106 +70,167 @@ export default function Frame({ selectedFolderURL, setSelectedFolderURL }) { return ( <> -
- + + + +
+
900 + ? 'calc(100% - 300px)' + : '', + }} + > + {observer} +
+ + { + setIsSidebarOpen(!isSidebarOpen); + }} + /> + + + { + const newItem = { id: uuidv4() }; + setItems([newItem, ...items]); + setCurrentId(newItem.id); + setItem(currentId, { + blocks: null, + }); + }} + /> + + {/* + {({ onClose }) => ( + <> + { - setCurrentId(item.id); + const newItem = { id: uuidv4() }; + setItems([newItem, ...items]); + setCurrentId(newItem.id); setItem(currentId, { blocks: null, }); onClose(); }} - className={ - item.id === currentId - ? 'is-active' - : '' - } > - + {__('New Note')} + </MenuItem> + </MenuGroup> + <MenuGroup> + {items.map((item) => ( + <MenuItem + key={item.id} + onClick={() => { + setCurrentId(item.id); + setItem(currentId, { + blocks: null, + }); + onClose(); + }} + className={ + item.id === currentId + ? 'is-active' + : '' + } + > + <Title item={item} /> + </MenuItem> + ))} + </MenuGroup> + <MenuGroup> + <MenuItem + onClick={async () => { + const { url } = + await Filesystem.pickDirectory(); + setSelectedFolderURL(url); + onClose(); + }} + > + {__('Pick Folder')} </MenuItem> - ))} - </MenuGroup> - <MenuGroup> - <MenuItem - onClick={async () => { - const { url } = - await Filesystem.pickDirectory(); - setSelectedFolderURL(url); - onClose(); - }} - > - {__('Pick Folder')} - </MenuItem> - <MenuItem - onClick={() => { - setSelectedFolderURL(); - }} - > - {__('Forget Folder')} - </MenuItem> - </MenuGroup> - </> + <MenuItem + onClick={() => { + setSelectedFolderURL(); + }} + > + {__('Forget Folder')} + </MenuItem> + </MenuGroup> + </> + )} + </DropdownMenu> */} + </div> + <div + id="editor" + style={{ + position: 'relative', + overflow: 'auto', + height: '100%', + width: '100%', + display: 'flex', + flexDirection: 'column', + }} + > + {currentItem.blocks && ( + <Editor + // Remount the editor when the item changes. + key={currentItem.id} + initialState={{ + blocks: currentItem.blocks, + selection: getInitialSelection(currentItem), + }} + setBlocks={(blocks) => { + setItem(currentItem.id, { blocks }); + }} + /> )} - </DropdownMenu> - </div> - <div - id="editor" - style={{ - position: 'relative', - overflow: 'auto', - height: '100%', - width: '100%', - display: 'flex', - flexDirection: 'column', - }} - > - {currentItem.blocks && ( - <Editor - // Remount the editor when the item changes. - key={currentItem.id} - initialState={{ - blocks: currentItem.blocks, - selection: getInitialSelection(currentItem), - }} - setBlocks={(blocks) => { - setItem(currentItem.id, { blocks }); - }} + </div> + {((ReadWrite) => ( + <ReadWrite + item={currentItem} + setItem={setItem} + selectedFolderURL={selectedFolderURL} /> - )} + ))(currentItem.blocks ? Write : Read)} </div> - {((ReadWrite) => ( - <ReadWrite - item={currentItem} - setItem={setItem} - selectedFolderURL={selectedFolderURL} - /> - ))(currentItem.blocks ? Write : Read)} </> ); } diff --git a/src/read-write.js b/src/read-write.js index eff3c8c..b3a7f96 100644 --- a/src/read-write.js +++ b/src/read-write.js @@ -67,9 +67,10 @@ export function getTitleFromBlocks(blocks) { for (const block of blocks) { const html = getBlockContent(block); - const textContent = sanitizeFileName( - html.replace(/<[^>]+>/g, '').trim() - ).slice(0, 50); + const textContent = html + .replace(/<[^>]+>/g, '') + .trim() + .slice(0, 50); if (textContent) { return textContent; } @@ -84,7 +85,7 @@ function useUpdateFile({ selectedFolderURL, item, setItem }) { } const base = path.split('/').slice(0, -1).join('/'); - const title = getTitleFromBlocks(note); + const title = sanitizeFileName(getTitleFromBlocks(note)); let newPath; if (title) { newPath = base ? base + '/' + title + '.html' : title + '.html'; diff --git a/src/sidebar.jsx b/src/sidebar.jsx new file mode 100644 index 0000000..733b164 --- /dev/null +++ b/src/sidebar.jsx @@ -0,0 +1,57 @@ +import { DataViews } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; +import React, { useState } from 'react'; +import { getTitleFromBlocks } from './read-write'; + +function Title({ item: { path, blocks } }) { + if (blocks) { + return getTitleFromBlocks(blocks) || <em>{__('Untitled')}</em>; + } + + const title = path?.replace(/(?:\.?[0-9]+)?\.html$/, ''); + return title ? decodeURIComponent(title) : <em>{__('Untitled')}</em>; +} + +export default function SiderBar({ items, setItem, currentId, setCurrentId }) { + const [view, setView] = useState({ + type: 'list', + search: '', + filters: [], + page: 1, + perPage: 5, + sort: { + field: 'path', + direction: 'desc', + }, + hiddenFields: [], + layout: {}, + }); + + return ( + <DataViews + data={items} + view={view} + fields={[ + { + id: 'path', + // To do: remove hidden text from rows. + header: ' ', + enableHiding: false, + render({ item }) { + return <Title item={item} />; + }, + }, + ]} + onChangeView={setView} + paginationInfo={{ + totalItems: items.length, + totalPages: 1, + }} + onSelectionChange={([item]) => { + setCurrentId(item.id); + setItem(currentId, { blocks: null }); + }} + supportedLayouts={['list']} + /> + ); +} diff --git a/src/style.css b/src/style.css index 32c4691..db84eee 100644 --- a/src/style.css +++ b/src/style.css @@ -28,11 +28,73 @@ body, font-size: 1em; } +#sidebar { + position: absolute; + top: env(safe-area-inset-top); + width: 300px; + bottom: 0; + overflow-y: auto; + border-right: 1px solid #e0e0e0; + word-break: break-all; +} + +.dataviews-filters__view-actions { + padding: 7px; + border-bottom: 1px solid #e0e0e0; + position: sticky; + top: 0; +} + +.dataviews-view-list__item-wrapper { + display: block !important; +} + +.dataviews-view-list__item[aria-pressed="true"] { + background-color: #3858e9; + color: white; + padding: 7px; +} + +.dataviews-view-list__item { + padding: 7px; + border-radius: 2px +} + +.dataviews-view-list__item > div { + display: block !important; +} + +ul.dataviews-view-list { + padding: 7px; + margin: 0; + list-style: none; +} + +ul.dataviews-view-list li { + padding: 0; + margin: 0; + list-style: none; +} + +.dataviews-view-table { + padding: 7px +} + +.dataviews-view-list__field { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +#content { + height: 100%; +} + #select { position: absolute; top: 0; width: 100%; - height: calc( env(safe-area-inset-top) + 46px ); + height: calc( env(safe-area-inset-top) + 47px ); display: flex; justify-content: start; align-items: end; @@ -66,6 +128,7 @@ iframe[name="editor-canvas"] { } #select, +.dataviews-filters__view-actions, /* .is-unstyled ? */ .components-dropdown__content div.components-popover__content { backdrop-filter: blur(5px); @@ -79,7 +142,6 @@ iframe[name="editor-canvas"] { } #select .blocknotes-select button { - border-right: 1px solid #e0e0e0; padding-right: 1em; } diff --git a/tests/mock.spec.js b/tests/mock.spec.js index 94283b9..642ad95 100644 --- a/tests/mock.spec.js +++ b/tests/mock.spec.js @@ -68,13 +68,11 @@ test.describe('Blocknotes', () => { await expect(emptyBlock).toBeFocused(); - const notesButton = page.getByRole('button', { name: 'Notes' }); - - await notesButton.click(); + await page.getByRole('button', { name: 'Notes' }).click(); - await expect( - page.getByRole('menu', { name: 'Notes' }).getByRole('menuitem') - ).toHaveText(['New Note', 'Untitled', 'Pick Folder', 'Forget Folder']); + await expect(page.getByRole('grid').getByRole('row')).toHaveText([ + 'Untitled', + ]); await emptyBlock.click(); @@ -84,11 +82,7 @@ test.describe('Blocknotes', () => { canvas(page).getByRole('document', { name: 'Block: Paragraph' }) ).toBeFocused(); - await notesButton.click(); - - await expect( - page.getByRole('menu', { name: 'Notes' }).getByRole('menuitem') - ).toHaveText(['New Note', 'aa', 'Pick Folder', 'Forget Folder']); + await expect(page.getByRole('row')).toHaveText(['aa']); // Nothing should have been saved yet because saving is debounced. expect(await getPaths(page)).toEqual([]); @@ -125,19 +119,14 @@ test.describe('Blocknotes', () => { <!-- /wp:paragraph -->`); // Create a second note. - await notesButton.click(); - await page.getByRole('menuitem', { name: 'New Note' }).click(); + await page.getByRole('button', { name: 'New Note' }).click(); await page.keyboard.type('b'); - await notesButton.click(); - - await expect( - page.getByRole('menu', { name: 'Notes' }).getByRole('menuitem') - ).toHaveText(['New Note', 'b', 'aaaa', 'Pick Folder', 'Forget Folder']); + await expect(page.getByRole('row')).toHaveText(['b', 'aaaa']); // Immediately switch back to note A. - await page.getByRole('menuitem', { name: 'aaaa' }).click(); + await page.getByRole('button', { name: 'aaaa' }).click(); await expect( canvas(page).getByRole('document', { name: 'Block: Paragraph' }) @@ -220,15 +209,14 @@ test.describe('Blocknotes', () => { const notesButton = page.getByRole('button', { name: 'Notes' }); - await notesButton.click(); - await page.getByRole('menuitem', { name: 'New Note' }).click(); + await page.getByRole('button', { name: 'New Note' }).click(); await page.keyboard.type('a'); await page.keyboard.press('Enter'); await page.keyboard.type('2'); await notesButton.click(); - await page.getByRole('menuitem', { name: 'a' }).nth(1).click(); + await page.getByRole('row', { name: 'a' }).nth(1).click(); await expect( canvas(page)