chore (rewrite): frontend rewrite of filesystem

This commit is contained in:
MickaelK 2024-04-28 23:43:33 +10:00
parent f5753e8de7
commit 7e4480981d
13 changed files with 174 additions and 158 deletions

View file

@ -1,64 +0,0 @@
.component_dropdown {
position: relative;
}
.component_dropdown .dropdown_container {
display: none;
position: absolute;
right: 0;
}
.component_dropdown .dropdown_button {
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 4px;
padding: 5px;
min-width: 20px;
text-align: center;
}
.component_dropdown .dropdown_container {
padding-top: 5px;
z-index: 3;
}
.component_dropdown .dropdown_container:before {
content: ' ';
position: absolute;
right: 10px;
top: 1px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
}
.component_dropdown .dropdown_container ul {
margin: 0;
list-style-type: none;
background: white;
border: 1px solid var(--border);
box-shadow: 1px 1px 2px var(--border);
color: rgba(0,10,20,0.85);
border-radius: 3px;
padding: 3px 0px;
font-size: 0.92em;
}
.component_dropdown .dropdown_container ul li {
display: flex;
}
.component_dropdown .dropdown_container ul li > div {
width: 160px;
padding: 8px 5px 8px 10px;
}
.component_dropdown.active .dropdown_container {
display: block;
}
.component_dropdown.active .dropdown_container li {
background: white;
transition: background 0.1s ease-out;
}
.component_dropdown.active .dropdown_container li:hover {
background: rgba(0, 0, 0, 0.05);
}
.component_dropdown.active .dropdown_button {
border-color: var(--bg-color);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
}

View file

@ -5,6 +5,7 @@ body.dark-mode {
--border: #303438;
--dark: #2b2d30;
}
body.dark-mode input {

View file

@ -5,16 +5,8 @@
display: none;
position: absolute;
right: 0;
}
.component_dropdown .dropdown_button {
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 4px;
padding: 5px;
min-width: 20px;
text-align: center;
}
.component_dropdown .dropdown_container {
padding-top: 9px;
padding-top: 10px;
margin-top: 3px;
z-index: 3;
}
.component_dropdown .dropdown_container:before {
@ -26,42 +18,43 @@
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid white;
border-bottom: 10px solid var(--dark);
}
.component_dropdown .dropdown_container ul {
cursor: pointer;
margin: 0;
list-style-type: none;
background: white;
border: 1px solid var(--bg-color);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
color: var(--color);
color: var(--bg-color);
background: var(--dark);
border-radius: 3px;
padding: 3px;
font-size: 0.92em;
}
.component_dropdown .dropdown_container ul li {
justify-content: space-between;
display: flex;
}
.component_dropdown .dropdown_container ul li > div {
width: 150px;
padding: 5px 5px 5px 10px;
background: var(--dark);
}
.dark-mode .component_dropdown .dropdown_container ul { color: var(--color); }
.component_dropdown.active .dropdown_container {
display: block;
}
.component_dropdown.active .dropdown_container li {
background: white;
transition: background 0.1s ease-out;
}
.component_dropdown.active .dropdown_container li img.component_icon{
border: 2px solid rgba(0,0,0,0);
height: 15px;
width: 15px;
box-sizing: border-box;
align-self: center;
}
.component_dropdown.active .dropdown_container li:hover {
background: var(--border);
border-radius: 3px;
}
.component_dropdown.active .dropdown_button {
border-color: var(--bg-color);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
}

View file

@ -40,10 +40,16 @@ export function preventDefault() {
}
export function onClick($node) {
assert.type($node, window.HTMLElement);
return rxjs.fromEvent($node, "click").pipe(
rxjs.map(() => $node)
const sideE = ($node) => {
assert.type($node, window.HTMLElement);
return rxjs.fromEvent($node, "click").pipe(
rxjs.map(() => $node)
);
};
if ($node instanceof window.NodeList) return rxjs.merge(
...[...$node].map(($n) => sideE($n)),
);
return sideE($node);
}
export function onLoad($node) {

View file

@ -189,7 +189,6 @@ export default async function(render) {
rxjs.mergeMap((url) => ajax({ url, responseType: "json" })),
rxjs.tap(({ responseJSON }) => location.href = responseJSON.result),
rxjs.catchError(ctrlError()),
rxjs.mergeMap(() => rxjs.EMPTY),
);
}),
rxjs.mergeMap((formData) => { // CASE 3: regular login

View file

@ -8,12 +8,13 @@ import { AjaxError, ApplicationError } from "../lib/error.js";
import "../components/icon.js";
export default function(render = createRender(qs(document.body, "[role=\"main\"]"))) {
return async function(err) {
return function(err) {
const [msg, trace] = processError(err);
const $page = createElement(`
<div>
<style>${css}</style>
<a href="/" class="backnav">
<a href="${calculateBacklink(location.pathname)}" class="backnav">
<component-icon name="arrow_left"></component-icon>
home
</a>
@ -46,7 +47,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"]
rxjs.tap(() => location.reload())
));
return rxjs.of(err);
return rxjs.EMPTY;
};
}
@ -122,3 +123,20 @@ const css = `
vertical-align: middle;
}
`;
function calculateBacklink(pathname) {
let url = "/";
const listPath = pathname.replace(new RegExp("/$"), "").split("/");
switch (listPath[1]) {
case "view": // in view mode, navigate to current folder
listPath[1] = "files";
listPath.pop();
url = listPath.join("/") + "/";
break;
case "files": // in file browser mode, navigate to parent folder
listPath.pop();
url = listPath.join("/") + "/";
break;
}
return url === "/files/" ? "/" : url;
}

View file

@ -8,14 +8,14 @@ import { createLoader } from "../../components/loader.js";
import ctrlError from "../ctrl_error.js";
import { createThing } from "./thing.js";
// import { handleError, getFiles } from "./ctrl_filesystem_state.js";
import { getState$ } from "./ctrl_filesystem_state.js";
import { ls } from "./model_files.js";
export default async function(render) {
const $page = createElement(`
<div class="component_filesystem container">
<div class="ifscroll-before"></div>
<div class="list"></div>
<div data-target="list" class="list"></div>
<div class="ifscroll-after"></div>
<br>
</div>
@ -28,7 +28,19 @@ export default async function(render) {
effect(rxjs.of(path).pipe(
ls(),
removeLoader,
rxjs.mergeMap(({ files, path }) => { // STEP1: setup the list of files
rxjs.mergeMap(({ files, ...rest }) => getState$().pipe(rxjs.map((p) => {
// files = files.sort()
if (p.show_hidden === false) files = files.filter(({ name }) => name[0] !== ".");
return { ...rest, files, ...p };
}))),
rxjs.mergeMap(({ files, ...rest }) => {
if (files.length === 0) {
renderEmpty(render);
return rxjs.EMPTY;
}
return rxjs.of({...rest, files });
}),
rxjs.mergeMap(({ files, path, view }) => { // STEP1: setup the list of files
const FILE_HEIGHT = 160;
const BLOCK_SIZE = Math.ceil(document.body.clientHeight / FILE_HEIGHT) + 1;
// const BLOCK_SIZE = 6;
@ -38,26 +50,28 @@ export default async function(render) {
if (size > VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW);
}
const $list = qs($page, ".list");
const $list = qs($page, `[data-target="list"]`);
$list.closest(".scroll-y").scrollTop = 0;
const $fs = document.createDocumentFragment();
for (let i = 0; i < size; i++) {
const file = files[i];
$fs.appendChild(createThing({
name: file.name,
type: file.type,
link: createLink(file, path),
...createLink(file, path),
view,
}));
}
animate($list, { time: 200, keyframes: slideYIn(5) });
$list.appendChild($fs);
$list.replaceChildren($fs);
/// //////////////////////////////////////
//////////////////////////////////////
// CASE 1: virtual scroll isn't enabled
if (files.length <= VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
return rxjs.EMPTY;
}
/// //////////////////////////////////////
//////////////////////////////////////
// CASE 2: with virtual scroll
const $listBefore = qs($page, ".ifscroll-before");
const $listAfter = qs($page, ".ifscroll-after");
@ -148,9 +162,8 @@ export default async function(render) {
}));
else $fs.appendChild(createThing({
name: file.name,
// name: `file ${i}`,
type: file.type,
link: createLink(file, path),
...createLink(file, path),
}));
n += 1;
}
@ -171,6 +184,17 @@ export default async function(render) {
));
}
function renderEmpty(render) {
render(createElement(`
<div class="error">
<p class="empty_image no-select">
<img class="component_icon" draggable="false" src="/assets/icons/empty_folder.svg" alt="empty_folder">
</p>
<p class="label">There is nothing here</p>
</div>
`));
}
export function init() {
return Promise.all([
loadCSS(import.meta.url, "./ctrl_filesystem.css"),
@ -178,9 +202,10 @@ export function init() {
]);
}
function createLink(file, path) {
if (file.type === "file") {
return "/view" + path + file.name;
}
return "/files" + path + file.name + "/";
function createLink(file, filepath) {
let path = filepath + file.name;
let link = "";
if (file.type === "directory") path += "/";
link = file.type === "directory" ? "/files" + path : "/view" + path;
return { path, link };
}

View file

@ -1,39 +1,24 @@
import rxjs from "../../lib/rx.js";
import rxjs, { effect } from "../../lib/rx.js";
const state$ = new rxjs.BehaviorSubject({
search: null,
view: "grid",
sort: null,
view: null,
acl: {},
path: "/",
mutation: {},
error: null
order: null,
show_hidden: false,
});
export const getState$ = () => state$.asObservable();
export const onNewFile = () => {
console.log("CLICK NEW FILE");
};
export const setState = (...args) => {
const obj = { ...state$.value };
for (let i=0; i<args.length; i+=2) {
obj[args[i]] = args[i+1];
}
state$.next(obj);
}
export const handleError = () => {
return rxjs.catchError((err) => {
if (err) {
state$.next({
...state$.value,
error: err
});
}
return rxjs.empty();
});
};
export const onNewDirectory = () => {
console.log("CLICK NEW DIRECTORY");
};
export const onSearch = () => {
console.log("SEARCH");
};
export const getFiles = (n) => {};
effect(rxjs.fromEvent(window, "keydown").pipe(
rxjs.tap((e) => e.preventDefault()),
rxjs.filter((e) => e.ctrlKey && e.key === "h"),
rxjs.tap(() => setState("show_hidden", !state$.value.show_hidden)),
));

View file

@ -2,7 +2,7 @@ import { createElement, createRender, createFragment, onDestroy, nop } from "../
import rxjs, { effect, applyMutation, onClick, preventDefault } from "../../lib/rx.js";
import { animate } from "../../lib/animate.js";
import { loadCSS } from "../../helpers/loader.js";
import { qs } from "../../lib/dom.js";
import { qs, qsa } from "../../lib/dom.js";
import { getSelection$, clearSelection } from "./model_files.js";
import { setAction } from "./model_action.js";
@ -14,8 +14,10 @@ import componentDelete from "./modal_delete.js";
import "../../components/dropdown.js";
import "../../components/icon.js";
import { createModal } from "../../components/modal.js";
import { setState } from "./ctrl_filesystem_state.js";
const modalOpt = {
withButtonsRight: "OK",
withButtonsLeft: "CANCEL",
@ -121,6 +123,7 @@ function componentRight(render) {
MAGNIFYING_GLASS: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8cGF0aCBzdHlsZT0iZmlsbDojNjI2NDY5O2ZpbGwtb3BhY2l0eToxIiBkPSJNNTA1IDQ0Mi43TDQwNS4zIDM0M2MtNC41LTQuNS0xMC42LTctMTctN0gzNzJjMjcuNi0zNS4zIDQ0LTc5LjcgNDQtMTI4QzQxNiA5My4xIDMyMi45IDAgMjA4IDBTMCA5My4xIDAgMjA4czkzLjEgMjA4IDIwOCAyMDhjNDguMyAwIDkyLjctMTYuNCAxMjgtNDR2MTYuM2MwIDYuNCAyLjUgMTIuNSA3IDE3bDk5LjcgOTkuN2M5LjQgOS40IDI0LjYgOS40IDMzLjkgMGwyOC4zLTI4LjNjOS40LTkuNCA5LjQtMjQuNi4xLTM0ek0yMDggMzM2Yy03MC43IDAtMTI4LTU3LjItMTI4LTEyOCAwLTcwLjcgNTcuMi0xMjggMTI4LTEyOCA3MC43IDAgMTI4IDU3LjIgMTI4IDEyOCAwIDcwLjctNTcuMiAxMjgtMTI4IDEyOHoiIC8+Cjwvc3ZnPgo=",
SORT: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMjAgNTEyIj4KICA8cGF0aCBzdHlsZT0iZmlsbDojNjI2NDY5O2ZpbGwtb3BhY2l0eToxIiBkPSJNNDEgMjg4aDIzOGMyMS40IDAgMzIuMSAyNS45IDE3IDQxTDE3NyA0NDhjLTkuNCA5LjQtMjQuNiA5LjQtMzMuOSAwTDI0IDMyOWMtMTUuMS0xNS4xLTQuNC00MSAxNy00MXptMjU1LTEwNUwxNzcgNjRjLTkuNC05LjQtMjQuNi05LjQtMzMuOSAwTDI0IDE4M2MtMTUuMSAxNS4xLTQuNCA0MSAxNyA0MWgyMzhjMjEuNCAwIDMyLjEtMjUuOSAxNy00MXoiIC8+Cjwvc3ZnPgo=",
CHECK: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8cGF0aCBzdHlsZT0iZmlsbDojOTA5MDkwO2ZpbGwtb3BhY2l0eToxIiBkPSJNMTczLjg5OCA0MzkuNDA0bC0xNjYuNC0xNjYuNGMtOS45OTctOS45OTctOS45OTctMjYuMjA2IDAtMzYuMjA0bDM2LjIwMy0zNi4yMDRjOS45OTctOS45OTggMjYuMjA3LTkuOTk4IDM2LjIwNCAwTDE5MiAzMTIuNjkgNDMyLjA5NSA3Mi41OTZjOS45OTctOS45OTcgMjYuMjA3LTkuOTk3IDM2LjIwNCAwbDM2LjIwMyAzNi4yMDRjOS45OTcgOS45OTcgOS45OTcgMjYuMjA2IDAgMzYuMjA0bC0yOTQuNCAyOTQuNDAxYy05Ljk5OCA5Ljk5Ny0yNi4yMDcgOS45OTctMzYuMjA0LS4wMDF6IiAvPgo8L3N2Zz4K",
};
effect(getSelection$().pipe(
@ -145,20 +148,15 @@ function componentRight(render) {
<div class="component_dropdown view sort" data-target="sort">
<div class="dropdown_container">
<ul>
<li>
<div>
Sort By Type <span>
<span style="float: right;">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8cGF0aCBzdHlsZT0iZmlsbDojOTA5MDkwO2ZpbGwtb3BhY2l0eToxIiBkPSJNMTczLjg5OCA0MzkuNDA0bC0xNjYuNC0xNjYuNGMtOS45OTctOS45OTctOS45OTctMjYuMjA2IDAtMzYuMjA0bDM2LjIwMy0zNi4yMDRjOS45OTctOS45OTggMjYuMjA3LTkuOTk4IDM2LjIwNCAwTDE5MiAzMTIuNjkgNDMyLjA5NSA3Mi41OTZjOS45OTctOS45OTcgMjYuMjA3LTkuOTk3IDM2LjIwNCAwbDM2LjIwMyAzNi4yMDRjOS45OTcgOS45OTcgOS45OTcgMjYuMjA2IDAgMzYuMjA0bC0yOTQuNCAyOTQuNDAxYy05Ljk5OCA5Ljk5Ny0yNi4yMDcgOS45OTctMzYuMjA0LS4wMDF6IiAvPgo8L3N2Zz4K" alt="check">
</span>
</span>
</div>
<li data-target="type">
Sort By Type
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,${ICONS.CHECK}" alt="check" />
</li>
<li>
<div>Sort By Date</div>
<li data-target="date">
Sort By Date
</li>
<li>
<div>Sort By Name</div>
<li data-target="name">
Sort By Name
</li>
</ul>
</div>
@ -190,8 +188,32 @@ function componentRight(render) {
}
}),
),
onClick(qs($page, `[data-action="sort"]`)).pipe(rxjs.tap(() => {
onClick(qs($page, `[data-action="view"]`)).pipe(rxjs.tap(($button) => {
const $img = $button.querySelector("img");
if ($img.getAttribute("alt") === "list") {
setState("view", "grid");
$img.setAttribute("alt", "grid");
$img.setAttribute("src", "data:image/svg+xml;base64," + ICONS.GRID_VIEW);
} else {
setState("view", "list");
$img.setAttribute("alt", "list");
$img.setAttribute("src", "data:image/svg+xml;base64," + ICONS.LIST_VIEW);
}
})),
onClick(qs($page, `[data-action="sort"]`)).pipe(rxjs.mergeMap(() => {
qs($page, `[data-target="sort"]`).classList.toggle("active");
const $lis = qsa($page, `.dropdown_container li`);
return onClick($lis).pipe(rxjs.tap(($el) => {
setState(
"sort", $el.getAttribute("data-target"),
"order", !!$el.querySelector("img") ? "asc" : "des",
);
[...$lis].map(($li) => {
const $img = $li.querySelector("img");
if ($img) $img.remove();
});
$el.appendChild(createElement(`<img class="component_icon" src="data:image/svg+xml;base64,${ICONS.CHECK}" alt="check" />`));
}));
})),
)),
));

View file

@ -5,7 +5,7 @@ import { qs } from "../../lib/dom.js";
export default function(render) {
const $page = createElement(`
<div class="component_upload_queue">
<div class="component_upload_queue hidden">
<h2>CURRENT UPLOAD <div class="count_block">
<span class="completed">24</span>
<span class="grandTotal">24</span>

View file

@ -1,7 +1,7 @@
.component_thing {
clear: both;
}
.component_thing:hover .box, .component_thing .highlight.box {
.component_thing:hover .box, .component_thing .highlight.box, .component_thing.hover .box, .component_thing .highlight.box {
transition: 0.1s ease-out background;
background: var(--border);
border-color: var(--super-light);

View file

@ -7,7 +7,7 @@ const IMAGE = {
};
const $tmpl = createElement(`
<div class="component_thing view-grid not-selected" draggable="true">
<div class="component_thing not-selected view-grid" draggable="true">
<a href="__TEMPLATE__" data-link>
<div class="box">
<div class="component_checkbox"><input type="checkbox"><span class="indicator"></span></div>
@ -35,6 +35,7 @@ const $tmpl = createElement(`
export function createThing({
name = null,
type = "N/A",
path = null,
// size = 0,
// time = null,
link = "",
@ -48,6 +49,7 @@ export function createThing({
$label.textContent = name;
$thing.querySelector("a").setAttribute("href", link);
$thing.querySelector("img").setAttribute("src", (type === "file" ? IMAGE.FILE : IMAGE.FOLDER));
$thing.setAttribute("data-droptarget", type === "directory");
if (type === "hidden") $thing.classList.add("hidden");
$thing.querySelector(".component_checkbox").onclick = function(e) {
@ -55,5 +57,34 @@ export function createThing({
e.stopPropagation();
addSelection(name, type);
};
$thing.ondragstart = (e) => {
e.dataTransfer.setData("path", path);
$thing.classList.add("hover");
const crt = $thing.cloneNode(true);
$thing.style.opacity = "0.7";
const $box = crt.querySelector(".box");
crt.style.opacity = "0.2 "
crt.style.backgroundColor = "var(--border)";
$box.style.backgroundColor = "inherit";
$box.style.border = "none";
$box.style.borderRadius = "0";
$thing.closest("[data-target=\"list\"]").appendChild(crt);
e.dataTransfer.setDragImage(crt, 0, 0);
};
$thing.ondragover = (e) => {
if ($thing.getAttribute("data-droptarget") !== "true") return;
e.preventDefault();
$thing.classList.add("hover");
};
$thing.ondragleave = () => {
$thing.classList.remove("hover");
};
$thing.ondrop = (e) => {
$thing.classList.remove("hover");
console.log("DROPPED!", e.dataTransfer.getData("path"));
};
return $thing;
}

View file

@ -94,8 +94,8 @@ export default async function(render) {
return editor;
}),
rxjs.tap((editor) => requestAnimationFrame(() => editor.refresh())),
rxjs.share(),
rxjs.catchError(ctrlError()),
rxjs.share(),
);
effect(setup$);