diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..d7ffab6 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,42 @@ +name: Build and deploy Pages + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: taiki-e/install-action@just + - run: npm ci + - run: just style + - run: just build + - uses: actions/upload-pages-artifact@v3 + if: github.event_name != 'pull_request' + with: + path: dist + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-slim + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 3d72576..f62cc61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ +# System files .DS_Store -.idea \ No newline at end of file + +# Editor settings +.idea/ + +# Installed dependencies +node_modules/ + +# Generated site +dist/ diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..742ed87 --- /dev/null +++ b/app/index.html @@ -0,0 +1,10 @@ + + + + + + + + StartTreeV2 - Home + + diff --git a/app/pages/edit.html b/app/pages/edit.html new file mode 100644 index 0000000..d26f951 --- /dev/null +++ b/app/pages/edit.html @@ -0,0 +1,10 @@ + + + + + + + StartTreeV3 - Edit + + + diff --git a/app/pages/view.html b/app/pages/view.html new file mode 100644 index 0000000..c578345 --- /dev/null +++ b/app/pages/view.html @@ -0,0 +1,10 @@ + + + + + + + + StartTreeV3 + + diff --git a/js/helper/dragOptions.js b/app/src/helper/dragOptions.ts similarity index 55% rename from js/helper/dragOptions.js rename to app/src/helper/dragOptions.ts index ec912ee..677372d 100644 --- a/js/helper/dragOptions.js +++ b/app/src/helper/dragOptions.ts @@ -1,5 +1,8 @@ export default class DragOptions { - constructor({ data, validDropzones }) { + data: string; + validDropzones: string[]; + + constructor({ data, validDropzones }: { data?: string; validDropzones?: string[] }) { this.data = data ?? ""; this.validDropzones = validDropzones ?? []; // classes that this draggable can be dropped on } diff --git a/js/helper/jsurl.js b/app/src/helper/jsurl.ts similarity index 82% rename from js/helper/jsurl.js rename to app/src/helper/jsurl.ts index 37145ed..46957eb 100644 --- a/js/helper/jsurl.js +++ b/app/src/helper/jsurl.ts @@ -23,17 +23,17 @@ * OTHER DEALINGS IN THE SOFTWARE. */ // -let stringify = function stringify(v) { - function encode(s) { +let stringify = function stringify(v: unknown): string | undefined { + function encode(s: string): string { return !/[^\w-.]/.test(s) ? s - : s.replace(/[^\w-.]/g, function (ch) { + : s.replace(/[^\w-.]/g, function (ch: string) { if (ch === "$") return "!"; - ch = ch.charCodeAt(0); + const code = ch.charCodeAt(0); // thanks to Douglas Crockford for the negative slice trick - return ch < 0x100 - ? "*" + ("00" + ch.toString(16)).slice(-2) - : "**" + ("0000" + ch.toString(16)).slice(-4); + return code < 0x100 + ? "*" + ("00" + code.toString(16)).slice(-2) + : "**" + ("0000" + code.toString(16)).slice(-4); }); } @@ -59,8 +59,8 @@ let stringify = function stringify(v) { return "~(" + (tmpAry.join("") || "~") + ")"; } else { for (var key in v) { - if (v.hasOwnProperty(key)) { - var val = stringify(v[key]); + if (Object.prototype.hasOwnProperty.call(v, key)) { + var val = stringify((v as Record)[key]); // skip undefined and functions if (val) { @@ -83,13 +83,13 @@ var reserved = { null: null, }; -let parse = function (s) { - if (!s) return s; - s = s.replace(/%(25)*27/g, "'"); +let parse = function (input: string | null): unknown { + if (!input) return input; + let s = input.replace(/%(25)*27/g, "'"); var i = 0, len = s.length; - function eat(expected) { + function eat(expected: string) { if (s.charAt(i) !== expected) throw new Error( "bad JSURL syntax: expected " + expected + ", got " + (s && s.charAt(i)) @@ -123,7 +123,7 @@ let parse = function (s) { return r + s.substring(beg, i); } - return (function parseOne() { + return (function parseOne(): unknown { var result, ch, beg; eat("~"); switch ((ch = s.charAt(i))) { @@ -138,11 +138,11 @@ let parse = function (s) { } while (s.charAt(i) === "~"); } } else { - result = {}; + result = {} as Record; if (s.charAt(i) !== ")") { do { var key = decode(); - result[key] = parseOne(); + (result as Record)[key] = parseOne(); } while (s.charAt(i) === "~" && ++i); } } @@ -159,7 +159,7 @@ let parse = function (s) { if (/[\d\-]/.test(ch)) { result = parseFloat(sub); } else { - result = reserved[sub]; + result = reserved[sub as keyof typeof reserved]; if (typeof result === "undefined") throw new Error("bad value keyword: " + sub); } diff --git a/js/helper/makeDraggable.js b/app/src/helper/makeDraggable.ts similarity index 70% rename from js/helper/makeDraggable.js rename to app/src/helper/makeDraggable.ts index 1bc5d2e..e3c774a 100644 --- a/js/helper/makeDraggable.js +++ b/app/src/helper/makeDraggable.ts @@ -1,3 +1,5 @@ +import DragOptions from "./dragOptions.js"; +export type DragAction = "dragstart" | "dragend" | "dragover" | "dragenter" | "dragleave" | "drop"; // ====================================================== // // ==================== makeDraggable =================== // // ====================================================== // @@ -5,19 +7,19 @@ // helper method to make an element draggable, configured with DragOptions let lastDropWasValid = false; -export default (element, dragOptions, callback) => { +export default (element: HTMLElement, dragOptions: DragOptions, callback: (type: DragAction, event: DragEvent) => void) => { element.draggable = true; element.classList.add("dropzone"); element.addEventListener("dragstart", (event) => { event.stopPropagation(); - event.dataTransfer.setData("text", dragOptions.data); + event.dataTransfer?.setData("text", dragOptions.data); callback("dragstart", event); }); element.addEventListener("dragend", (event) => { event.preventDefault(); - if (event.dataTransfer.dropEffect != "none" && lastDropWasValid) { + if (event.dataTransfer?.dropEffect != "none" && lastDropWasValid) { callback("dragend", event); event.stopPropagation(); } @@ -45,7 +47,7 @@ export default (element, dragOptions, callback) => { element.addEventListener("drop", (event) => { event.preventDefault(); lastDropWasValid = _isValidDropzone( - JSON.parse(event.dataTransfer.getData("text")) + JSON.parse(event.dataTransfer?.getData("text") ?? "{}") ); if (lastDropWasValid) { event.stopPropagation(); @@ -53,14 +55,14 @@ export default (element, dragOptions, callback) => { } }); - const _isValidDropzone = (dropZone) => { + const _isValidDropzone = (dropZone: { classList: string[] }) => { // check if an element of dropzone.classlist is in the array dragOptions.classList return _isValid(dropZone, dragOptions.validDropzones); }; - const _isValid = (element, classes) => { + const _isValid = (element: { classList: string[] }, classes: string[]) => { for (let i = 0; i < classes.length; i++) { - if ([...element.classList].indexOf(classes[i]) > -1) { + if ([...element.classList].indexOf(classes[i]!) > -1) { return true; } } diff --git a/app/src/helper/utils.ts b/app/src/helper/utils.ts new file mode 100644 index 0000000..a7daa8a --- /dev/null +++ b/app/src/helper/utils.ts @@ -0,0 +1,3 @@ +export function insertAfter(newNode: Node, existingNode: Node) { + existingNode.parentNode?.insertBefore(newNode, existingNode.nextSibling); +} diff --git a/js/html/edit.js b/app/src/pages/edit.ts similarity index 87% rename from js/html/edit.js rename to app/src/pages/edit.ts index c9b3eec..c593778 100644 --- a/js/html/edit.js +++ b/app/src/pages/edit.ts @@ -1,13 +1,14 @@ +import type { TreeConfig } from "../views/tree/components/treeTypes.js"; import EditTree from "../views/tree/editTree/components/editTree.js"; import { parse, stringify } from "../helper/jsurl.js"; import Button from "../views/other/button.js"; const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); -const treeConfig = parse(urlParams.get("t")) || {}; +const treeConfig = (parse(urlParams.get("t")) as TreeConfig | null) ?? { bmc: [], s: {}, t: {} }; const REPO_URL = "https://github.com/AlexW00/StartTreeV2"; -const t = new EditTree(treeConfig, true); +const t = new EditTree(treeConfig); document.body.appendChild(t.html()); const getExportUrl = () => { @@ -15,7 +16,7 @@ const getExportUrl = () => { path = location.pathname, affix = "?t=", data = stringify(t.export()); - return host + path.replace("e.html", "v.html") + affix + data; + return host + path.replace("edit.html", "view.html") + affix + data; }; const cancelButtonHtml = () => { diff --git a/app/src/pages/index.ts b/app/src/pages/index.ts new file mode 100644 index 0000000..8dd1f2e --- /dev/null +++ b/app/src/pages/index.ts @@ -0,0 +1,9 @@ +import { stringify } from "../helper/jsurl.js"; + +const exampleData = await fetch("./src/views/tree/exampleConfig.json").then( + (response) => response.json() +); + +const jsonStringified = stringify(exampleData); + +document.location.replace(`./pages/edit.html?t=${jsonStringified}`); diff --git a/js/html/view.js b/app/src/pages/view.ts similarity index 71% rename from js/html/view.js rename to app/src/pages/view.ts index 30f39da..393b94b 100644 --- a/js/html/view.js +++ b/app/src/pages/view.ts @@ -1,13 +1,14 @@ +import type { TreeConfig } from "../views/tree/components/treeTypes.js"; import Tree from "../views/tree/components/tree.js"; import { parse } from "../helper/jsurl.js"; import Button from "../views/other/button.js"; const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); -const treeConfig = parse(urlParams.get("t")) || {}; -const editModeHref = "./e.html"; +const treeConfig = (parse(urlParams.get("t")) as TreeConfig | null) ?? { bmc: [], s: {}, t: {} }; +const editModeHref = "./edit.html"; -const t = new Tree(treeConfig, false); +const t = new Tree(treeConfig); document.body.appendChild(t.html()); const editModeButtonHtml = () => { diff --git a/js/views/other/button.js b/app/src/views/other/button.ts similarity index 98% rename from js/views/other/button.js rename to app/src/views/other/button.ts index 7bb3b2e..23a88ce 100644 --- a/js/views/other/button.js +++ b/app/src/views/other/button.ts @@ -3,8 +3,11 @@ // ====================================================== // export default class Button { + svg: string; + type: string; + // possible values for type: "save", "cancel", "add" or "delete" - constructor(type, width, height) { + constructor(type: string, width?: number, height?: number) { this.type = type; if (type === "add") { this.svg = ` diff --git a/js/views/tree/components/searchBar.js b/app/src/views/tree/components/searchBar.ts similarity index 90% rename from js/views/tree/components/searchBar.js rename to app/src/views/tree/components/searchBar.ts index 774d967..071976b 100644 --- a/js/views/tree/components/searchBar.js +++ b/app/src/views/tree/components/searchBar.ts @@ -3,7 +3,15 @@ // ====================================================== //; export default class SearchBar { - constructor(config) { + form!: HTMLFormElement; + formInput!: HTMLInputElement; + h1!: HTMLHeadingElement; + root!: HTMLDivElement; + searchEngineNameShort: string; + searchEngineUrl: string; + sectionName!: HTMLDivElement; + + constructor(config: { n?: string; u?: string }) { this.searchEngineUrl = config.u ?? "https://duckduckgo.com/?q="; this.searchEngineNameShort = config.n ?? "ddg"; } diff --git a/js/views/tree/components/tree.js b/app/src/views/tree/components/tree.ts similarity index 82% rename from js/views/tree/components/tree.js rename to app/src/views/tree/components/tree.ts index 781bb3f..9fa6b87 100644 --- a/js/views/tree/components/tree.js +++ b/app/src/views/tree/components/tree.ts @@ -1,3 +1,4 @@ +import type { TreeConfig, CategoryConfig } from "./treeTypes.js"; import ThemeChanger from "../themechanger/theme-changer.js"; import SearchBar from "./searchBar.js"; import TreeColumn from "./treeColumn.js"; @@ -7,7 +8,15 @@ import TreeColumn from "./treeColumn.js"; // ====================================================== // export default class Tree { - constructor(config) { + bookmarkColumns: TreeColumn[]; + bookmarkRow!: HTMLDivElement; + root!: HTMLDivElement; + searchBar: SearchBar; + themeChanger: ThemeChanger; + titlePrompt!: HTMLDivElement; + version: string; + + constructor(config: TreeConfig) { this.version = config.v || "0.0"; this.bookmarkColumns = this.initBookmarkColumns(config.bmc); @@ -17,15 +26,15 @@ export default class Tree { // ~~~~~~~~ initialization methods ~~~~~~~ // - initBookmarkColumns(config) { + initBookmarkColumns(config: CategoryConfig[][]) { return config.map((column) => new TreeColumn(column)); } - initSearchBar(config) { + initSearchBar(config: TreeConfig["s"]) { return new SearchBar(config); } - initThemeChanger(config) { + initThemeChanger(config: TreeConfig["t"]) { return new ThemeChanger(config); } diff --git a/js/views/tree/components/treeColumn.js b/app/src/views/tree/components/treeColumn.ts similarity index 85% rename from js/views/tree/components/treeColumn.js rename to app/src/views/tree/components/treeColumn.ts index a41d6ba..fae0501 100644 --- a/js/views/tree/components/treeColumn.js +++ b/app/src/views/tree/components/treeColumn.ts @@ -5,8 +5,15 @@ import TreeColumnCategory from "./treeColumnCategory.js"; // ====================================================== // export default class TreeColumn { + bookmarkCategories: TreeColumnCategory[]; + categoryList!: HTMLUListElement; + columnTitle!: HTMLHeadingElement; + id: string; + root!: HTMLDivElement; + tree!: HTMLDivElement; + static count = 0; - constructor(bookmarkColumn) { + constructor(bookmarkColumn: { cn: string; b: { n: string; u: string }[] }[]) { TreeColumn.count++; this.id = `column-${TreeColumn.count}`; this.bookmarkCategories = this.initBookmarkCategories(bookmarkColumn); @@ -67,7 +74,7 @@ export default class TreeColumn { // ~~~~~~~~~~~~ class functionality ~~~~~~~~~~~~ // - initBookmarkCategories(categoryConfig) { + initBookmarkCategories(categoryConfig: { cn: string; b: { n: string; u: string }[] }[]) { return categoryConfig.map((bookmarkCategory) => { return new TreeColumnCategory(bookmarkCategory); }); diff --git a/js/views/tree/components/treeColumnCategory.js b/app/src/views/tree/components/treeColumnCategory.ts similarity index 85% rename from js/views/tree/components/treeColumnCategory.js rename to app/src/views/tree/components/treeColumnCategory.ts index eb9305a..1dbc230 100644 --- a/js/views/tree/components/treeColumnCategory.js +++ b/app/src/views/tree/components/treeColumnCategory.ts @@ -5,8 +5,15 @@ import TreeItem from "./treeItem.js"; export default class TreeColumnCategory { + bookmarkList!: HTMLUListElement; + categoryTitle!: HTMLHeadingElement; + id: string; + name: string; + root!: HTMLLIElement; + treeItems: TreeItem[]; + static count = 0; - constructor(bookmarkCategory) { + constructor(bookmarkCategory: { cn: string; b: { n: string; u: string }[] }) { TreeColumnCategory.count++; this.id = `category-${TreeColumnCategory.count}`; this.name = bookmarkCategory.cn; @@ -58,7 +65,7 @@ export default class TreeColumnCategory { // ~~~~~~~~~~~~ class functionality ~~~~~~~~~~~~ // - initTreeItems(bookmarkConfig) { + initTreeItems(bookmarkConfig: { n: string; u: string }[]) { return bookmarkConfig.map((bookmark) => { return new TreeItem(bookmark); }); diff --git a/js/views/tree/components/treeItem.js b/app/src/views/tree/components/treeItem.ts similarity index 86% rename from js/views/tree/components/treeItem.js rename to app/src/views/tree/components/treeItem.ts index 03b4b79..d318e96 100644 --- a/js/views/tree/components/treeItem.js +++ b/app/src/views/tree/components/treeItem.ts @@ -4,8 +4,14 @@ // export default class TreeItem { + a!: HTMLAnchorElement; + id: string; + name: string; + root!: HTMLLIElement; + url: string; + static count = 0; - constructor(bookmark) { + constructor(bookmark: { n: string; u: string }) { TreeItem.count++; this.id = `bookmark-${TreeItem.count}`; this.name = bookmark.n; @@ -39,7 +45,7 @@ export default class TreeItem { return root; } - generateLinkText(url) { + generateLinkText(url: string) { // if the url has a protocol, return the url, else append a protocol if (url.includes("//")) { return url; diff --git a/app/src/views/tree/components/treeTypes.ts b/app/src/views/tree/components/treeTypes.ts new file mode 100644 index 0000000..449e843 --- /dev/null +++ b/app/src/views/tree/components/treeTypes.ts @@ -0,0 +1,3 @@ +export type BookmarkConfig = { n: string; u: string }; +export type CategoryConfig = { cn: string; b: BookmarkConfig[] }; +export type TreeConfig = { v?: string; bmc: CategoryConfig[][]; s: { n?: string; u?: string }; t: { nr?: number } }; diff --git a/js/views/tree/editTree/components/editSearchBar.js b/app/src/views/tree/editTree/components/editSearchBar.ts similarity index 92% rename from js/views/tree/editTree/components/editSearchBar.js rename to app/src/views/tree/editTree/components/editSearchBar.ts index 4a7e8bd..a51706a 100644 --- a/js/views/tree/editTree/components/editSearchBar.js +++ b/app/src/views/tree/editTree/components/editSearchBar.ts @@ -9,13 +9,14 @@ import editorTarget from "../editor/helperObjects/editorTarget.js"; import Button from "../../../other/button.js"; export default class EditSearchBar extends SearchBar { - constructor(config) { + + constructor(config: { n?: string; u?: string }) { super(config); } // ~~~~~~~ override parent methods ~~~~~~~ // - renderHtml() { + override renderHtml() { this.root = this.rootHtml(); this.sectionName = this.#sectionNameHtml(); @@ -56,8 +57,8 @@ export default class EditSearchBar extends SearchBar { }), (editorFinishEvent) => { if (editorFinishEvent.type === "save") { - this.searchEngineUrl = editorFinishEvent.editResult.link; - this.searchEngineNameShort = editorFinishEvent.editResult.text; + this.searchEngineUrl = editorFinishEvent.editResult!.link; + this.searchEngineNameShort = editorFinishEvent.editResult!.text; this.root.prepend(this.#sectionNameHtml()); } else { this.root.prepend(this.#sectionNameHtml()); diff --git a/js/views/tree/editTree/components/editTree.js b/app/src/views/tree/editTree/components/editTree.ts similarity index 79% rename from js/views/tree/editTree/components/editTree.js rename to app/src/views/tree/editTree/components/editTree.ts index d42ea35..c83929d 100644 --- a/js/views/tree/editTree/components/editTree.js +++ b/app/src/views/tree/editTree/components/editTree.ts @@ -2,6 +2,8 @@ import ThemeChanger from "../../themechanger/theme-changer.js"; import EditSearchBar from "./editSearchBar.js"; import EditTreeColumn from "./editTreeColumn.js"; import Button from "../../../other/button.js"; +import type { TreeConfig, CategoryConfig } from "../../components/treeTypes.js"; +import TreeUpdateEvent from "../events/treeUpdateEvent.js"; import Tree from "../../components/tree.js"; // ====================================================== // @@ -9,27 +11,28 @@ import Tree from "../../components/tree.js"; // ====================================================== // export default class EditTree extends Tree { - constructor(config) { + + constructor(config: TreeConfig) { super(config); } // ~~~~~~~~ override parent methods ~~~~~~~ // - initBookmarkColumns(config) { + override initBookmarkColumns(config: CategoryConfig[][]) { return config.map( (column) => new EditTreeColumn(column, this.onColumnUpdate.bind(this)) ); } - initSearchBar(config) { + override initSearchBar(config: TreeConfig["s"]) { return new EditSearchBar(config); } - initThemeChanger(config) { + override initThemeChanger(config: TreeConfig["t"]) { return new ThemeChanger(config); } - renderHtml() { + override renderHtml() { this.root = super.renderHtml(); this.root.append(this.themeChanger.html()); this.titlePrompt.appendChild(this.#addColumnButtonHtml()); @@ -54,7 +57,7 @@ export default class EditTree extends Tree { // ~~~~~~~~~~~ callback methods ~~~~~~~~~~ // - onColumnUpdate(treeUpdateEvent) { + onColumnUpdate(treeUpdateEvent: TreeUpdateEvent) { if (treeUpdateEvent.type === "delete") { const index = this.bookmarkColumns.indexOf(treeUpdateEvent.updatedObject); this.bookmarkColumns.splice(index, 1); diff --git a/js/views/tree/editTree/components/editTreeColumn.js b/app/src/views/tree/editTree/components/editTreeColumn.ts similarity index 80% rename from js/views/tree/editTree/components/editTreeColumn.js rename to app/src/views/tree/editTree/components/editTreeColumn.ts index ca14029..e3b4906 100644 --- a/js/views/tree/editTree/components/editTreeColumn.js +++ b/app/src/views/tree/editTree/components/editTreeColumn.ts @@ -4,6 +4,7 @@ import Editor from "../editor/components/editor.js"; import editorTarget from "../editor/helperObjects/editorTarget.js"; import EditorOptions from "../editor/helperObjects/editorOptions.js"; import TreeUpdateEvent from "../events/treeUpdateEvent.js"; +import type { CategoryConfig } from "../../components/treeTypes.js"; import TreeColumn from "../../components/treeColumn.js"; // ====================================================== // @@ -11,14 +12,16 @@ import TreeColumn from "../../components/treeColumn.js"; // ====================================================== // export default class EditTreeColumn extends TreeColumn { - constructor(bookmarkColumn, onUpdate) { + onUpdate: (event: TreeUpdateEvent) => void; + + constructor(bookmarkColumn: CategoryConfig[], onUpdate: (event: TreeUpdateEvent) => void) { super(bookmarkColumn); this.onUpdate = onUpdate; } // ~~~~~~~~~~~~ override parent methods ~~~~~~~~~~~~ // - initBookmarkCategories(categoryConfig) { + override initBookmarkCategories(categoryConfig: CategoryConfig[]) { return categoryConfig.map((bookmarkCategory) => { return new EditTreeColumnCategory( bookmarkCategory, @@ -27,17 +30,17 @@ export default class EditTreeColumn extends TreeColumn { }); } - renderHtml() { + override renderHtml() { this.root = super.renderHtml(); - this.categoryList.lastElementChild.appendChild( + this.categoryList.lastElementChild?.appendChild( this.#newAddCategoryButton() ); return this.root; } - columnTitleHtml() { + override columnTitleHtml() { const h1 = super.columnTitleHtml(); - h1.addEventListener("click", () => this.#categoryTitleListener); + h1.addEventListener("click", this.#categoryTitleListener); return h1; } @@ -46,7 +49,7 @@ export default class EditTreeColumn extends TreeColumn { #categoryTitleListener = () => { new Editor( this.tree, - new editorTarget(".", null, this.h1.id), + new editorTarget(".", null, this.columnTitle.id), new EditorOptions({ allowTextEdit: false, buttons: ["cancel", "delete"], @@ -60,8 +63,8 @@ export default class EditTreeColumn extends TreeColumn { this.root.remove(); } else { this.tree.insertBefore( - this.h1, - this.tree.querySelectorAll("ul")[editorFinishEvent.index] + this.columnTitle, + this.tree.querySelectorAll("ul")[editorFinishEvent.index] ?? null ); } } @@ -82,10 +85,10 @@ export default class EditTreeColumn extends TreeColumn { } // listener for when the add button is clicked - #onAddCategoryButtonClick = (addCategoryButton) => { + #onAddCategoryButtonClick = (addCategoryButton: HTMLDivElement) => { // removes add button from last category const elements = this.categoryList.querySelectorAll("li.category"); - elements[elements.length - 1].removeChild(addCategoryButton); + elements[elements.length - 1]?.removeChild(addCategoryButton); // creates new category and appends it to the end of the node const newBookmarkCategory = new EditTreeColumnCategory( @@ -104,7 +107,7 @@ export default class EditTreeColumn extends TreeColumn { (editorFinishEvent) => { if (editorFinishEvent.type === "save") { // append new bookmark category to end of list and push to array - newBookmarkCategory.name = editorFinishEvent.editResult.text; + newBookmarkCategory.name = editorFinishEvent.editResult!.text; const newBookmarkCategoryHtml = newBookmarkCategory.renderHtml(); this.categoryList.appendChild(newBookmarkCategoryHtml); newBookmarkCategoryHtml.appendChild(addCategoryButton); @@ -112,7 +115,7 @@ export default class EditTreeColumn extends TreeColumn { this.bookmarkCategories.push(newBookmarkCategory); } else { // re append the button - this.categoryList.lastElementChild.appendChild(addCategoryButton); + this.categoryList.lastElementChild?.appendChild(addCategoryButton); } } ); @@ -120,7 +123,7 @@ export default class EditTreeColumn extends TreeColumn { // ~~~~~~~~~~~~ callbacks ~~~~~~~~~~~~ // - onCategoryUpdate(treeUpdateEvent) { + onCategoryUpdate(treeUpdateEvent: TreeUpdateEvent) { if (treeUpdateEvent.type === "delete") { const categoryIndex = this.bookmarkCategories.indexOf( treeUpdateEvent.updatedObject @@ -136,11 +139,11 @@ export default class EditTreeColumn extends TreeColumn { } // else if this was the last category in the list, append the add button to the new last category else if (categoryIndex === this.bookmarkCategories.length) { - const lastCategory = this.root.querySelector("ul").lastChild; - lastCategory.appendChild(this.#newAddCategoryButton(this.root)); + const lastCategory = this.root.querySelector("ul")?.lastChild; + lastCategory?.appendChild(this.#newAddCategoryButton()); } } - if (treeUpdateEvent.type === "add") { + if (treeUpdateEvent.type === "add" && treeUpdateEvent.newObject) { // get the index of the category to add const categoryIndex = this.bookmarkCategories.indexOf( treeUpdateEvent.updatedObject diff --git a/js/views/tree/editTree/components/editTreeColumnCategory.js b/app/src/views/tree/editTree/components/editTreeColumnCategory.ts similarity index 80% rename from js/views/tree/editTree/components/editTreeColumnCategory.js rename to app/src/views/tree/editTree/components/editTreeColumnCategory.ts index e2ea67d..090ae1d 100644 --- a/js/views/tree/editTree/components/editTreeColumnCategory.js +++ b/app/src/views/tree/editTree/components/editTreeColumnCategory.ts @@ -6,6 +6,7 @@ import EditorOptions from "../editor/helperObjects/editorOptions.js"; import TreeUpdateEvent from "../events/treeUpdateEvent.js"; import DragOptions from "../../../../helper/dragOptions.js"; import makeDraggable from "../../../../helper/makeDraggable.js"; +import type { CategoryConfig, BookmarkConfig } from "../../components/treeTypes.js"; import TreeColumnCategory from "../../components/treeColumnCategory.js"; // ====================================================== // @@ -13,28 +14,30 @@ import TreeColumnCategory from "../../components/treeColumnCategory.js"; // ====================================================== // export default class EditTreeColumnCategory extends TreeColumnCategory { - static count = 0; - constructor(bookmarkCategory, onUpdate) { + onUpdate: (event: TreeUpdateEvent) => void; + + static override count = 0; + constructor(bookmarkCategory: CategoryConfig, onUpdate: (event: TreeUpdateEvent) => void) { super(bookmarkCategory); this.onUpdate = onUpdate; } // ~~~~~~~ override parent methods ~~~~~~~ // - initTreeItems(bookmarkConfig) { + override initTreeItems(bookmarkConfig: BookmarkConfig[]) { return bookmarkConfig.map((bookmark) => { return new EditTreeItem(bookmark, this.onBookmarkUpdate.bind(this)); }); } - renderHtml() { + override renderHtml() { super.renderHtml(); this.root.appendChild(this.#newAddCategoryButton(this.bookmarkList)); this.#makeDraggable(); return this.root; } - categoryTitleHtml() { + override categoryTitleHtml() { const h1 = super.categoryTitleHtml(); h1.addEventListener("click", () => { this.#categoryTitleClickListener(h1); @@ -53,8 +56,7 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { } #makeDraggable = () => { - const dragOptionsData = this.export(); - dragOptionsData.classList = ["category"]; + const dragOptionsData = { ...this.export(), classList: ["category"] }; const dragOptions = new DragOptions({ data: JSON.stringify(dragOptionsData), validDropzones: ["category", "bookmark"], @@ -77,8 +79,8 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { this.onUpdate(new TreeUpdateEvent({ type: "delete", updatedObject: this })); } - #onDrop = (event) => { - const dragItemData = JSON.parse(event.dataTransfer.getData("text")); + #onDrop = (event: DragEvent) => { + const dragItemData = JSON.parse(event.dataTransfer?.getData("text") ?? "{}"); const dragItemClass = dragItemData.classList[0]; @@ -86,13 +88,13 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { else if (dragItemClass === "category") this.#onDropCategory(dragItemData); }; - #onDropCategory(dragItemData) { + #onDropCategory(dragItemData: CategoryConfig) { const draggedItem = new EditTreeColumnCategory( { cn: dragItemData.cn, b: dragItemData.b }, this.onUpdate ); const draggedItemHtml = draggedItem.html(); - this.root.parentNode.insertBefore(draggedItemHtml, this.root); + this.root.parentNode?.insertBefore(draggedItemHtml, this.root); this.onUpdate( new TreeUpdateEvent({ type: "add", @@ -102,7 +104,7 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { ); } - #onDropBookmark(dragItemData) { + #onDropBookmark(dragItemData: BookmarkConfig) { const draggedItem = new EditTreeItem( { n: dragItemData.n, u: dragItemData.u }, this.onBookmarkUpdate.bind(this) @@ -112,14 +114,14 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { // prepend this item to the list const categoryList = this.root.querySelector("ul"); this.treeItems.unshift(draggedItem); - categoryList.insertBefore( + categoryList?.insertBefore( draggedItemHtml, - categoryList.querySelector(".bookmark") + categoryList?.querySelector(".bookmark") ); } // TODO: create extra class for this?? - #newAddCategoryButton = (ul) => { + #newAddCategoryButton = (ul: HTMLUListElement) => { const addBookmarkButton = new Button("add").html(); addBookmarkButton.classList.add("add-bookmark-button"); addBookmarkButton.style.paddingLeft = "1.5rem"; @@ -142,8 +144,8 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { (editorFinishEvent) => { if (editorFinishEvent.type === "save") { // append new bookmark to end of list and push to array - newBookmark.name = editorFinishEvent.editResult.text; - newBookmark.url = editorFinishEvent.editResult.link ?? "#"; + newBookmark.name = editorFinishEvent.editResult!.text; + newBookmark.url = editorFinishEvent.editResult!.link ?? "#"; this.treeItems.push(newBookmark); ul.appendChild(newBookmark.renderHtml()); } @@ -153,7 +155,7 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { return addBookmarkButton; }; - #categoryTitleClickListener = (h1) => { + #categoryTitleClickListener = (h1: HTMLHeadingElement) => { new Editor( this.root, new editorTarget(this.name, null, h1.id), @@ -161,7 +163,7 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { (editorFinishEvent) => { if (editorFinishEvent.type === "save") { // if the editor exits with "save", update the category name - this.name = editorFinishEvent.editResult.text; + this.name = editorFinishEvent.editResult!.text; h1 = this.categoryTitleHtml(); this.root.insertBefore(h1, this.bookmarkList); this.onUpdate( @@ -183,13 +185,13 @@ export default class EditTreeColumnCategory extends TreeColumnCategory { // ~~~~~~~~~~~~~~ callbacks ~~~~~~~~~~~~~~ // - onBookmarkUpdate(treeUpdateEvent) { + onBookmarkUpdate(treeUpdateEvent: TreeUpdateEvent) { if (treeUpdateEvent.type === "delete") { this.treeItems.splice( this.treeItems.indexOf(treeUpdateEvent.updatedObject), 1 ); - } else if (treeUpdateEvent.type === "add") { + } else if (treeUpdateEvent.type === "add" && treeUpdateEvent.newObject) { // add the bookmark to bookmarks after the updated object, if updated object isn't in listit will be added to the end const dragTargetIndex = this.treeItems.indexOf(treeUpdateEvent.updatedObject) ?? diff --git a/js/views/tree/editTree/components/editTreeItem.js b/app/src/views/tree/editTree/components/editTreeItem.ts similarity index 82% rename from js/views/tree/editTree/components/editTreeItem.js rename to app/src/views/tree/editTree/components/editTreeItem.ts index ed19f2c..4b54910 100644 --- a/js/views/tree/editTree/components/editTreeItem.js +++ b/app/src/views/tree/editTree/components/editTreeItem.ts @@ -6,6 +6,7 @@ import DragOptions from "../../../../helper/dragOptions.js"; import makeDraggable from "../../../../helper/makeDraggable.js"; import { insertAfter } from "../../../../helper/utils.js"; import Button from "../../../other/button.js"; +import type { BookmarkConfig } from "../../components/treeTypes.js"; import TreeItem from "../../components/treeItem.js"; import Editor from "../editor/components/editor.js"; import EditorOptions from "../editor/helperObjects/editorOptions.js"; @@ -13,18 +14,21 @@ import editorTarget from "../editor/helperObjects/editorTarget.js"; import TreeUpdateEvent from "../events/treeUpdateEvent.js"; export default class EditTreeItem extends TreeItem { - constructor(bookmark, onUpdate) { + editButton!: HTMLDivElement; + onUpdate: (event: TreeUpdateEvent) => void; + + constructor(bookmark: BookmarkConfig, onUpdate: (event: TreeUpdateEvent) => void) { super(bookmark); this.onUpdate = onUpdate; } // ~~~~~~~ override parent methods ~~~~~~ // - html() { + override html() { return this.root ?? this.renderHtml(); } - renderHtml() { + override renderHtml() { this.root = super.renderHtml(); this.editButton = this.editButtonHtml(); this.root.appendChild(this.editButton); @@ -32,7 +36,7 @@ export default class EditTreeItem extends TreeItem { return this.root; } - linkHtml() { + override linkHtml() { const a = super.linkHtml(); a.addEventListener("click", (event) => { event.preventDefault(); @@ -51,8 +55,7 @@ export default class EditTreeItem extends TreeItem { } #makeDraggable = () => { - const dragOptionsData = this.export(); - dragOptionsData.classList = ["bookmark"]; + const dragOptionsData = { ...this.export(), classList: ["bookmark"] }; const dragOptions = new DragOptions({ data: JSON.stringify(dragOptionsData), validDropzones: ["bookmark"], @@ -68,8 +71,8 @@ export default class EditTreeItem extends TreeItem { }; // valid element is dropped on this element - #onDrop(event) { - const dragItemData = JSON.parse(event.dataTransfer.getData("text")), + #onDrop(event: DragEvent) { + const dragItemData = JSON.parse(event.dataTransfer?.getData("text") ?? "{}"), newEditTreeItem = new EditTreeItem( { n: dragItemData.n, u: dragItemData.u }, this.onUpdate @@ -93,7 +96,7 @@ export default class EditTreeItem extends TreeItem { // creates a new editor and adds it to the DOM #onBookmarkClick = () => { - const parentNode = this.root.parentNode; + const parentNode = this.root.parentNode as HTMLElement; new Editor( parentNode, new editorTarget(this.name, this.url, this.id), @@ -103,16 +106,16 @@ export default class EditTreeItem extends TreeItem { }), (editorFinishEvent) => { if (editorFinishEvent.type === "save") { - this.name = editorFinishEvent.editResult.text; - this.url = editorFinishEvent.editResult.link ?? "#"; + this.name = editorFinishEvent.editResult!.text; + this.url = editorFinishEvent.editResult!.link ?? "#"; parentNode.insertBefore( this.renderHtml(), - parentNode.querySelectorAll("li")[editorFinishEvent.index] + parentNode.querySelectorAll("li")[editorFinishEvent.index] ?? null ); } else if (editorFinishEvent.type === "cancel") { parentNode.insertBefore( this.html(), - parentNode.querySelectorAll("li")[editorFinishEvent.index] + parentNode.querySelectorAll("li")[editorFinishEvent.index] ?? null ); } else if (editorFinishEvent.type === "delete") { this.onUpdate( diff --git a/js/views/tree/editTree/editor/components/editor.js b/app/src/views/tree/editTree/editor/components/editor.ts similarity index 80% rename from js/views/tree/editTree/editor/components/editor.js rename to app/src/views/tree/editTree/editor/components/editor.ts index 084a877..975bcc3 100644 --- a/js/views/tree/editTree/editor/components/editor.js +++ b/app/src/views/tree/editTree/editor/components/editor.ts @@ -3,13 +3,30 @@ // ====================================================== // import EditorFinishEvent from "../events/editorFinishEvent.js"; +import EditorOptions from "../helperObjects/editorOptions.js"; +import EditorTarget from "../helperObjects/editorTarget.js"; import Toolbar from "./toolbar.js"; export default class Editor { - static openInstance = null; + allowTextEdit: boolean; + buttons: string[]; + cb: (event: EditorFinishEvent) => void; + editorTarget: EditorTarget; + index = 0; + isClosed = false; + link: string; + linkEditorIsOpen: boolean; + nodeType: string; + parentNode: HTMLElement; + root!: HTMLElement; + secondRow?: HTMLDivElement; + text: string; + toolbar!: Toolbar; + + static openInstance: Editor | null = null; // ~~~~~~~~~ Initialization functions ~~~~~~~~~ // - constructor(parentNode, editorTarget, editorOptions, cb) { + constructor(parentNode: HTMLElement, editorTarget: EditorTarget, editorOptions: EditorOptions, cb: (event: EditorFinishEvent) => void) { this.cb = cb; // callback function this.buttons = editorOptions.buttons; // ["link", "delete", "cancel"]; this.parentNode = parentNode; // the parent node of the edit target @@ -34,13 +51,13 @@ export default class Editor { this.root = this.html(); this.parentNode.replaceChild( this.root, - this.parentNode.querySelector("#" + this.editorTarget.id) + this.parentNode.querySelector("#" + this.editorTarget.id)! ); const children = this.parentNode.children; // loop over all for (let i = 0; i < children.length; i++) { - if (children[i].classList.contains("editor")) { + if (children[i]!.classList.contains("editor")) { this.index = i; break; } @@ -72,7 +89,7 @@ export default class Editor { #setMouseDownListener = () => { document.addEventListener("mousedown", (e) => { - if (this.root.contains(e.target)) return; + if (e.target instanceof Node && this.root.contains(e.target)) return; this.save(); }); }; @@ -93,7 +110,7 @@ export default class Editor { this.link = input.value; if (e.keyCode === 13 && input === document.activeElement) { e.preventDefault(); - this.save(this.root.parentNode); + this.save(this.root.parentNode ?? undefined); } }); @@ -102,7 +119,7 @@ export default class Editor { }; // creates an input field - #createInputField(li) { + #createInputField(li: HTMLElement) { // create input field const input = document.createElement("input"); @@ -111,7 +128,7 @@ export default class Editor { input.addEventListener("keyup", (e) => { this.text = input.value; if (e.keyCode === 13 && input === document.activeElement) { - this.save(li.parentNode); + this.save(li.parentNode ?? undefined); } }); if (!this.allowTextEdit) input.disabled = true; @@ -119,7 +136,7 @@ export default class Editor { } // creates the toolbar html - #createToolbar(li) { + #createToolbar(li: HTMLElement) { this.toolbar = new Toolbar(this.buttons, (buttonName) => { console.log(buttonName); if (buttonName === "cancel") { @@ -133,7 +150,7 @@ export default class Editor { return this.toolbar.html(); } - #toggleLinkInput(li) { + #toggleLinkInput(li: HTMLElement) { if (this.linkEditorIsOpen) { this.#closeLinkInput(li); this.toolbar.linkButton.classList.remove("active"); @@ -143,13 +160,13 @@ export default class Editor { } } - #openLinkInput(li) { + #openLinkInput(li: HTMLElement) { li.appendChild(this.#secondRowHtml()); } // ~~~~~~~ Toolbar event callbacks ~~~~~~ // - save() { + save(_parentNode?: Node) { if (this.isClosed) return; this.#remove(); this.cb( @@ -182,10 +199,10 @@ export default class Editor { }); } - #closeLinkInput() { + #closeLinkInput(_li?: Node): Promise { if (!this.linkEditorIsOpen) return Promise.resolve(); return new Promise((resolve) => { - const linkInput = this.root.querySelector(".secondRow"); + const linkInput = this.root.querySelector(".secondRow")!; linkInput.classList.add("slide-down-animate-out"); linkInput.onanimationend = () => { this.linkEditorIsOpen = false; diff --git a/js/views/tree/editTree/editor/components/toolbar.js b/app/src/views/tree/editTree/editor/components/toolbar.ts similarity index 85% rename from js/views/tree/editTree/editor/components/toolbar.js rename to app/src/views/tree/editTree/editor/components/toolbar.ts index 2243a5b..26709d4 100644 --- a/js/views/tree/editTree/editor/components/toolbar.js +++ b/app/src/views/tree/editTree/editor/components/toolbar.ts @@ -4,7 +4,13 @@ import Button from "../../../../other/button.js"; export default class Toolbar { - constructor(buttons, cb) { + buttons: string[]; + cancelButton?: HTMLDivElement; + cb: (button: string) => void; + deleteButton?: HTMLDivElement; + linkButton!: HTMLDivElement; + + constructor(buttons: string[], cb: (button: string) => void) { this.cb = cb; // callback function this.buttons = buttons; // ["link", "delete", "cancel"]; } diff --git a/js/views/tree/editTree/editor/events/editorFinishEvent.js b/app/src/views/tree/editTree/editor/events/editorFinishEvent.ts similarity index 69% rename from js/views/tree/editTree/editor/events/editorFinishEvent.js rename to app/src/views/tree/editTree/editor/events/editorFinishEvent.ts index 751ac84..acb63db 100644 --- a/js/views/tree/editTree/editor/events/editorFinishEvent.js +++ b/app/src/views/tree/editTree/editor/events/editorFinishEvent.ts @@ -3,7 +3,11 @@ // ====================================================== // export default class EditorFinishEvent { - constructor(type, editResult, index) { + editResult: { text: string; link: string } | null; + index: number; + type: string; + + constructor(type: string, editResult: { text: string; link: string } | null, index: number) { this.type = type; // either "save", "cancel" or "delete" this.editResult = editResult; // { text, link } this.index = index; // index of the edited item in the parent node diff --git a/js/views/tree/editTree/editor/helperObjects/editorOptions.js b/app/src/views/tree/editTree/editor/helperObjects/editorOptions.ts similarity index 75% rename from js/views/tree/editTree/editor/helperObjects/editorOptions.js rename to app/src/views/tree/editTree/editor/helperObjects/editorOptions.ts index 867f598..3332a18 100644 --- a/js/views/tree/editTree/editor/helperObjects/editorOptions.js +++ b/app/src/views/tree/editTree/editor/helperObjects/editorOptions.ts @@ -3,7 +3,17 @@ // ====================================================== // export default class EditorOptions { - constructor({ allowTextEdit, openWithLinkInput, buttons, nodeType }) { + allowTextEdit: boolean; + buttons: string[]; + nodeType: string; + openWithLinkInput: boolean; + + constructor({ allowTextEdit, openWithLinkInput, buttons, nodeType }: { + allowTextEdit?: boolean; + openWithLinkInput?: boolean; + buttons?: string[]; + nodeType?: string; + } = {}) { this.allowTextEdit = allowTextEdit ?? true; // whether or not to allow the user to edit the text of the editor this.openWithLinkInput = openWithLinkInput ?? false; // whether or not to open the link input when the editor is opened this.buttons = buttons ?? ["cancel", "delete"]; // the buttons to show in the editor diff --git a/js/views/tree/editTree/editor/helperObjects/editorTarget.js b/app/src/views/tree/editTree/editor/helperObjects/editorTarget.ts similarity index 77% rename from js/views/tree/editTree/editor/helperObjects/editorTarget.js rename to app/src/views/tree/editTree/editor/helperObjects/editorTarget.ts index dfdf9d0..9d867b4 100644 --- a/js/views/tree/editTree/editor/helperObjects/editorTarget.js +++ b/app/src/views/tree/editTree/editor/helperObjects/editorTarget.ts @@ -3,7 +3,11 @@ // ====================================================== // export default class editorTarget { - constructor(text, url, id) { + id: string; + text: string; + url: string; + + constructor(text: string, url: string | null, id: string) { this.text = text ?? ""; // text to be edited this.url = url ?? "#"; // url to be edited this.id = id; // id of the edited item diff --git a/js/views/tree/editTree/events/treeUpdateEvent.js b/app/src/views/tree/editTree/events/treeUpdateEvent.ts similarity index 71% rename from js/views/tree/editTree/events/treeUpdateEvent.js rename to app/src/views/tree/editTree/events/treeUpdateEvent.ts index 4676a5f..6adc878 100644 --- a/js/views/tree/editTree/events/treeUpdateEvent.js +++ b/app/src/views/tree/editTree/events/treeUpdateEvent.ts @@ -2,16 +2,24 @@ // =================== TreeUpdateEvent ================== // // ====================================================== // -export default class TreeUpdateEvent { +export default class TreeUpdateEvent { + newObject: T | undefined; + type: string; + updatedObject: T; + static legalTypes = ["save", "delete", "add"]; - constructor({ type, updatedObject, newObject }) { + constructor({ type, updatedObject, newObject }: { + type: string; + updatedObject: T; + newObject?: T; + }) { this.type = this.checkType(type); // event type, must be one of legalTypes this.updatedObject = updatedObject; // the updated object this.newObject = newObject; // the new object (if available) } - checkType(type) { + checkType(type: string) { if (!TreeUpdateEvent.legalTypes.includes(type)) { throw new Error( "TreeUpdateEvent: type must be 'save', 'cancel', or 'delete'. Found: " + diff --git a/js/views/tree/exampleConfig.json b/app/src/views/tree/exampleConfig.json similarity index 100% rename from js/views/tree/exampleConfig.json rename to app/src/views/tree/exampleConfig.json diff --git a/js/views/tree/themechanger/theme-changer.js b/app/src/views/tree/themechanger/theme-changer.ts similarity index 71% rename from js/views/tree/themechanger/theme-changer.js rename to app/src/views/tree/themechanger/theme-changer.ts index a6f35f8..1ebef7f 100644 --- a/js/views/tree/themechanger/theme-changer.js +++ b/app/src/views/tree/themechanger/theme-changer.ts @@ -4,6 +4,8 @@ import ThemeItem from "./themeItem.js"; export default class ThemeChanger { + themeNr: number; + themeChanger = document.createElement("div"); selectedThemeId = 2; @@ -39,17 +41,17 @@ export default class ThemeChanger { "catppuccin" ]; - themeCssLink = document.querySelector("link[href='../styles/colors.css']"); + themeCssLink = document.querySelector("link[href='../styles/colors.css']"); - constructor(config) { + constructor(config: { nr?: number }) { this.themeNr = config.nr ?? 0; this.addListener(); this.addListenerMouseLeave(); - this.changeTheme(this.THEMES[this.themeNr]); + this.changeTheme(this.THEMES[this.themeNr] ?? this.THEMES[0]!); } - changeTheme(nameTheme) { - this.themeCssLink.href = `../themes/${nameTheme}.css`; + changeTheme(nameTheme: string) { + this.themeCssLink!.href = `../themes/${nameTheme}.css`; } html() { @@ -70,10 +72,10 @@ export default class ThemeChanger { themeChangerContainer.appendChild(h1); themeChangerContainer.appendChild(this.themeChanger); // Change id for selected theme div - this.themeChanger.childNodes[this.selectedThemeId].id = "selected"; + (this.themeChanger.childNodes[this.selectedThemeId] as HTMLElement).id = "selected"; // Insert selected theme on the top this.themeChanger.insertBefore( - this.themeChanger.childNodes[this.selectedThemeId], + this.themeChanger.childNodes[this.selectedThemeId]!, this.themeChanger.firstChild ); @@ -83,14 +85,15 @@ export default class ThemeChanger { addListener = () => { document.addEventListener("click", (e) => { - if (e.target.name === "theme-radio") { + const target = e.target as HTMLInputElement; + if (target.name === "theme-radio") { // print index of clicked element - this.themeNr = this.THEMES.indexOf(e.target.id); - this.changeTheme(e.target.id); + this.themeNr = this.THEMES.indexOf(target.id); + this.changeTheme(target.id); const selectedTheme = document.querySelector("#selected"); - selectedTheme.removeAttribute("id"); - e.target.parentElement.id = "selected"; + selectedTheme?.removeAttribute("id"); + target.parentElement!.id = "selected"; } }); }; @@ -98,9 +101,9 @@ export default class ThemeChanger { addListenerMouseLeave = () => { this.themeChanger.addEventListener("mouseleave", function (event) { const selectedTheme = document.querySelector("#selected"); - const path = event.path || (event.composedPath && event.composedPath()); - path[0].insertBefore(selectedTheme, path[0].firstChild); - event.target.scrollTop = 0; + const target = event.currentTarget as HTMLElement; + if (selectedTheme) target.insertBefore(selectedTheme, target.firstChild); + target.scrollTop = 0; }); }; diff --git a/js/views/tree/themechanger/themeItem.js b/app/src/views/tree/themechanger/themeItem.ts similarity index 93% rename from js/views/tree/themechanger/themeItem.js rename to app/src/views/tree/themechanger/themeItem.ts index 56216c8..47b817d 100644 --- a/js/views/tree/themechanger/themeItem.js +++ b/app/src/views/tree/themechanger/themeItem.ts @@ -3,11 +3,13 @@ // ====================================================== // export default class ThemeItem { + name: string; + static themeCssLink = document.querySelector( "link[href='./styles/colors.css']" ); - constructor(themeName) { + constructor(themeName: string) { this.name = themeName; } diff --git a/styles/Hack.ttf b/app/styles/Hack.ttf similarity index 100% rename from styles/Hack.ttf rename to app/styles/Hack.ttf diff --git a/styles/colors.css b/app/styles/colors.css similarity index 100% rename from styles/colors.css rename to app/styles/colors.css diff --git a/styles/style.css b/app/styles/style.css similarity index 100% rename from styles/style.css rename to app/styles/style.css diff --git a/themes/MonokaiPro.css b/app/themes/MonokaiPro.css similarity index 100% rename from themes/MonokaiPro.css rename to app/themes/MonokaiPro.css diff --git a/themes/Programiz.css b/app/themes/Programiz.css similarity index 100% rename from themes/Programiz.css rename to app/themes/Programiz.css diff --git a/themes/autumn-mech.css b/app/themes/autumn-mech.css similarity index 100% rename from themes/autumn-mech.css rename to app/themes/autumn-mech.css diff --git a/themes/bigsur.css b/app/themes/bigsur.css similarity index 100% rename from themes/bigsur.css rename to app/themes/bigsur.css diff --git a/themes/black-ice.css b/app/themes/black-ice.css similarity index 100% rename from themes/black-ice.css rename to app/themes/black-ice.css diff --git a/themes/capitane.css b/app/themes/capitane.css similarity index 100% rename from themes/capitane.css rename to app/themes/capitane.css diff --git a/themes/carnival.css b/app/themes/carnival.css similarity index 100% rename from themes/carnival.css rename to app/themes/carnival.css diff --git a/themes/catppuccin.css b/app/themes/catppuccin.css similarity index 100% rename from themes/catppuccin.css rename to app/themes/catppuccin.css diff --git a/themes/cotton-candy.css b/app/themes/cotton-candy.css similarity index 100% rename from themes/cotton-candy.css rename to app/themes/cotton-candy.css diff --git a/themes/desert-sky.css b/app/themes/desert-sky.css similarity index 100% rename from themes/desert-sky.css rename to app/themes/desert-sky.css diff --git a/themes/ferns.css b/app/themes/ferns.css similarity index 100% rename from themes/ferns.css rename to app/themes/ferns.css diff --git a/themes/forest.css b/app/themes/forest.css similarity index 100% rename from themes/forest.css rename to app/themes/forest.css diff --git a/themes/gold-hunter.css b/app/themes/gold-hunter.css similarity index 100% rename from themes/gold-hunter.css rename to app/themes/gold-hunter.css diff --git a/themes/gruvbox.css b/app/themes/gruvbox.css similarity index 100% rename from themes/gruvbox.css rename to app/themes/gruvbox.css diff --git a/themes/intrigue.css b/app/themes/intrigue.css similarity index 100% rename from themes/intrigue.css rename to app/themes/intrigue.css diff --git a/themes/just-red.css b/app/themes/just-red.css similarity index 100% rename from themes/just-red.css rename to app/themes/just-red.css diff --git a/themes/monterey.css b/app/themes/monterey.css similarity index 100% rename from themes/monterey.css rename to app/themes/monterey.css diff --git a/themes/neon-pink-dark.css b/app/themes/neon-pink-dark.css similarity index 100% rename from themes/neon-pink-dark.css rename to app/themes/neon-pink-dark.css diff --git a/themes/neon.css b/app/themes/neon.css similarity index 100% rename from themes/neon.css rename to app/themes/neon.css diff --git a/themes/nord.css b/app/themes/nord.css similarity index 100% rename from themes/nord.css rename to app/themes/nord.css diff --git a/themes/orange-dark.css b/app/themes/orange-dark.css similarity index 100% rename from themes/orange-dark.css rename to app/themes/orange-dark.css diff --git a/themes/sierra.css b/app/themes/sierra.css similarity index 100% rename from themes/sierra.css rename to app/themes/sierra.css diff --git a/themes/slick-red.css b/app/themes/slick-red.css similarity index 100% rename from themes/slick-red.css rename to app/themes/slick-red.css diff --git a/themes/storm.css b/app/themes/storm.css similarity index 100% rename from themes/storm.css rename to app/themes/storm.css diff --git a/themes/this-ones-good.css b/app/themes/this-ones-good.css similarity index 100% rename from themes/this-ones-good.css rename to app/themes/this-ones-good.css diff --git a/themes/tomorrow-night-eighties.css b/app/themes/tomorrow-night-eighties.css similarity index 100% rename from themes/tomorrow-night-eighties.css rename to app/themes/tomorrow-night-eighties.css diff --git a/themes/void.css b/app/themes/void.css similarity index 100% rename from themes/void.css rename to app/themes/void.css diff --git a/themes/water-fire.css b/app/themes/water-fire.css similarity index 100% rename from themes/water-fire.css rename to app/themes/water-fire.css diff --git a/html/e.html b/html/e.html deleted file mode 100644 index 4478452..0000000 --- a/html/e.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - StartTreeV2 - Edit - - - - - - \ No newline at end of file diff --git a/html/v.html b/html/v.html deleted file mode 100644 index dc78b2b..0000000 --- a/html/v.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - StartTreeV2 - - - - - - diff --git a/index.html b/index.html deleted file mode 100644 index e342ee0..0000000 --- a/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - StartTreeV2 - Home - - - - - - - diff --git a/js/helper/utils.js b/js/helper/utils.js deleted file mode 100644 index b3bc283..0000000 --- a/js/helper/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function insertAfter(newNode, existingNode) { - existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); -} diff --git a/js/html/index.js b/js/html/index.js deleted file mode 100644 index 5fc1007..0000000 --- a/js/html/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import { stringify } from "../helper/jsurl.js"; - -const exampleData = await fetch("./js/views/tree/exampleConfig.json").then( - (response) => response.json() -); - -const jsonStringified = stringify(exampleData); - -document.location.replace(`./html/e.html?t=${jsonStringified}`); diff --git a/justfile b/justfile new file mode 100644 index 0000000..0ec5191 --- /dev/null +++ b/justfile @@ -0,0 +1,22 @@ +# Show available commands. +default: + @just --list + +# Check TypeScript types without writing files. +style: + @./node_modules/.bin/tsc --noEmit + +# Check types and build the static site in dist/. +build: style + @rm -rf dist + @mkdir -p dist + @cp app/index.html dist/index.html + @cp -R app/pages app/styles app/themes dist/ + @./node_modules/.bin/tsc + @mkdir -p dist/src/views/tree + @cp app/src/views/tree/exampleConfig.json dist/src/views/tree/exampleConfig.json + @touch dist/.nojekyll + +# Build and serve the site at http://localhost:8000/. +run: build + @node scripts/serve.mjs diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..88ae0a8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "starttree-v3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "starttree-v3", + "devDependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f4087a0 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "starttree-v3", + "private": true, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/readme.md b/readme.md index 5696e91..f664cbb 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ StartTreeV2 is a custom start page for your browser, also it's: - 🔒 **Private**: no database, saved via url - ⚡ **Lightweight**: zero dependencies -→ Start [HERE](https://alexw00.github.io/StartTreeV2/) +→ Start [HERE](https://macarthur955.github.io/StartTreeV3/) ## Guides @@ -84,22 +84,36 @@ Click on the top right check button to **copy** and **go** to your new URL. -## Featured +## Development -StartTreeV2 - A $tree styled start page generator. | Product Hunt +Install [Node.js 22](https://nodejs.org/en/download) (which includes +[npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)) and +[just](https://just.systems/man/en/packages.html). These are the only tools +needed for local development; `npm ci` installs the project's TypeScript +dependency from `package-lock.json`. +Run `npm ci` in the project directory before using the commands below. -## Development +The site source lives in `app/`: TypeScript in `app/src/`, pages in +`app/pages/`, and CSS and fonts in `app/styles/` and `app/themes/`. +The build writes browser-ready files to `dist/`, including compiled JavaScript +in `dist/src/` and pages in `dist/pages/`. + +| Command | Purpose | +| --- | --- | +| `just style` | Check TypeScript types | +| `just build` | Build the site in `dist/` | +| `just run` | Build and serve locally at `http://localhost:8000/` using Node.js | -The current version is stable and tested in Firefox, Chrome and Safari. -If you want to contribute, feel free to make a pull request. Please keep it simple and lightweight. +Run `just` to see the command list. For GitHub Pages, select **GitHub Actions** +as the publishing source in the repository's Pages settings.
Adding themes - 1. Create a new .css file in [/themes](https://github.com/AlexW00/StartTreeV2/tree/master/themes), which follows the same structure as the existing ones (most of them were generated using [Pywal](https://github.com/dylanaraps/pywal)). + 1. Create a new .css file in [app/themes](https://github.com/MacArthur955/StartTreeV3/tree/main/app/themes), which follows the same structure as the existing ones (most of them were generated using [Pywal](https://github.com/dylanaraps/pywal)). - 2. Add a new entry **at the end** of the `THEMES` array in [theme-changer.js](https://github.com/AlexW00/StartTreeV2/blob/master/js/views/tree/themechanger/theme-changer.js) + 2. Add a new entry **at the end** of the `THEMES` array in [theme-changer.ts](https://github.com/MacArthur955/StartTreeV3/blob/main/app/src/views/tree/themechanger/theme-changer.ts) 3. (make a pull request)
diff --git a/scripts/serve.mjs b/scripts/serve.mjs new file mode 100644 index 0000000..7f64d76 --- /dev/null +++ b/scripts/serve.mjs @@ -0,0 +1,38 @@ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, resolve, sep } from "node:path"; + +const root = resolve("dist"); +const types = { + ".css": "text/css", + ".html": "text/html", + ".js": "text/javascript", + ".json": "application/json", + ".ttf": "font/ttf", +}; + +createServer(async (request, response) => { + if (request.method !== "GET" && request.method !== "HEAD") { + response.writeHead(405, { Allow: "GET, HEAD" }).end(); + return; + } + + let file; + try { + const pathname = decodeURIComponent(new URL(request.url, "http://localhost").pathname); + file = resolve(root, `.${pathname === "/" ? "/index.html" : pathname}`); + if (!file.startsWith(root + sep)) { + response.writeHead(403).end(); + return; + } + const body = await readFile(file); + response.writeHead(200, { + "Content-Type": `${types[extname(file)] ?? "application/octet-stream"}; charset=utf-8`, + }); + response.end(request.method === "HEAD" ? undefined : body); + } catch (error) { + response.writeHead(error.code === "ENOENT" || error.code === "EISDIR" ? 404 : 400).end(); + } +}).listen(8000, "127.0.0.1", () => { + console.log("Serving http://localhost:8000/"); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5449079 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rootDir": "app/src", + "outDir": "dist/src", + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true, + "noEmitOnError": true + }, + "include": ["app/src/**/*.ts"] +}