diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0607ac4..670c451 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.231.3/containers/javascript-node { - "name": "Node.js", + "name": "Schmackhaft", "build": { "dockerfile": "Dockerfile", // Update 'VARIANT' to pick a Node version: 16, 14, 12. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1645c0c..10cd1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.2.0 + +- Auto-Focus the "quicksearch" field +- Provide keyboard shortcuts to navigate links when using the quicksearch. + # 1.1.0 - Fix scrolling issue in the settings & help page diff --git a/Makefile b/Makefile index 4c8667b..85abbf6 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,17 @@ mozilla: pages bundled_docs chrome: pages bundled_docs mkdir -p unpackaged/chrome/src/core cp -r build/pages/* unpackaged/chrome/ - inkscape -w 48 -h 48 public/assets/icon.svg \ + inkscape -w 48 -h 48 assets/icon.svg \ -o unpackaged/chrome/assets/icon48.png || \ - inkscape -w 48 -h 48 public/assets/icon.svg \ + inkscape -w 48 -h 48 assets/icon.svg \ -e unpackaged/chrome/assets/icon48.png - inkscape -w 96 -h 96 public/assets/icon.svg \ + inkscape -w 96 -h 96 assets/icon.svg \ -o unpackaged/chrome/assets/icon96.png || \ - inkscape -w 96 -h 96 public/assets/icon.svg \ + inkscape -w 96 -h 96 assets/icon.svg \ -e unpackaged/chrome/assets/icon96.png - inkscape -w 128 -h 128 public/assets/icon.svg \ + inkscape -w 128 -h 128 assets/icon.svg \ -o unpackaged/chrome/assets/icon128.png || \ - inkscape -w 128 -h 128 public/assets/icon.svg \ + inkscape -w 128 -h 128 assets/icon.svg \ -e unpackaged/chrome/assets/icon128.png sed -e 's/__version__/$(CURRENT_VERSION)/' \ manifest-chrome.json > unpackaged/chrome/manifest.json diff --git a/TODO b/TODO index ff8d1d7..60e0b9f 100644 --- a/TODO +++ b/TODO @@ -3,5 +3,5 @@ - Add default tags per source - Replace arrays with sets where possible - Add a name to each source -- Add default tags to each source - Deduplicate bookmarks (if more than one source has the same bookmark, it is added with duplicates) +- Is "postcss" still needed? diff --git a/public/assets/icon.png b/assets/icon.png similarity index 100% rename from public/assets/icon.png rename to assets/icon.png diff --git a/public/assets/icon.svg b/assets/icon.svg similarity index 100% rename from public/assets/icon.svg rename to assets/icon.svg diff --git a/demo/demo.ts b/demo/demo.ts index ff4c7fb..3a0a4a4 100644 --- a/demo/demo.ts +++ b/demo/demo.ts @@ -1,113 +1,164 @@ -import "../src/components/views/sh-settings"; -import "../src/components/components/layout-vsplit"; +import "../src/views/sh-settings"; +import "../src/components/layout-vsplit"; import { BookmarkSource } from "../src/types"; import { FakeBrowser } from "./fake-browser"; +import { Schmackhaft } from "../src/app-schmackhaft"; import { Settings } from "../src/model/settings"; -import { SettingsBridge } from "../src/core/settings"; +import { Settings as SettingsElement } from "../src/views/sh-settings"; -let settingsElementV1 = document.getElementById("SettingsV1") as SettingsBridge; -let settingsElementV2 = document.getElementById("SettingsV2") as SettingsBridge; -settingsElementV2.settings = JSON.stringify({ - remoteUrls: [ - "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", - "https://demo-2.json", - ], - enableBrowserBookmarks: true, - version: 2, -}); -settingsElementV2.addEventListener("change", (event) => { - console.log(JSON.parse(event.detail["settings"])); -}); +/** + * Initialise the HTML elements which are used to play with the component + * settings + */ +function initSettingsUI() { + let settingsElementV1 = document.getElementById( + "SettingsV1" + ) as SettingsElement; + let settingsElementV2 = document.getElementById( + "SettingsV2" + ) as SettingsElement; + settingsElementV2.settings = JSON.stringify({ + remoteUrls: [ + "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", + "https://demo-2.json", + ], + enableBrowserBookmarks: true, + version: 2, + }); + settingsElementV2.addEventListener("change", (event) => { + let evt = event as CustomEvent; + console.log(JSON.parse(evt.detail["settings"])); + }); -settingsElementV1.settings = JSON.stringify({ - remoteUrl: - "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", - version: 1, -}); + settingsElementV1.settings = JSON.stringify({ + remoteUrl: + "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", + version: 1, + }); +} -let bookmarksElement = document.getElementById("schmackhaft"); +/** + * Initialise a core "schmackhaft custom-element" + */ +function initSchmackhaftUI() { + let bookmarksElement = document.getElementById("schmackhaft") as Schmackhaft; -// We can't use the default settings bridge here, because this only works in a -// browser-extension execution context. -let settings = new Settings( - [ - { - type: BookmarkSource.HTTP, - settings: { - url: "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", + // We can't use the default settings bridge here, because this only works in a + // browser-extension execution context. + let settings = new Settings( + [ + { + type: BookmarkSource.HTTP, + settings: { + url: "https://raw.githubusercontent.com/exhuma/dotfiles/master/bookmarks.json", + }, }, - }, - { - type: BookmarkSource.HTTP, - settings: { - url: "https://raw.githubusercontent.com/exhuma/schmackhaft/e6439061eedd24c50e00e8b2374ec50d376bc6e5/docs/examples/external-file.json", + { + type: BookmarkSource.HTTP, + settings: { + url: "https://raw.githubusercontent.com/exhuma/schmackhaft/e6439061eedd24c50e00e8b2374ec50d376bc6e5/docs/examples/external-file.json", + }, }, - }, - { - type: BookmarkSource.BROWSER, - settings: {}, - }, - { - type: BookmarkSource.EXTENSION_STORAGE, - settings: {}, - }, - ], - 3 -); -bookmarksElement.settings = settings.toJson(); -bookmarksElement?.addEventListener("settingsChanged", (event) => { - console.log("Settings Changed to:"); - console.log(JSON.parse(event.detail["settings"])); -}); -bookmarksElement.injections = { getBrowser: async () => new FakeBrowser() }; + { + type: BookmarkSource.BROWSER, + settings: {}, + }, + { + type: BookmarkSource.EXTENSION_STORAGE, + settings: {}, + }, + ], + 3 + ); + bookmarksElement.settings = settings.toJson(); + bookmarksElement?.addEventListener("settingsChanged", (event) => { + let evt = event as CustomEvent; + console.log("Settings Changed to:"); + console.log(JSON.parse(evt.detail["settings"])); + }); + bookmarksElement.injections = { getBrowser: async () => new FakeBrowser() }; +} /** - * Ensure only the div related to the clicked link is visible + * Toggle the visibility of a single element with the "togglable" class. * - * @param evt A click-event from the browser + * All "togglable" elements will be hidden *except* the one with the given + * SGML-ID + * + * @param id The ID of the element which should become/remain visible. */ -function toggleDiv(evt) { - let enabledName = evt.target.dataset["div"]; +function toggleDiv(id: string): void { document.querySelectorAll(".toggleable").forEach((element) => { - let currentName = element.id; - let displayValue = enabledName === currentName ? "block" : "none"; - element.style.display = displayValue; + let elmt = element as HTMLElement; + let currentName = elmt.id; + let displayValue = id === currentName ? "block" : "none"; + elmt.style.display = displayValue; }); } -document.querySelectorAll(".clickable").forEach((element) => { - element.addEventListener("click", toggleDiv); -}); +/** + * Delegate click events to the visibility "toggler". The clicked element *must* + * have the attribute "data-div" with the SGML-ID as value of the element that + * should be displayed. + * + * @param evt A click-event from the browser + */ +function onTabClicked(evt: Event) { + let enabledName = evt.target.dataset["div"]; + toggleDiv(enabledName); +} /** * Update bookmarks from an external JSON file * * @param url The URL from which to fetch the JSON */ -async function reloadJson(url) { +async function reloadJson(url: string) { if (url === undefined || url.trim() === "") { return; } - let response = await fetch(url); - if (!response.ok) { - console.error(`Unable to fetch ${url} (${response.statusText})`); - return; - } - let text = await response.text(); - let bookmarksElement = document.getElementById("schmackhaft"); - bookmarksElement.links = text; + let bookmarksElement = document.getElementById("schmackhaft") as Schmackhaft; + let settings = new Settings([ + { + type: BookmarkSource.HTTP, + settings: { + url: url, + }, + }, + ]); + bookmarksElement.settings = settings.toJson(); } -document.getElementById("ReloadJsonButton").addEventListener("click", () => { +/** + * Initialise the elements in the demo-page toolbar + */ +function initToolbar() { + document.getElementById("ReloadJsonButton").addEventListener("click", () => { + let txtJsonFile = document.getElementById( + "ExternalJsonFile" + ) as HTMLInputElement; + let url = txtJsonFile.value; + reloadJson(url); + }); + let txtJsonFile = document.getElementById("ExternalJsonFile"); - let url = txtJsonFile.value; - reloadJson(url); -}); + txtJsonFile.addEventListener("change", async (evt) => { + let target = evt.target as HTMLInputElement; + let url = target.value; + reloadJson(url); + }); -let txtJsonFile = document.getElementById("ExternalJsonFile"); -txtJsonFile.addEventListener("change", async (evt) => { - let url = evt.target.value; - reloadJson(url); -}); + document.querySelectorAll(".clickable").forEach((element) => { + element.addEventListener("click", onTabClicked); + }); +} -document.querySelector(".toggleable").style.display = "block"; +/** + * Initialise all UI elements for the demo page + */ +export function initUI() { + initSettingsUI(); + initSchmackhaftUI(); + initToolbar(); + toggleDiv("Bookmarks"); +} diff --git a/demo/fake-browser.ts b/demo/fake-browser.ts index b09bba3..c3c5406 100644 --- a/demo/fake-browser.ts +++ b/demo/fake-browser.ts @@ -30,7 +30,16 @@ class FakeStorage { } } +class FakeTabs { + create(createProperties: { url: string }) { + console.info(`Would open a new tab to the URL ${createProperties.url}`); + } +} + export class FakeBrowser { + tabs: FakeTabs = new FakeTabs(); + runtime: any; + get bookmarks() { return new BookmarkTree(); } diff --git a/demo/index.html b/demo/index.html index 8aff1ee..498fc56 100644 --- a/demo/index.html +++ b/demo/index.html @@ -5,8 +5,7 @@ Development Page - - + Settings V2 + diff --git a/manifest-chrome.json b/manifest-chrome.json index 8deceaf..37762a0 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -18,5 +18,5 @@ "show_matches": ["http://*/*", "https://*/*"], "default_popup": "/src/views/action_button/index.html" }, - "permissions": ["activeTab", "storage", "bookmarks"] + "permissions": ["tabs", "activeTab", "storage", "bookmarks"] } diff --git a/package.json b/package.json index 8109e75..1048c47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schmackhaft", - "version": "1.1.0", + "version": "1.2.0", "description": "A bookmark manager based on del.icio.us", "author": "Michel Albert ", "license": "MIT", diff --git a/src/components/app-schmackhaft.ts b/src/app-schmackhaft.ts similarity index 75% rename from src/components/app-schmackhaft.ts rename to src/app-schmackhaft.ts index 2e632e0..9f99ba9 100644 --- a/src/components/app-schmackhaft.ts +++ b/src/app-schmackhaft.ts @@ -1,4 +1,5 @@ import "./components/layout-vsplit"; +import "./components/sh-bookmarklist"; import "./components/sh-link"; import "./components/sh-linklist"; import "./components/sh-taglist"; @@ -6,29 +7,21 @@ import "./components/sh-toolbar"; import "./views/sh-settings"; import "@material/mwc-button"; import "material-icon-component/md-icon.js"; -import { - Bookmark, - PageName, - TBookmarkSource, - TBrowserFactory, - TagStateTransition, -} from "../types"; +import { Bookmark, PageName, TBookmarkSource, TBrowserFactory } from "./types"; import { LitElement, css, html } from "lit"; import { Ref, createRef, ref } from "lit/directives/ref.js"; import { customElement, property, state } from "lit/decorators.js"; // @ts-ignore import { parse, setOptions } from "marked"; // @ts-ignore -import Help from "../help/help.md?raw"; -import { Link } from "../model/link"; -import { LinkList } from "./components/sh-linklist"; -import { Links } from "./core/links"; -import { Settings } from "../model/settings"; -import { TagList } from "./components/sh-taglist"; +import Help from "./help/help.md?raw"; +import { Link } from "./model/link"; +import { Links } from "./model/link-collection"; +import { Settings } from "./model/settings"; import { ToolbarAction } from "./components/sh-toolbar"; -import { createStorage } from "../core/storage/factory"; +import { createStorage } from "./core/storage/factory"; // @ts-ignore -import helpStyles from "./help.css"; +import helpStyles from "./help/help.css"; import hljs from "highlight.js"; // @ts-ignore import hlstyle from "highlight.js/styles/monokai.css"; @@ -68,8 +61,6 @@ export class Schmackhaft extends LitElement { tagsRef: Ref = createRef(); searchTextRef: Ref = createRef(); - linkListRef: Ref = createRef(); - tagListRef: Ref = createRef(); getBrowser: TBrowserFactory = async () => null; private _links: Links = new Links(); @@ -141,23 +132,6 @@ export class Schmackhaft extends LitElement { this.requestUpdate(); } - onChipClicked(evt: { - detail: { direction: TagStateTransition; name: string }; - }) { - switch (evt.detail.direction) { - case TagStateTransition.ADVANCE: - default: - this._links.advanceState(evt.detail.name); - break; - case TagStateTransition.REVERSE: - this._links.reverseState(evt.detail.name); - break; - } - this.requestUpdate(); - this.linkListRef.value?.requestUpdate(); - this.tagListRef.value?.requestUpdate(); - } - onRefreshClicked() { this._fetchBookmarks(); } @@ -183,6 +157,12 @@ export class Schmackhaft extends LitElement { ); } + async _onLinkActivated(evt: { detail: { link: Link } }) { + let browser = await this.getBrowser(); + browser?.tabs.create({ url: evt.detail.link.href }); + window.close(); + } + _renderBookmarks() { if (this._links.isEmpty) { return html` No links found. @@ -191,26 +171,10 @@ export class Schmackhaft extends LitElement { open the settings and add one or more sources.`; } - return html` - - - - - `; + return html``; } _renderSettings() { @@ -253,15 +217,6 @@ export class Schmackhaft extends LitElement { `; } - _onSearchChanged(evt: { detail: { searchText: string } }) { - // TODO: lit does not detect any changes deep inside the "this._links" - // object and we need to manually trigger the update. This is error-prone - // and should be improved. - this._links.search(evt.detail.searchText); - this.linkListRef.value?.requestUpdate(); - this.tagListRef.value?.requestUpdate(); - } - _onToolbarButtonClick(evt: { detail: { name: ToolbarAction } }) { switch (evt.detail.name) { case ToolbarAction.BOOKMARKS: @@ -288,7 +243,6 @@ export class Schmackhaft extends LitElement { ?busy=${this._busy} toast=${this._toast} @buttonClicked=${this._onToolbarButtonClick} - @searchTextChange=${this._onSearchChanged} > ${this._renderMainContent()} ${this.errors.map((error) => this._renderError(error))} diff --git a/src/components/components/sh-linklist.ts b/src/components/components/sh-linklist.ts deleted file mode 100644 index 16826fb..0000000 --- a/src/components/components/sh-linklist.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { LitElement, css, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { Link } from "../../model/link"; -import { Links } from "../core/links"; - -@customElement("sh-linklist") -export class LinkList extends LitElement { - static styles = css``; - - @property({ type: Object }) - links = new Links(); - - @property({ type: Boolean }) - renderSearchedTags: boolean = true; - - @property({ type: Boolean }) - dense: boolean = false; - - @property() - favIconTemplate = ""; - - _renderTag(tagName: string, component: any) { - return html`${tagName}`; - } - - _renderLink(link: Link) { - let tagsWithStates = link.tags.map((tagName) => { - return [tagName, this.links.getState(tagName)]; - }); - return html` - - `; - } - - onChipClicked(evt: { detail: any }) { - this.dispatchEvent(new CustomEvent("chipClicked", { detail: evt.detail })); - } - - override render() { - if (this.renderSearchedTags) { - return html` -

${this.links.searchedTags.map(this._renderTag, this)}

- ${this.links.filtered.map(this._renderLink, this)} - `; - } - return html` ${this.links.filtered.map(this._renderLink, this)} `; - } -} diff --git a/src/components/data.ts b/src/components/data.ts index 6ffc9b8..993c9ac 100644 --- a/src/components/data.ts +++ b/src/components/data.ts @@ -1,5 +1,5 @@ import { Link } from "../model/link"; -import { Links } from "./core/links"; +import { Links } from "./model/link-collection"; export const demoLinks = new Links([ new Link( diff --git a/src/components/components/layout-vsplit.ts b/src/components/layout-vsplit.ts similarity index 100% rename from src/components/components/layout-vsplit.ts rename to src/components/layout-vsplit.ts diff --git a/src/components/sh-bookmarklist.ts b/src/components/sh-bookmarklist.ts new file mode 100644 index 0000000..baf53e9 --- /dev/null +++ b/src/components/sh-bookmarklist.ts @@ -0,0 +1,119 @@ +import "material-icon-component/md-icon.js"; +import { LitElement, PropertyValueMap, css, html } from "lit"; +import { Ref, createRef, ref } from "lit/directives/ref.js"; +import { customElement, property } from "lit/decorators.js"; +import { LinkList } from "./sh-linklist"; +import { Links } from "../model/link-collection"; +import { TagList } from "./sh-taglist"; +import { TagStateTransition } from "../types"; +// @ts-ignore +import tailwind from "../tailwind.css"; + +@customElement("sh-bookmarklist") +export class Bookmarks extends LitElement { + static styles = [ + // @ts-ignore + css([tailwind]), + css` + :host { + /* TODO: "display: contents" has accessibility issues. See + * https://developer.mozilla.org/en-US/docs/Web/CSS/display-box for more + * information + */ + display: contents; + height: 100px; + border: 2px dashed blue; + } + `, + ]; + + @property({ type: Object }) + links: Links = new Links(); + + @property({ attribute: "fav-icon-template" }) + favIconTemplate = ""; + + tagListRef: Ref = createRef(); + linkListRef: Ref = createRef(); + quickSearchRef: Ref = createRef(); + + _onKeyUp(evt: { target: { value: string }; key: string }) { + switch (evt.key) { + case "Enter": + let link = this.linkListRef?.value?.focussedLink; + if (link !== undefined && link !== null) { + this.dispatchEvent( + new CustomEvent("linkActivated", { detail: { link } }) + ); + } + break; + case "ArrowDown": + this.linkListRef.value?.focusNextLink(); + break; + case "ArrowUp": + this.linkListRef.value?.focusPreviousLink(); + break; + default: + // TODO: lit does not detect any changes deep inside the "this.links" + // object and we need to manually trigger the update. This is error-prone + // and should be improved. + this.links.search(evt.target.value); + this.linkListRef.value?.requestUpdate(); + this.tagListRef.value?.requestUpdate(); + this.linkListRef.value?.focusLink(0); + } + } + + _onChipClicked(evt: { + detail: { direction: TagStateTransition; name: string }; + }) { + switch (evt.detail.direction) { + case TagStateTransition.ADVANCE: + default: + this.links.advanceState(evt.detail.name); + break; + case TagStateTransition.REVERSE: + this.links.reverseState(evt.detail.name); + break; + } + this.requestUpdate(); + this.linkListRef.value?.requestUpdate(); + this.tagListRef.value?.requestUpdate(); + } + + protected firstUpdated( + _changedProperties: PropertyValueMap | Map + ): void { + this.quickSearchRef.value?.focus(); + } + + override render() { + return html` + + + + + + `; + } +} diff --git a/src/components/tailwind.css b/src/components/sh-chip.css similarity index 69% rename from src/components/tailwind.css rename to src/components/sh-chip.css index ab03153..6aacc72 100644 --- a/src/components/tailwind.css +++ b/src/components/sh-chip.css @@ -1,6 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +:host { + @apply inline-block mb-1; +} .chip.included { @apply bg-lime-300 dark:bg-lime-700; diff --git a/src/components/components/sh-chip.ts b/src/components/sh-chip.ts similarity index 63% rename from src/components/components/sh-chip.ts rename to src/components/sh-chip.ts index 9c3d6f2..017ffa3 100644 --- a/src/components/components/sh-chip.ts +++ b/src/components/sh-chip.ts @@ -1,7 +1,9 @@ import "material-icon-component/md-icon.js"; import { LitElement, css, html } from "lit"; -import { TagState, TagStateTransition } from "../../types"; +import { TagState, TagStateTransition } from "../types"; import { customElement, property } from "lit/decorators.js"; +// @ts-ignore +import chipStyles from "./sh-chip.css"; import { classMap } from "lit/directives/class-map.js"; // @ts-ignore import tailwind from "../tailwind.css"; @@ -11,11 +13,16 @@ export class Chip extends LitElement { static styles = [ // @ts-ignore css([tailwind]), + // @ts-ignore + css([chipStyles]), ]; @property() name = ""; + @property() + count = 0; + @property({ type: Boolean }) dense: boolean = false; @@ -44,6 +51,8 @@ export class Chip extends LitElement { neutral: this.state === TagState.NEUTRAL, included: this.state === TagState.INCLUDED, excluded: this.state === TagState.EXCLUDED, + ["rounded-l"]: this.state === TagState.NEUTRAL, + ["rounded"]: this.state !== TagState.NEUTRAL, }; let label; let actionText; @@ -62,17 +71,30 @@ export class Chip extends LitElement { actionText = "include only links with this tag"; break; } + let countBox = html``; + if (this.state === TagState.NEUTRAL) { + countBox = html` +
+ ${this.count} +
+ `; + } return html` -
-
${label}
-
${this.name}
+
+
+
${label}
+
${this.name}
+
+ ${countBox}
`; } diff --git a/src/components/components/sh-http-settings.ts b/src/components/sh-http-settings.ts similarity index 100% rename from src/components/components/sh-http-settings.ts rename to src/components/sh-http-settings.ts diff --git a/src/components/components/sh-link.css b/src/components/sh-link.css similarity index 100% rename from src/components/components/sh-link.css rename to src/components/sh-link.css diff --git a/src/components/components/sh-link.ts b/src/components/sh-link.ts similarity index 97% rename from src/components/components/sh-link.ts rename to src/components/sh-link.ts index d92f5a9..2503d14 100644 --- a/src/components/components/sh-link.ts +++ b/src/components/sh-link.ts @@ -1,7 +1,7 @@ import "./sh-chip"; import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { TagState } from "../../types"; +import { TagState } from "../types"; import { classMap } from "lit/directives/class-map.js"; // @ts-ignore import tailwind from "./sh-link.css"; @@ -13,6 +13,9 @@ class Link extends LitElement { // @ts-ignore css([tailwind]), css` + :host { + display: block; + } .screenshot { width: 64px; height: 64px; diff --git a/src/components/sh-linklist.css b/src/components/sh-linklist.css new file mode 100644 index 0000000..df955b7 --- /dev/null +++ b/src/components/sh-linklist.css @@ -0,0 +1,9 @@ +.selected { + @apply border rounded mr-5; + @apply bg-slate-200 border-slate-300; + @apply dark:bg-slate-700 dark:border-slate-600; +} + +.non-selected { + @apply border rounded border-transparent; +} diff --git a/src/components/sh-linklist.ts b/src/components/sh-linklist.ts new file mode 100644 index 0000000..74e4d6e --- /dev/null +++ b/src/components/sh-linklist.ts @@ -0,0 +1,111 @@ +import { LitElement, css, html } from "lit"; +import { Ref, createRef, ref } from "lit/directives/ref.js"; +import { customElement, property } from "lit/decorators.js"; +import { Link } from "../model/link"; +import { Links } from "../model/link-collection"; +import { classMap } from "lit/directives/class-map.js"; +// @ts-ignore +import linkListStyles from "./sh-linklist.css"; +// @ts-ignore +import tailwind from "../tailwind.css"; + +@customElement("sh-linklist") +export class LinkList extends LitElement { + static styles = [ + // @ts-ignore + css([tailwind]), + // @ts-ignore + css([linkListStyles]), + ]; + + @property({ type: Object }) + links = new Links(); + + @property({ type: Boolean }) + renderSearchedTags: boolean = true; + + @property({ type: Boolean }) + dense: boolean = false; + + @property() + favIconTemplate = ""; + + containerRef: Ref = createRef(); + + focussedLinkIndex = 0; + + focusLink(index: number) { + this.focussedLinkIndex = index; + if (this.focussedLinkIndex < 0) { + this.focussedLinkIndex = 0; + } else if (this.focussedLinkIndex > this.links.filtered.length - 1) { + this.focussedLinkIndex = this.links.filtered.length - 1; + } + let selectedElement = + this.containerRef?.value?.getElementsByClassName("selected"); + if (selectedElement && selectedElement[0]) { + selectedElement[0].scrollIntoView({ block: "center" }); + } + this.requestUpdate(); + } + + focusPreviousLink() { + this.focusLink(this.focussedLinkIndex - 1); + } + + focusNextLink() { + this.focusLink(this.focussedLinkIndex + 1); + } + + get focussedLink(): Link | null { + if (this.links.isEmpty) { + return null; + } + return this.links.filtered[this.focussedLinkIndex]; + } + + _renderTag(tagName: string, component: any) { + return html`${tagName}`; + } + + _renderLink(link: Link, idx: number) { + let tagsWithStates = link.tags.map((tagName) => { + return [tagName, this.links.getState(tagName)]; + }); + let dynamicClasses = { + selected: idx === this.focussedLinkIndex, + ["non-selected"]: idx !== this.focussedLinkIndex, + }; + return html` + + `; + } + + onChipClicked(evt: { detail: any }) { + this.dispatchEvent(new CustomEvent("chipClicked", { detail: evt.detail })); + } + + override render() { + if (this.renderSearchedTags) { + return html` +

${this.links.searchedTags.map(this._renderTag, this)}

+ ${this.links.filtered.map(this._renderLink, this)} + `; + } + return html`
+ ${this.links.filtered.map(this._renderLink, this)} +
`; + } +} diff --git a/src/components/components/sh-taglist.ts b/src/components/sh-taglist.ts similarity index 72% rename from src/components/components/sh-taglist.ts rename to src/components/sh-taglist.ts index 7aee777..393d7f4 100644 --- a/src/components/components/sh-taglist.ts +++ b/src/components/sh-taglist.ts @@ -1,7 +1,7 @@ import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { Counter } from "../core/counter"; -import { Links } from "../core/links"; +import { Links } from "../model/link-collection"; import { classMap } from "lit/directives/class-map.js"; // @ts-ignore import tailwind from "../tailwind.css"; @@ -26,25 +26,14 @@ export class TagList extends LitElement { return null; } let state = this.links.getState(tag[0]); - let dynamicClasses = { dense: this.dense }; return html` -
-
- ${tag[1]} -
-
+ `; } diff --git a/src/components/components/sh-toolbar.css b/src/components/sh-toolbar.css similarity index 100% rename from src/components/components/sh-toolbar.css rename to src/components/sh-toolbar.css diff --git a/src/components/components/sh-toolbar.ts b/src/components/sh-toolbar.ts similarity index 88% rename from src/components/components/sh-toolbar.ts rename to src/components/sh-toolbar.ts index 54ca7bc..3a39ecb 100644 --- a/src/components/components/sh-toolbar.ts +++ b/src/components/sh-toolbar.ts @@ -79,16 +79,6 @@ export class Toolbar extends LitElement { ); } - _onSearchTextEdited(evt: { target: { value: string } }) { - this.dispatchEvent( - new CustomEvent("searchTextChange", { - detail: { - searchText: evt.target.value, - }, - }) - ); - } - get spinnerClasses() { return { ["animate-reverse-spin"]: this.busy, @@ -153,12 +143,6 @@ export class Toolbar extends LitElement {
- ${_toastElement} `; diff --git a/src/components/core/counter.ts b/src/core/counter.ts similarity index 100% rename from src/components/core/counter.ts rename to src/core/counter.ts diff --git a/src/components/help.css b/src/help/help.css similarity index 100% rename from src/components/help.css rename to src/help/help.css diff --git a/src/help/help.md b/src/help/help.md index 46e56af..7733fa6 100644 --- a/src/help/help.md +++ b/src/help/help.md @@ -57,6 +57,12 @@ To quickly "include" a tag, simply click on it if it is neutral. To quickly "exclude" a tag, simply right-click on it if it is neutral. +### Keyboard Navigation + +While the quicksearch field is in focus, the up-/down-arrow keys can be used to +focus a specific link. Pressing "Enter" on the keyboard will then open the +selected link. + ## Toolbar There is a toolbar above the tag- and bookmark-list providing additional "management" options. It allows you to: diff --git a/src/components/core/links.ts b/src/model/link-collection.ts similarity index 97% rename from src/components/core/links.ts rename to src/model/link-collection.ts index 1beddf2..cba3a82 100644 --- a/src/components/core/links.ts +++ b/src/model/link-collection.ts @@ -3,9 +3,9 @@ * provides implementations for searching links on various conditions. */ -import { Bookmark, TagState } from "../../types"; -import { Link } from "../../model/link"; -import { intersection } from "../../collections"; +import { Bookmark, TagState } from "../types"; +import { Link } from "../model/link"; +import { intersection } from "../collections"; /** * A colelction of links. @@ -26,7 +26,7 @@ export class Links { } /** - * Return whether this collection is empty or not + * @returns whether this collection is empty or not */ get isEmpty() { return this.links.length === 0; diff --git a/src/tailwind.css b/src/tailwind.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/views/action_button/actionbutton.ts b/src/views/action_button/actionbutton.ts index e6f04df..295ec6d 100644 --- a/src/views/action_button/actionbutton.ts +++ b/src/views/action_button/actionbutton.ts @@ -2,7 +2,7 @@ * This file contains supporting JS code for the browser-extension action button */ import { Browser } from "../../types"; -import { Schmackhaft } from "../../components/app-schmackhaft"; +import { Schmackhaft } from "../../app-schmackhaft"; import { SettingsBridge } from "../../core/settings"; /** diff --git a/src/views/action_button/index.html b/src/views/action_button/index.html index 4013f52..734665d 100644 --- a/src/views/action_button/index.html +++ b/src/views/action_button/index.html @@ -15,7 +15,7 @@ rel="stylesheet" /> - +