Przeglądaj źródła

Add `pushstate` when previews are opened in lightbox and `popstate` when closed. Still work to do with potentially managing the `popstate` handler for files too. Potentially addresses the main problem in #59.

dom111 2 lat temu
rodzic
commit
7feaf710f2

+ 2 - 1
TODO.md

@@ -1,11 +1,12 @@
 - [x] Add more unit tests for the UI
 - [x] Add end-to-end UI testing
 - [x] Move to TypeScript
+- [x] Support keyboard navigation whilst overlay is visible
 - [ ] Add drag and drop tests
 - [ ] Allow uploading of directories ([#48](https://github.com/dom111/webdav-js/issues/48))
 - [ ] Add functionality for copying and moving files and directories
 - [ ] Add progress bar for file uploads
-- [ ] Support keyboard navigation whilst overlay is visible
 - [ ] ReactJS implementation
 - [ ] VueJS implementation
 - [ ] Maybe a refactor...
+- [ ] Add eventMap to `Event` object.

Plik diff jest za duży
+ 0 - 323
assets/css/style.css


+ 2 - 3
src/lib/DAV.ts

@@ -2,6 +2,7 @@ import EventObject from './EventObject';
 import HTTP from './HTTP';
 import Response from './DAV/Response';
 import joinPath from './joinPath';
+import trailingSlash from './trailingSlash';
 
 type ConstructorOptions = {
   bypassCheck?: boolean;
@@ -113,9 +114,7 @@ export default class DAV extends EventObject {
   }
 
   async list(uri, bypassCache = false) {
-    if (!uri.match(/\/$/)) {
-      uri = `${uri}/`;
-    }
+    uri = trailingSlash(uri);
 
     if (!bypassCache) {
       const cached = await this.#cache.get(uri);

+ 2 - 5
src/lib/DAV/Collection.ts

@@ -1,6 +1,7 @@
 import Entry from './Entry';
 import EventObject from '../EventObject';
 import joinPath from '../joinPath';
+import trailingSlash from '../trailingSlash';
 
 export default class Collection extends EventObject {
   #path;
@@ -95,13 +96,9 @@ export default class Collection extends EventObject {
         return;
       }
 
-      if (entry.directory && !destinationFullPath.endsWith('/')) {
-        destinationFullPath += '/';
-      }
-
       const newEntry = new Entry({
         directory: entry.directory,
-        fullPath: destinationFullPath,
+        fullPath: trailingSlash(destinationFullPath),
         modified: entry.modified,
         size: entry.size,
         mimeType: entry.mimeType,

+ 1 - 7
src/lib/DAV/Entry.ts

@@ -188,15 +188,13 @@ export default class Entry extends EventObject {
 
   get type(): string {
     if (!this.#type) {
-      let type;
-
       const types = {
         text: /\.(?:te?xt|i?nfo|php|cgi|faq|ini|htaccess|log|md|sql|sfv|conf|sh|pl|pm|py|rb|(?:s?c|sa)ss|js|java|coffee|[sx]?html?|xml)$/i,
         image: /\.(?:jpe?g|gif|a?png|svg)$/i,
         video: /\.(?:mp(?:e?g)?4|mov|avi|webm|ogv|mkv)$/i,
         audio: /\.(?:mp3|wav|ogg|flac|mka)$/i,
         font: /\.(?:woff2?|eot|[ot]tf)$/i,
-        pdf: /\.pdf/i,
+        pdf: /\.pdf$/i,
       };
 
       for (const [key, value] of Object.entries(types)) {
@@ -205,10 +203,6 @@ export default class Entry extends EventObject {
         }
       }
 
-      if (this.#mimeType && (type = this.#mimeType.split('/').shift())) {
-        return (this.#type = type);
-      }
-
       this.#type = 'unknown';
     }
 

+ 78 - 6
src/lib/UI/NativeDOM.ts

@@ -2,6 +2,7 @@ import Container from './NativeDOM/Container';
 import Footer from './NativeDOM/Footer';
 import Melba from 'melba-toast';
 import UI from './UI';
+import trailingSlash from '../trailingSlash';
 
 export default class NativeDOM extends UI {
   render(container = new Container(), footer = new Footer()) {
@@ -22,7 +23,18 @@ export default class NativeDOM extends UI {
         return typeof element[`on${eventName}`] === 'function';
       },
       isTouch = supportsEvent('touchstart'),
-      supportsDragDrop = supportsEvent('dragstart') && supportsEvent('drop');
+      supportsDragDrop = supportsEvent('dragstart') && supportsEvent('drop'),
+      updateTitle = (title) => {
+        if (document.title !== title) {
+          document.title = title;
+        }
+      },
+      updatePath = (path) => {
+        if (location.pathname !== path) {
+          history.pushState(history.state, path, path);
+        }
+      };
+
     // DOM events
     if (isTouch) {
       this.container.classList.add('is-touch');
@@ -33,7 +45,32 @@ export default class NativeDOM extends UI {
     }
 
     window.addEventListener('popstate', () => {
-      this.trigger('go');
+      const url = location.pathname;
+
+      element.dispatchEvent(
+        new CustomEvent('preview:close', {
+          bubbles: true,
+          detail: {
+            preview: true,
+          },
+        })
+      );
+
+      if (url.endsWith('/')) {
+        return this.trigger('go');
+      }
+
+      const path = url.replace(/[^/]+$/, '');
+
+      this.trigger('go', path, {
+        bypassPushState: true,
+        success: () =>
+          this.container
+            .querySelector(`main ul li[data-full-path="${url}"]`)
+            ?.dispatchEvent(new CustomEvent('click')),
+      });
+
+      // trigger opening file
     });
 
     if (supportsDragDrop) {
@@ -182,7 +219,15 @@ export default class NativeDOM extends UI {
 
     this.on(
       'go',
-      async (path = location.pathname, bypassCache = false, failure = null) => {
+      async (
+        path = location.pathname,
+        {
+          bypassCache = false,
+          bypassPushState = false,
+          failure = null,
+          success = null,
+        } = {}
+      ) => {
         const prevPath = location.pathname;
 
         this.trigger('list:update:request', path);
@@ -202,12 +247,39 @@ export default class NativeDOM extends UI {
 
         this.trigger('list:update:success', collection);
 
-        if (path !== prevPath) {
-          history.pushState(history.state, path, path);
+        if (!bypassPushState) {
+          updatePath(path);
         }
 
-        document.title = `${decodeURIComponent(path)} | WebDAV`;
+        updateTitle(`${decodeURIComponent(path)} | WebDAV`);
+
+        if (success) {
+          success(collection);
+        }
       }
     );
+
+    this.on('preview:opened', (entry) => {
+      document.body.classList.add('preview-open');
+      this.container
+        .querySelector(`[data-full-path="${entry.fullPath}"]`)
+        ?.focus();
+
+      updatePath(entry.fullPath);
+      updateTitle(`${decodeURIComponent(entry.fullPath)} | WebDAV`);
+    });
+
+    this.on('preview:closed', (entry, { preview = false } = {}) => {
+      if (preview) {
+        return;
+      }
+
+      const path = trailingSlash(entry.path);
+
+      document.body.classList.remove('preview-open');
+
+      updatePath(path);
+      updateTitle(`${decodeURIComponent(path)} | WebDAV`);
+    });
   }
 }

+ 2 - 1
src/lib/UI/NativeDOM/Footer.ts

@@ -1,5 +1,6 @@
 import Element from './Element';
 import joinPath from '../../joinPath';
+import trailingSlash from '../../trailingSlash';
 
 export default class Footer extends Element {
   constructor() {
@@ -39,7 +40,7 @@ export default class Footer extends Element {
 
         this.trigger(
           'create-directory',
-          `${joinPath(location.pathname, directoryName)}/`,
+          trailingSlash(joinPath(location.pathname, directoryName)),
           directoryName,
           location.pathname
         );

+ 46 - 3
src/lib/UI/NativeDOM/List.ts

@@ -40,15 +40,58 @@ export default class List extends Element {
       const current = this.element.querySelector(
           `li:focus${supportsFocusWithin ? ', li:focus-within' : ''}`
         ),
-        next = current
-          ? current.nextSibling
+        isPreview = document.body.classList.contains('preview-open'),
+        previewItems = [
+          ...this.element.querySelectorAll(
+            'li:not(.directory):not([data-type="unknown"])'
+          ),
+        ],
+        currentItemIndex = previewItems.indexOf(current),
+        next = isPreview
+          ? currentItemIndex > -1
+            ? previewItems.slice(currentItemIndex + 1).shift()
+            : null
+          : current
+          ? current.nextElementSibling
           : this.element.querySelector('li:first-child'),
-        previous = current ? current.previousSibling : null;
+        previous = isPreview
+          ? currentItemIndex > -1
+            ? previewItems.slice(0, currentItemIndex).pop()
+            : null
+          : current
+          ? current.previousElementSibling
+          : null;
 
       if (event.key === 'ArrowUp' && previous) {
         previous.focus();
+
+        if (isPreview) {
+          this.element.dispatchEvent(
+            new CustomEvent('preview:close', {
+              bubbles: true,
+              detail: {
+                preview: true,
+              },
+            })
+          );
+
+          previous.dispatchEvent(new CustomEvent('click'));
+        }
       } else if (event.key === 'ArrowDown' && next) {
         next.focus();
+
+        if (isPreview) {
+          this.element.dispatchEvent(
+            new CustomEvent('preview:close', {
+              bubbles: true,
+              detail: {
+                preview: true,
+              },
+            })
+          );
+
+          next.dispatchEvent(new CustomEvent('click'));
+        }
       }
     };
 

+ 23 - 6
src/lib/UI/NativeDOM/List/Item.ts

@@ -41,7 +41,7 @@ export default class Item extends Element {
   });
 
   constructor(entry, base64Encoder = btoa) {
-    super(`<li tabindex="0" data-full-path="${entry.fullPath}">
+    super(`<li tabindex="0" data-full-path="${entry.fullPath}" data-type="${entry.type}">
   <span class="title">${entry.title}</span>
   <input type="text" name="rename" class="hidden" readonly>
   <span class="size">${entry.displaySize}</span>
@@ -157,7 +157,9 @@ export default class Item extends Element {
       return;
     }
 
-    this.element.querySelector('[download]').click();
+    this.element
+      .querySelector('[download]')
+      .dispatchEvent(new CustomEvent('click'));
   }
 
   loading(loading = true) {
@@ -180,25 +182,40 @@ export default class Item extends Element {
     }
 
     const launchLightbox = (lightboxContent, onShow = null) => {
-      const escapeListener = (event) => {
+      const close = () => lightbox.close(),
+        escapeListener = (event) => {
           if (event.key === 'Escape') {
-            lightbox.close();
+            close();
           }
         },
         lightbox = BasicLightbox.create(lightboxContent, {
           className: entry.type,
           onShow: () => {
             this.loading(false);
+
             document.addEventListener('keydown', escapeListener);
+            document.addEventListener('preview:close', (event: CustomEvent) => {
+              lightbox.preview = event.detail?.preview;
+
+              close();
+            });
 
             if (onShow) {
               onShow(lightbox);
             }
           },
-          onClose: () =>
-            document.removeEventListener('keydown', escapeListener),
+          onClose: () => {
+            document.removeEventListener('keydown', escapeListener);
+            document.removeEventListener('preview:close', close);
+
+            this.trigger('preview:closed', this.#entry, {
+              preview: lightbox.preview,
+            });
+          },
         });
       lightbox.show();
+
+      this.trigger('preview:opened', this.#entry);
     };
 
     if (['video', 'audio', 'image', 'font', 'pdf'].includes(entry.type)) {

+ 4 - 0
src/lib/trailingSlash.ts

@@ -0,0 +1,4 @@
+export const trailingSlash = (text: string) =>
+  text.endsWith('/') ? text : `${text}/`;
+
+export default trailingSlash;

Plik diff jest za duży
+ 0 - 0
src/webdav-min.js


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików