diff --git a/public/assets/components/sidebar.js b/public/assets/components/sidebar.js index b2a789a7..268aa4eb 100644 --- a/public/assets/components/sidebar.js +++ b/public/assets/components/sidebar.js @@ -1,20 +1,15 @@ import { createElement, createRender, onDestroy } from "../lib/skeleton/index.js"; import rxjs, { effect, onClick } from "../lib/rx.js"; -import ajax from "../lib/ajax.js"; -import { toHref } from "../lib/skeleton/router.js"; -import { qs, qsa, safe } from "../lib/dom.js"; -import { forwardURLParams } from "../lib/path.js"; +import { qs } from "../lib/dom.js"; import { settingsGet, settingsSave } from "../lib/store.js"; -import { get as getConfig } from "../model/config.js"; import { loadCSS } from "../helpers/loader.js"; import t from "../locales/index.js"; -import cache from "../pages/filespage/cache.js"; -import { hooks, mv as mv$ } from "../pages/filespage/model_files.js"; -import { extractPath, isDir, isNativeFileUpload, isMobile } from "../pages/filespage/helper.js"; -import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js"; import { getCurrentPath } from "../pages/viewerpage/common.js"; import { generateSkeleton } from "./skeleton.js"; +import ctrlNavigationPane from "./sidebar_files.js"; +import ctrlTagPane from "./sidebar_tags.js"; + export default async function ctrlSidebar(render, {}) { if (new URL(location.toString()).searchParams.get("nav") === "false") return; else if (window.self !== window.top) return; @@ -133,261 +128,6 @@ const withInstantLoad = (function() { }; }()); -async function ctrlNavigationPane(render, { $sidebar, path }) { - // feature: init dom - const $fs = document.createDocumentFragment(); - const dirname = path.replace(new RegExp("[^\/]*$"), ""); - const chunks = dirname.split("/"); - for (let i=1; i { - const cleaners = [ - hooks.ls.listen(({ path }) => subscriber.next(path)), - hooks.mutation.listen(async({ op, path }) => { - if (["mv", "mkdir", "rm"].indexOf(op) === -1) return; - subscriber.next(path); - }), - ]; - return () => cleaners.map((fn) => fn()); - }).pipe( - rxjs.tap(async(path) => { - const display = path === "/" ? render : createRender(qs($sidebar, `[data-path="${path}"] ul`)); - display(await _createListOfFiles(path, {})); - }), - )); - - // feature: highlight current selection - try { - const $active = qs($sidebar, `[data-path="${dirname}"] a`); - const checkVisible = ($el) => { - const rect = $el.getBoundingClientRect(); - return rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth); - }; - $active.setAttribute("aria-selected", "true"); - const tags = new URLSearchParams(location.search).getAll("tag").length; - if (checkVisible($active) === false && tags === 0) { - $active.offsetTop < window.innerHeight - ? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" }) - : $active.scrollIntoView({ behavior: "smooth", block: "nearest" }); - } - } catch (err) {} - - // feature: quick search - effect(rxjs.fromEvent(qs($sidebar, "h3 input"), "keydown").pipe( - rxjs.debounceTime(200), - rxjs.tap((e) => { - const inputValue = e.target.value.toLowerCase(); - qsa($sidebar, "[data-bind=\"your-files\"] li a").forEach(($li) => { - if (inputValue === "") { - $li.classList.remove("hidden"); - $sidebar.classList.remove("search"); - return; - } - $sidebar.classList.add("search"); - qs($li, "div").textContent.toLowerCase().indexOf(inputValue) === -1 - ? $li.classList.add("hidden") - : $li.classList.remove("hidden"); - }); - }), - rxjs.finalize(() => $sidebar.classList.remove("search")), - )); -} - -async function ctrlTagPane(render, { tags, path }) { - if (getConfig("enable_tags", false) === false) { - render(document.createElement("div")); - return; - } - const $page = createElement(` -
-

- tag - ${t("Tags")} -

- -
- `); - const renderTaglist = createRender(qs($page, `[data-bind="taglist"]`)); - effect(rxjs.merge( - tags.length === 0 ? rxjs.EMPTY : rxjs.of({ tags }), - ajax({ - url: forwardURLParams(`api/metadata/search`, ["share"]), - method: "POST", - responseType: "json", - body: { - tags: [], - path, - }, - }).pipe( - rxjs.map(({ responseJSON }) => { - const tags = {}; - Object.values(responseJSON.results).forEach((forms) => { - forms.forEach(({ id, value = "" }) => { - if (id !== "tags") return; - value.split(",").forEach((tag) => { - tags[tag.trim()] = null; - }); - }); - }); - return { tags: Object.keys(tags).sort(), response: responseJSON.results }; - }), - rxjs.catchError(() => rxjs.of({ tags: [] })), - ), - ).pipe( - // feature: create the DOM - rxjs.mergeMap(({ tags, response }) => { - render($page); - if (tags.length === 0) { - renderTaglist(document.createElement("div")); - return rxjs.EMPTY; - } - const $fragment = document.createDocumentFragment(); - tags.forEach((name) => { - const $tag = createElement(` - -
- - ${safe(name)} -
- - - - -
- `); - const url = new URL(location.href); - if (url.searchParams.getAll("tag").indexOf(name) === -1) { - $tag.setAttribute("href", forwardURLParams(toHref("/files" + path.replace(new RegExp("[^\/]+$"), "") + "?tag=" + name), ["share", "tag"])); - } else { - url.searchParams.delete("tag", name); - $tag.setAttribute("href", url.toString()); - $tag.setAttribute("aria-selected", "true"); - } - $fragment.appendChild($tag); - }); - return rxjs.of({ $list: renderTaglist($fragment), response }); - }), - // feature: tag mouse hover effect - rxjs.tap(({ $list, response }) => { - if (isMobile) return; - else if (!response) return; - $list.childNodes.forEach(($tag) => { - if ($tag.getAttribute("aria-selected") === "true") return; - const tagname = $tag.innerText.trim(); - const paths = []; - for (const path in response) { - const form = response[path].find(({ id }) => id === "tags"); - if (!form) continue; - const tags = form.value.split(",").map((val) => val.trim()); - if (tags.indexOf(tagname) === -1) continue; - paths.push(path); - } - $tag.onmouseenter = () => { - const $things = document.querySelectorAll(".component_thing"); - $things.forEach(($thing) => { - const thingpath = $thing.getAttribute("data-path"); - for (let i=0; i $things.forEach(($thing) => $thing.classList.remove("hover")); - }; - }); - }), - )); -} - -const mv = (from, to) => withVirtualLayer( - mv$(from, to), - mvVL(from, to), -); - -async function _createListOfFiles(path, { basename = null, dirname = null }) { - const r = await cache().get(path); - const whats = r === null - ? (basename ? [basename] : []) - : r.files - .filter(({ type, name }) => type === "directory" && name[0] !== ".") - .map(({ name }) => name) - .sort(); - - const MAX_DISPLAY = 100; - const $lis = document.createDocumentFragment(); - const $fragment = document.createDocumentFragment(); - const $ul = document.createElement("ul"); - for (let i=0; i - - directory -
${safe(whats[i])}
-
-
    - - `); - const $link = qs($li, "a"); - if ($link.getAttribute("href") === "/files" + dirname) { - $link.removeAttribute("href", ""); - $link.removeAttribute("data-link"); - } else { - $link.ondrop = async(e) => { - $link.classList.remove("highlight"); - const from = e.dataTransfer.getData("path"); - let to = $link.parentElement.getAttribute("data-path"); - const [, fromName] = extractPath(from); - to += fromName; - if (isDir(from)) to += "/"; - if (from === to) return; - await mv(from, to).toPromise(); - }; - $link.ondragover = (e) => { - if (isNativeFileUpload(e)) return; - e.preventDefault(); - $link.classList.add("highlight"); - }; - $link.ondragleave = () => { - $link.classList.remove("highlight"); - }; - } - - if (i <= MAX_DISPLAY) $lis.appendChild($li); - else $fragment.appendChild($li); - if (i === MAX_DISPLAY) { - const $more = createElement(` -
  • -
    ...
    -
  • - `); - $lis.appendChild($more); - $more.onclick = () => { - $ul.appendChild($fragment); - $more.remove(); - }; - } - } - $ul.appendChild($lis); - return $ul; -} - export function init() { return loadCSS(import.meta.url, "./sidebar.css"); } diff --git a/public/assets/components/sidebar_files.js b/public/assets/components/sidebar_files.js new file mode 100644 index 00000000..0e3e11ab --- /dev/null +++ b/public/assets/components/sidebar_files.js @@ -0,0 +1,155 @@ +import rxjs, { effect } from "../lib/rx.js"; +import { createElement, createRender } from "../lib/skeleton/index.js"; +import { toHref } from "../lib/skeleton/router.js"; +import { qs, qsa, safe } from "../lib/dom.js"; +import { forwardURLParams } from "../lib/path.js"; +import cache from "../pages/filespage/cache.js"; +import { extractPath, isDir, isNativeFileUpload } from "../pages/filespage/helper.js"; +import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js"; +import { hooks, mv as mv$ } from "../pages/filespage/model_files.js"; + +export default async function ctrlNavigationPane(render, { $sidebar, path }) { + // feature: init dom + const $fs = document.createDocumentFragment(); + const dirname = path.replace(new RegExp("[^\/]*$"), ""); + const chunks = dirname.split("/"); + for (let i=1; i { + const cleaners = [ + hooks.ls.listen(({ path }) => subscriber.next(path)), + hooks.mutation.listen(async({ op, path }) => { + if (["mv", "mkdir", "rm"].indexOf(op) === -1) return; + subscriber.next(path); + }), + ]; + return () => cleaners.map((fn) => fn()); + }).pipe( + rxjs.tap(async(path) => { + const display = path === "/" ? render : createRender(qs($sidebar, `[data-path="${path}"] ul`)); + display(await _createListOfFiles(path, {})); + }), + )); + + // feature: highlight current selection + try { + const $active = qs($sidebar, `[data-path="${dirname}"] a`); + const checkVisible = ($el) => { + const rect = $el.getBoundingClientRect(); + return rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth); + }; + $active.setAttribute("aria-selected", "true"); + const tags = new URLSearchParams(location.search).getAll("tag").length; + if (checkVisible($active) === false && tags === 0) { + $active.offsetTop < window.innerHeight + ? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" }) + : $active.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } catch (err) {} + + // feature: quick search + effect(rxjs.fromEvent(qs($sidebar, "h3 input"), "keydown").pipe( + rxjs.debounceTime(200), + rxjs.tap((e) => { + const inputValue = e.target.value.toLowerCase(); + qsa($sidebar, "[data-bind=\"your-files\"] li a").forEach(($li) => { + if (inputValue === "") { + $li.classList.remove("hidden"); + $sidebar.classList.remove("search"); + return; + } + $sidebar.classList.add("search"); + qs($li, "div").textContent.toLowerCase().indexOf(inputValue) === -1 + ? $li.classList.add("hidden") + : $li.classList.remove("hidden"); + }); + }), + rxjs.finalize(() => $sidebar.classList.remove("search")), + )); +} + +const mv = (from, to) => withVirtualLayer( + mv$(from, to), + mvVL(from, to), +); + +async function _createListOfFiles(path, { basename = null, dirname = null }) { + const r = await cache().get(path); + const whats = r === null + ? (basename ? [basename] : []) + : r.files + .filter(({ type, name }) => type === "directory" && name[0] !== ".") + .map(({ name }) => name) + .sort(); + + const MAX_DISPLAY = 100; + const $lis = document.createDocumentFragment(); + const $fragment = document.createDocumentFragment(); + const $ul = document.createElement("ul"); + for (let i=0; i + + directory +
    ${safe(whats[i])}
    +
    +
      + + `); + const $link = qs($li, "a"); + if ($link.getAttribute("href") === "/files" + dirname) { + $link.removeAttribute("href", ""); + $link.removeAttribute("data-link"); + } else { + $link.ondrop = async(e) => { + $link.classList.remove("highlight"); + const from = e.dataTransfer.getData("path"); + let to = $link.parentElement.getAttribute("data-path"); + const [, fromName] = extractPath(from); + to += fromName; + if (isDir(from)) to += "/"; + if (from === to) return; + await mv(from, to).toPromise(); + }; + $link.ondragover = (e) => { + if (isNativeFileUpload(e)) return; + e.preventDefault(); + $link.classList.add("highlight"); + }; + $link.ondragleave = () => { + $link.classList.remove("highlight"); + }; + } + + if (i <= MAX_DISPLAY) $lis.appendChild($li); + else $fragment.appendChild($li); + if (i === MAX_DISPLAY) { + const $more = createElement(` +
    • +
      ...
      +
    • + `); + $lis.appendChild($more); + $more.onclick = () => { + $ul.appendChild($fragment); + $more.remove(); + }; + } + } + $ul.appendChild($lis); + return $ul; +} diff --git a/public/assets/components/sidebar_tags.js b/public/assets/components/sidebar_tags.js new file mode 100644 index 00000000..a6cd6736 --- /dev/null +++ b/public/assets/components/sidebar_tags.js @@ -0,0 +1,118 @@ +import rxjs, { effect } from "../lib/rx.js"; +import { createElement, createRender } from "../lib/skeleton/index.js"; +import { toHref } from "../lib/skeleton/router.js"; +import ajax from "../lib/ajax.js"; +import { qs, safe } from "../lib/dom.js"; +import { forwardURLParams } from "../lib/path.js"; +import { get as getConfig } from "../model/config.js"; +import { isMobile } from "../pages/filespage/helper.js"; +import t from "../locales/index.js"; + +export default async function ctrlTagPane(render, { tags, path }) { + if (getConfig("enable_tags", false) === false) { + render(document.createElement("div")); + return; + } + const $page = createElement(` +
      +

      + tag + ${t("Tags")} +

      +
        +
      • +
      +
      + `); + const renderTaglist = createRender(qs($page, `[data-bind="taglist"]`)); + effect(rxjs.merge( + tags.length === 0 ? rxjs.EMPTY : rxjs.of({ tags }), + ajax({ + url: forwardURLParams(`api/metadata/search`, ["share"]), + method: "POST", + responseType: "json", + body: { + tags: [], + path, + }, + }).pipe( + rxjs.map(({ responseJSON }) => { + const tags = {}; + Object.values(responseJSON.results).forEach((forms) => { + forms.forEach(({ id, value = "" }) => { + if (id !== "tags") return; + value.split(",").forEach((tag) => { + tags[tag.trim()] = null; + }); + }); + }); + return { tags: Object.keys(tags).sort(), response: responseJSON.results }; + }), + rxjs.catchError(() => rxjs.of({ tags: [] })), + ), + ).pipe( + // feature: create the DOM + rxjs.mergeMap(({ tags, response }) => { + render($page); + if (tags.length === 0) { + renderTaglist(document.createElement("div")); + return rxjs.EMPTY; + } + const $fragment = document.createDocumentFragment(); + tags.forEach((name) => { + const $tag = createElement(` + +
      + + ${safe(name)} +
      + + + + +
      + `); + const url = new URL(location.href); + if (url.searchParams.getAll("tag").indexOf(name) === -1) { + $tag.setAttribute("href", forwardURLParams(toHref("/files" + path.replace(new RegExp("[^\/]+$"), "") + "?tag=" + name), ["share", "tag"])); + } else { + url.searchParams.delete("tag", name); + $tag.setAttribute("href", url.toString()); + $tag.setAttribute("aria-selected", "true"); + } + $fragment.appendChild($tag); + }); + return rxjs.of({ $list: renderTaglist($fragment), response }); + }), + // feature: tag mouse hover effect + rxjs.tap(({ $list, response }) => { + if (isMobile) return; + else if (!response) return; + $list.childNodes.forEach(($tag) => { + if ($tag.getAttribute("aria-selected") === "true") return; + const tagname = $tag.innerText.trim(); + const paths = []; + for (const path in response) { + const form = response[path].find(({ id }) => id === "tags"); + if (!form) continue; + const tags = form.value.split(",").map((val) => val.trim()); + if (tags.indexOf(tagname) === -1) continue; + paths.push(path); + } + $tag.onmouseenter = () => { + const $things = document.querySelectorAll(".component_thing"); + $things.forEach(($thing) => { + const thingpath = $thing.getAttribute("data-path"); + for (let i=0; i $things.forEach(($thing) => $thing.classList.remove("hover")); + }; + }); + }), + )); +} diff --git a/server/ctrl/static.go b/server/ctrl/static.go index d12898ea..b7c69135 100644 --- a/server/ctrl/static.go +++ b/server/ctrl/static.go @@ -285,6 +285,8 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) { "/assets/" + BUILD_REF + "/components/notification.js", "/assets/" + BUILD_REF + "/components/notification.css", "/assets/" + BUILD_REF + "/components/sidebar.js", + "/assets/" + BUILD_REF + "/components/sidebar_files.js", + "/assets/" + BUILD_REF + "/components/sidebar_tags.js", "/assets/" + BUILD_REF + "/components/sidebar.css", "/assets/" + BUILD_REF + "/components/dropdown.js", "/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.js",