From ac2e367474ef96f2aeb7ff6089307fe89afc195d Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Fri, 7 Feb 2025 13:49:32 +0100 Subject: [PATCH] Add keyboard navigation to books pages, fiduswriter/fiduwriter#1279 --- .github/workflows/main.yml | 2 - .../book/static/js/modules/books/index.js | 105 +++++++++++++++++- .../book/static/js/modules/books/menu.js | 3 + .../book/static/js/plugins/menu/books.js | 3 +- pyproject.toml | 2 +- 5 files changed, 105 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ca3d25..ff3c098 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,8 +59,6 @@ jobs: pip install packaging pip install webdriver-manager pip install selenium - pip install flake8 - pip install black coverage run $(which fiduswriter) setup --no-static - name: Run tests uses: nick-invision/retry@v3 diff --git a/fiduswriter/book/static/js/modules/books/index.js b/fiduswriter/book/static/js/modules/books/index.js index 9a56d8f..fdb53a6 100644 --- a/fiduswriter/book/static/js/modules/books/index.js +++ b/fiduswriter/book/static/js/modules/books/index.js @@ -1,5 +1,6 @@ import deepEqual from "fast-deep-equal" import {DataTable} from "simple-datatables" +import {keyName} from "w3c-keyname" import * as plugins from "../../plugins/books_overview" import { @@ -172,7 +173,7 @@ export class BookOverview { fileList.unshift([ "-1", "top", - "", + false, ` @@ -218,14 +219,79 @@ export class BookOverview { hidden: true }, { - select: [2, 7, 8], + select: 2, + sortable: false, + type: "boolean" + }, + { + select: [7, 8], sortable: false }, { select: [this.lastSort.column], sort: this.lastSort.dir } - ] + ], + rowNavigation: true, + rowSelectionKeys: ["Enter", "Delete", " "], + tabIndex: 1, + rowRender: (row, tr, _index) => { + if (row.cells[1].data === "folder") { + tr.childNodes[0].childNodes = [] + return + } + const id = row.cells[0].data + const inputNode = { + nodeName: "input", + attributes: { + type: "checkbox", + class: "entry-select fw-check", + "data-id": id, + id: `book-${id}` + } + } + if (row.cells[2].data) { + inputNode.attributes.checked = true + } + tr.childNodes[0].childNodes = [ + inputNode, + { + nodeName: "label", + attributes: { + for: `book-${id}` + } + } + ] + } + }) + + this.table.on("datatable.selectrow", (rowIndex, event, focused) => { + event.preventDefault() + if (event.type === "keydown") { + const key = keyName(event) + if (key === "Enter") { + const link = this.table.dom.querySelector( + `tr[data-index="${rowIndex}"] a.fw-data-table-title` + ) + if (link) { + link.click() + } + } else if (key === " ") { + const cell = this.table.data.data[rowIndex].cells[2] + cell.data = !cell.data + cell.text = String(cell.data) + this.table.update() + } else if (key === "Delete") { + const cell = this.table.data.data[rowIndex].cells[0] + const bookId = cell.data + this.mod.actions.deleteBookDialog([bookId], this.app) + } + } else { + if (!focused) { + this.table.dom.focus() + } + this.table.rows.setCursor(rowIndex) + } }) this.table.on("datatable.sort", (column, dir) => { @@ -276,7 +342,7 @@ export class BookOverview { const row = [ "0", "folder", - "", + false, ` @@ -300,9 +366,9 @@ export class BookOverview { // This is the folder of the file. Return the file. return [ - String(book.id), + book.id, "file", - ``, + false, // checkbox ` @@ -395,6 +461,23 @@ export class BookOverview { this.dom.addEventListener("click", event => { const el = {} switch (true) { + case findTarget( + event, + ".entry-select, .entry-select + label", + el + ): { + const checkbox = el.target + const dataIndex = checkbox + .closest("tr") + .getAttribute("data-index", null) + if (dataIndex) { + const index = Number.parseInt(dataIndex) + const data = this.table.data.data[index] + data.cells[2].data = !checkbox.checked + data.cells[2].text = String(!checkbox.checked) + } + break + } case findTarget(event, ".delete-book", el): { if (this.app.isOffline()) { addAlert( @@ -517,4 +600,14 @@ export class BookOverview { this.dom.querySelectorAll(".entry-select:checked:not(:disabled)") ).map(el => Number.parseInt(el.getAttribute("data-id"))) } + + close() { + if (this.table) { + this.table.destroy() + } + if (this.menu) { + this.menu.destroy() + this.menu = null + } + } } diff --git a/fiduswriter/book/static/js/modules/books/menu.js b/fiduswriter/book/static/js/modules/books/menu.js index 56d8444..828e87b 100644 --- a/fiduswriter/book/static/js/modules/books/menu.js +++ b/fiduswriter/book/static/js/modules/books/menu.js @@ -15,6 +15,7 @@ export const menuModel = () => ({ { type: "text", title: gettext("Create new book"), + keys: "n", action: overview => { overview.getImageDB().then(() => { overview.mod.actions.createBookDialog(0, overview.imageDB) @@ -25,6 +26,7 @@ export const menuModel = () => ({ { type: "text", title: gettext("Create new folder"), + keys: "o", action: overview => { const dialog = new NewFolderDialog(folderName => { overview.path = overview.path + folderName + "/" @@ -39,6 +41,7 @@ export const menuModel = () => ({ type: "search", icon: "search", title: gettext("Search books"), + keys: "s", input: (overview, text) => { if (text.length && !currentlySearching) { overview.initTable(true) diff --git a/fiduswriter/book/static/js/plugins/menu/books.js b/fiduswriter/book/static/js/plugins/menu/books.js index 16a2655..ef4069b 100644 --- a/fiduswriter/book/static/js/plugins/menu/books.js +++ b/fiduswriter/book/static/js/plugins/menu/books.js @@ -11,7 +11,8 @@ export class BookMenuItem { title: gettext("compose books"), url: "/books/", text: gettext("Books"), - order: 5 + order: 5, + keys: "Alt-o" }) } } diff --git a/pyproject.toml b/pyproject.toml index 36de64e..79fd2d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ authors = [ classifiers=[ "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 4.1", + "Framework :: Django :: 5.1", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Affero General Public License v3", "Operating System :: OS Independent",