瀏覽代碼

Further bugfixes and improvements.

- Fix `font-size` for users who have changed the default font.
- IE11 compatibility.
dom111 5 年之前
父節點
當前提交
1e73259962

+ 10 - 1
.babelrc

@@ -1,6 +1,15 @@
 {
 {
   "presets": [
   "presets": [
-    "@babel/preset-env",
+    [
+      "@babel/preset-env",
+      {
+        "useBuiltIns": "usage",
+        "corejs": 3,
+        "targets": {
+          "ie": "11"
+        }
+      }
+    ],
     "minify"
     "minify"
   ],
   ],
   "plugins": [
   "plugins": [

+ 2 - 2
.eslintignore

@@ -1,2 +1,2 @@
-/node_modules/**
-/src/webdav-min.js
+node_modules/**
+src/webdav-min.js

+ 3 - 1
README.md

@@ -19,7 +19,8 @@ Before releasing v2.0.0 I'd like to:
 - [x] Add keyboard navigation (up/down arrow)
 - [x] Add keyboard navigation (up/down arrow)
 - [x] Dynamically update the list when uploading files with placeholders
 - [x] Dynamically update the list when uploading files with placeholders
 - [x] Fix rename bug after successfully renaming a file (rename input box shows previous filename)
 - [x] Fix rename bug after successfully renaming a file (rename input box shows previous filename)
-- [ ] Test other browsers
+- [x] Test other browsers - Firefox and IE11
+- [x] Fix IE11
 
 
 Beyond that:
 Beyond that:
 
 
@@ -30,3 +31,4 @@ Beyond that:
 - [ ] Support keyboard navigation whilst overlay is visible
 - [ ] Support keyboard navigation whilst overlay is visible
 - [ ] Add progress bar for file uploads
 - [ ] Add progress bar for file uploads
 - [ ] Improve code in `item.js` - maybe split out the functionality into each action?
 - [ ] Improve code in `item.js` - maybe split out the functionality into each action?
+- [ ] Look into conversion to TypeScript

文件差異過大導致無法顯示
+ 0 - 0
assets/css/style-min.css


+ 4 - 0
assets/css/style.css

@@ -202,6 +202,10 @@ pre[class*="language-"] {
   cursor: help;
   cursor: help;
 }
 }
 /* assets/scss/style.scss */
 /* assets/scss/style.scss */
+html {
+  font-size: 16px;
+}
+
 html,
 html,
 body,
 body,
 ul {
 ul {

+ 4 - 0
assets/scss/style.scss

@@ -1,4 +1,8 @@
 // Mini reset
 // Mini reset
+html {
+  font-size: 16px;
+}
+
 html,
 html,
 body,
 body,
 ul {
 ul {

+ 13 - 0
build/examples-tag.sh

@@ -0,0 +1,13 @@
+[ ! -d ./tmp ] && mkdir -p ./tmp;
+
+VERSION="$1";
+
+node src/example.generator.js --cdn --version "$VERSION" > ./tmp/example-cdn.js;
+npm run --silent terser -- ./tmp/example-cdn.js -c -m -e > ./tmp/example-cdn-min.js;
+
+printf 'javascript:' > ./examples/bookmarklet/source-min.js;
+cat ./tmp/example-cdn-min.js >> ./examples/bookmarklet/source-min.js;
+
+printf '<script type="text/javascript">' > ./examples/apache-directory-list/header.html;
+cat ./tmp/example-cdn-min.js >> ./examples/apache-directory-list/header.html;
+printf '</script><!--' >> ./examples/apache-directory-list/header.html;

+ 6 - 2
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "webdav-js",
   "name": "webdav-js",
-  "version": "2.0.0-beta2",
+  "version": "2.0.0-rc2",
   "description": "WebDAV functionality intended for use as a bookmarklet or to make a simple Apache webserver an interactive WebDAV environment.",
   "description": "WebDAV functionality intended for use as a bookmarklet or to make a simple Apache webserver an interactive WebDAV environment.",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
@@ -26,6 +26,7 @@
     "@babel/core": "^7.6.2",
     "@babel/core": "^7.6.2",
     "@babel/plugin-proposal-class-properties": "^7.5.5",
     "@babel/plugin-proposal-class-properties": "^7.5.5",
     "@babel/plugin-proposal-private-methods": "^7.6.0",
     "@babel/plugin-proposal-private-methods": "^7.6.0",
+    "@babel/plugin-transform-runtime": "^7.6.2",
     "@babel/preset-env": "^7.6.2",
     "@babel/preset-env": "^7.6.2",
     "babel-eslint": "^10.0.3",
     "babel-eslint": "^10.0.3",
     "babel-loader": "^8.0.6",
     "babel-loader": "^8.0.6",
@@ -52,8 +53,11 @@
     "webpack-cli": "^3.3.9"
     "webpack-cli": "^3.3.9"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@babel/runtime": "^7.7.2",
     "basiclightbox": "^5.0.2",
     "basiclightbox": "^5.0.2",
+    "core-js": "3",
     "melba-toast": "^0.2.1",
     "melba-toast": "^0.2.1",
-    "prismjs": "^1.17.1"
+    "prismjs": "^1.17.1",
+    "whatwg-fetch": "^3.0.0"
   }
   }
 }
 }

+ 10 - 9
src/lib/DAV.js

@@ -1,6 +1,7 @@
 import EventObject from './EventObject.js';
 import EventObject from './EventObject.js';
 import HTTP from './HTTP.js';
 import HTTP from './HTTP.js';
 import Response from './DAV/Response.js';
 import Response from './DAV/Response.js';
+import joinPath from './joinPath.js';
 
 
 export default class DAV extends EventObject {
 export default class DAV extends EventObject {
   #cache;
   #cache;
@@ -64,16 +65,16 @@ export default class DAV extends EventObject {
     return this.#http.HEAD(uri);
     return this.#http.HEAD(uri);
   }
   }
 
 
-  async copy(from, to) {
+  async copy(from, to, entry) {
     return this.#dispatchWithEvents(() => this.#http.COPY(from, {
     return this.#dispatchWithEvents(() => this.#http.COPY(from, {
       headers: {
       headers: {
         Destination: this.#validDestination(to)
         Destination: this.#validDestination(to)
       }
       }
-    }), 'copy', from, to);
+    }), 'copy', from, to, entry);
   }
   }
 
 
-  async del(uri) {
-    return this.#dispatchWithEvents(() => this.#http.DELETE(uri), 'delete', uri);
+  async del(uri, entry) {
+    return this.#dispatchWithEvents(() => this.#http.DELETE(uri), 'delete', uri, entry);
   }
   }
 
 
   async get(uri) {
   async get(uri) {
@@ -109,20 +110,20 @@ export default class DAV extends EventObject {
     return collection;
     return collection;
   }
   }
 
 
-  async mkcol(dest) {
-    return this.#dispatchWithEvents(() => this.#http.MKCOL(dest), 'mkcol', dest);
+  async mkcol(fullPath, directoryName, path) {
+    return this.#dispatchWithEvents(() => this.#http.MKCOL(fullPath), 'mkcol', fullPath, directoryName, path);
   }
   }
 
 
-  async move(from, to) {
+  async move(from, to, entry) {
     return this.#dispatchWithEvents(() => this.#http.MOVE(from, {
     return this.#dispatchWithEvents(() => this.#http.MOVE(from, {
       headers: {
       headers: {
         Destination: this.#validDestination(to)
         Destination: this.#validDestination(to)
       }
       }
-    }), 'move', from, to);
+    }), 'move', from, to, entry);
   }
   }
 
 
   async upload(path, file) {
   async upload(path, file) {
-    const targetFile = path + file.name;
+    const targetFile = joinPath(path, file.name);
 
 
     return this.#dispatchWithEvents(() => this.#http.PUT(targetFile, {
     return this.#dispatchWithEvents(() => this.#http.PUT(targetFile, {
       headers: {
       headers: {

+ 14 - 19
src/lib/DAV/Collection.js

@@ -1,5 +1,6 @@
 import Entry from './Entry.js';
 import Entry from './Entry.js';
 import EventObject from '../EventObject.js';
 import EventObject from '../EventObject.js';
+import joinPath from '../joinPath.js';
 
 
 export default class Collection extends EventObject {
 export default class Collection extends EventObject {
     #path;
     #path;
@@ -21,7 +22,7 @@ export default class Collection extends EventObject {
       // the first entry is a stub for the directory itself, we can remove that for the root path...
       // the first entry is a stub for the directory itself, we can remove that for the root path...
       const parent = this.#entries.shift();
       const parent = this.#entries.shift();
 
 
-      this.#path = parent.fullPath;
+      this.#path = joinPath(parent.fullPath);
 
 
       if (parent.fullPath !== '/') {
       if (parent.fullPath !== '/') {
         // ...but change the details for all others.
         // ...but change the details for all others.
@@ -37,22 +38,19 @@ export default class Collection extends EventObject {
 
 
     bindEvents() {
     bindEvents() {
       this.on('upload:request', (path, file) => {
       this.on('upload:request', (path, file) => {
-        if (path === this.#path) {
-          const entry = new Entry({
-            fullPath: path + file.name,
+        if (joinPath(path) === this.#path) {
+          this.add(new Entry({
+            fullPath: joinPath(path, file.name),
             modified: file.lastModifiedDate,
             modified: file.lastModifiedDate,
             size: file.size,
             size: file.size,
             mimeType: file.type,
             mimeType: file.type,
-            placeholder: true,
-            collection: this
-          });
-
-          this.add(entry);
+            placeholder: true
+          }));
         }
         }
       });
       });
 
 
       this.on('upload:success', (path, completedFile) => {
       this.on('upload:success', (path, completedFile) => {
-        const [completedEntry] = this.filter((entry) => entry.fullPath === path + completedFile.name);
+        const [completedEntry] = this.filter((entry) => entry.fullPath === joinPath(path, completedFile.name));
 
 
         if (completedEntry) {
         if (completedEntry) {
           completedEntry.placeholder = false;
           completedEntry.placeholder = false;
@@ -60,7 +58,7 @@ export default class Collection extends EventObject {
       });
       });
 
 
       this.on('upload:failed', (path, failedFile) => {
       this.on('upload:failed', (path, failedFile) => {
-        const [failedEntry] = this.filter((entry) => entry.fullPath === path + failedFile.name);
+        const [failedEntry] = this.filter((entry) => entry.fullPath === joinPath(path, failedFile.name));
 
 
         if (failedEntry) {
         if (failedEntry) {
           this.remove(failedEntry);
           this.remove(failedEntry);
@@ -88,8 +86,7 @@ export default class Collection extends EventObject {
           modified: entry.modified,
           modified: entry.modified,
           size: entry.size,
           size: entry.size,
           mimeType: entry.mimeType,
           mimeType: entry.mimeType,
-          del: entry.del,
-          collection: this
+          del: entry.del
         });
         });
 
 
         this.remove(entry);
         this.remove(entry);
@@ -101,21 +98,19 @@ export default class Collection extends EventObject {
         this.trigger('cache:invalidate', newEntry.path);
         this.trigger('cache:invalidate', newEntry.path);
       });
       });
 
 
-      this.on('mkcol:success', (destination) => {
-        const [, path] = destination.match(/^(.*\/)[^/]+$/);
-
-        if (path === this.#path) {
+      this.on('mkcol:success', (destination, directoryName, path) => {
+        if (joinPath(path) === this.#path) {
           this.add(new Entry({
           this.add(new Entry({
             directory: true,
             directory: true,
             fullPath: destination,
             fullPath: destination,
-            modified: new Date(),
-            collection: this
+            modified: new Date()
           }));
           }));
         }
         }
       });
       });
     }
     }
 
 
     add(entry) {
     add(entry) {
+      entry.collection = this;
       this.#entries.push(entry);
       this.#entries.push(entry);
 
 
       this.#sort();
       this.#sort();

+ 13 - 2
src/lib/DAV/Entry.js

@@ -1,4 +1,5 @@
 import EventObject from '../EventObject.js';
 import EventObject from '../EventObject.js';
+import joinPath from '../joinPath.js';
 
 
 export default class Entry extends EventObject {
 export default class Entry extends EventObject {
   #del;
   #del;
@@ -11,6 +12,7 @@ export default class Entry extends EventObject {
   #name;
   #name;
   #path;
   #path;
   #placeholder;
   #placeholder;
+  #rename;
   #size;
   #size;
   #title;
   #title;
   #type;
   #type;
@@ -25,19 +27,21 @@ export default class Entry extends EventObject {
     size = 0,
     size = 0,
     mimeType = '',
     mimeType = '',
     del = true,
     del = true,
+    rename = true,
     placeholder = false,
     placeholder = false,
     collection = null
     collection = null
   }) {
   }) {
     super();
     super();
 
 
     this.#directory = directory;
     this.#directory = directory;
-    this.#fullPath = fullPath;
+    this.#fullPath = joinPath(fullPath);
     [this.#path, this.#name] = this.getFilename();
     [this.#path, this.#name] = this.getFilename();
     this.#title = title;
     this.#title = title;
     this.#modified = modified;
     this.#modified = modified;
     this.#size = size;
     this.#size = size;
     this.#mimeType = mimeType;
     this.#mimeType = mimeType;
     this.#del = del;
     this.#del = del;
+    this.#rename = rename;
     this.#placeholder = placeholder;
     this.#placeholder = placeholder;
     this.collection = collection;
     this.collection = collection;
   }
   }
@@ -45,7 +49,9 @@ export default class Entry extends EventObject {
   createParentEntry() {
   createParentEntry() {
     return this.update({
     return this.update({
       fullPath: this.path,
       fullPath: this.path,
-      title: '&larr;'
+      title: '&larr;',
+      del: false,
+      rename: false
     });
     });
   }
   }
 
 
@@ -65,6 +71,7 @@ export default class Entry extends EventObject {
         size: this.size,
         size: this.size,
         mimeType: this.mimeType,
         mimeType: this.mimeType,
         del: this.del,
         del: this.del,
+        rename: this.rename,
         collection: this.collection
         collection: this.collection
       },
       },
       ...properties
       ...properties
@@ -138,6 +145,10 @@ export default class Entry extends EventObject {
     this.trigger('entry:update', this);
     this.trigger('entry:update', this);
   }
   }
 
 
+  get rename() {
+    return this.#rename;
+  }
+
   get size() {
   get size() {
     return this.#size;
     return this.#size;
   }
   }

+ 19 - 23
src/lib/UI/NativeDOM.js

@@ -5,7 +5,8 @@ import UI from './UI.js';
 
 
 export default class NativeDOM extends UI {
 export default class NativeDOM extends UI {
   render(container = new Container(), footer = new Footer()) {
   render(container = new Container(), footer = new Footer()) {
-    this.container.append(container.element, footer.element);
+    this.container.appendChild(container.element);
+    this.container.appendChild(footer.element);
 
 
     this.bindEvents();
     this.bindEvents();
 
 
@@ -82,7 +83,7 @@ export default class NativeDOM extends UI {
       if (existingFile) {
       if (existingFile) {
         // TODO: nicer notification
         // TODO: nicer notification
         // TODO: i18m
         // TODO: i18m
-        if (! confirm(`A file called '${existingFile.name}' already exists, would you like to overwrite it?`)) {
+        if (! confirm(`A file called '${existingFile.title}' already exists, would you like to overwrite it?`)) {
           return false;
           return false;
         }
         }
       }
       }
@@ -90,51 +91,48 @@ export default class NativeDOM extends UI {
       await this.dav.upload(path, file);
       await this.dav.upload(path, file);
     });
     });
 
 
-    this.on('upload:success', (path, file) => {
+    this.on('upload:success', async (path, file) => {
       new Melba({
       new Melba({
-        content: `'${file.name}' has been successfully uploaded.`,
+        content: `'${file.title}' has been successfully uploaded.`,
         type: 'success',
         type: 'success',
         hide: 5
         hide: 5
       });
       });
     });
     });
 
 
-    this.on('move', async (source, destination) => {
-      await this.dav.move(source, destination);
+    this.on('move', async (source, destination, entry) => {
+      await this.dav.move(source, destination, entry);
     });
     });
 
 
-    this.on('move:success', (source, destination) => {
-      const [, sourcePath, sourceFile] = source.match(/^(.*)\/([^/]+\/?)$/),
-        [, destinationUrl, destinationFile] = destination.match(/^(.*)\/([^/]+\/?)$/),
+    this.on('move:success', (source, destination, entry) => {
+      const [, destinationUrl, destinationFile] = destination.match(/^(.*)\/([^/]+\/?)$/),
         destinationPath = destinationUrl && destinationUrl.replace(
         destinationPath = destinationUrl && destinationUrl.replace(
           `${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ''}`,
           `${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ''}`,
           ''
           ''
         )
         )
       ;
       ;
 
 
-      if (sourcePath === destinationPath) {
+      if (entry.path === destinationPath) {
         return new Melba({
         return new Melba({
-          content: `'${sourceFile}' successfully renamed to '${destinationFile}'.`,
+          content: `'${entry.title}' successfully renamed to '${decodeURIComponent(destinationFile)}'.`,
           type: 'success',
           type: 'success',
           hide: 5
           hide: 5
         });
         });
       }
       }
 
 
       new Melba({
       new Melba({
-        content: `'${sourceFile}' successfully moved to '${destinationPath}'.`,
+        content: `'${entry.title}' successfully moved to '${decodeURIComponent(destinationPath)}'.`,
         type: 'success',
         type: 'success',
         hide: 5
         hide: 5
       });
       });
     });
     });
 
 
-    this.on('delete', async (file) => {
-      await this.dav.del(file);
+    this.on('delete', async (path, entry) => {
+      await this.dav.del(path, entry);
     });
     });
 
 
-    this.on('delete:success', (path) => {
-      const filename = path && path.split(/\//).pop();
-
+    this.on('delete:success', (path, entry) => {
       new Melba({
       new Melba({
-        content: `'${filename}' has been deleted.`,
+        content: `'${entry.title}' has been deleted.`,
         type: 'success',
         type: 'success',
         hide: 5
         hide: 5
       });
       });
@@ -160,13 +158,11 @@ export default class NativeDOM extends UI {
       }
       }
     });
     });
 
 
-    this.on('create-directory', async (directoryName) => {
-      await this.dav.mkcol(directoryName);
+    this.on('create-directory', async (fullPath, directoryName, path) => {
+      await this.dav.mkcol(fullPath, directoryName, path);
     });
     });
 
 
-    this.on('mkcol:success', (path) => {
-      const directoryName = path && path.split(/\//).pop();
-
+    this.on('mkcol:success', (fullPath, directoryName) => {
       new Melba({
       new Melba({
         content: `'${directoryName}' has been created.`,
         content: `'${directoryName}' has been created.`,
         type: 'success',
         type: 'success',

+ 1 - 1
src/lib/UI/NativeDOM/Container.js

@@ -9,6 +9,6 @@ export default class Container extends Element {
 
 
     const list = new List();
     const list = new List();
 
 
-    this.element.append(list.element);
+    this.element.appendChild(list.element);
   }
   }
 }
 }

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

@@ -1,4 +1,5 @@
 import Element from './Element.js';
 import Element from './Element.js';
+import joinPath from '../../joinPath.js';
 
 
 export default class Footer extends Element {
 export default class Footer extends Element {
   constructor() {
   constructor() {
@@ -32,7 +33,7 @@ export default class Footer extends Element {
         return;
         return;
       }
       }
 
 
-      this.trigger('create-directory', location.pathname + directoryName, location.pathname);
+      this.trigger('create-directory', joinPath(location.pathname, directoryName), directoryName, location.pathname);
     });
     });
   }
   }
 }
 }

+ 8 - 5
src/lib/UI/NativeDOM/List.js

@@ -1,5 +1,6 @@
 import Element from './Element.js';
 import Element from './Element.js';
 import Item from './List/Item.js';
 import Item from './List/Item.js';
+import supportsFocusWithin from '../supportsFocusWithin.js';
 
 
 export default class List extends Element {
 export default class List extends Element {
   #collection;
   #collection;
@@ -31,22 +32,22 @@ export default class List extends Element {
     });
     });
 
 
     const arrowHandler = (event) => {
     const arrowHandler = (event) => {
-      if (! ['ArrowUp', 'ArrowDown'].includes(event.key)) {
+      if (! [38, 40].includes(event.which)) { // if (! ['ArrowUp', 'ArrowDown'].includes(event.key)) {
         return;
         return;
       }
       }
 
 
       event.preventDefault();
       event.preventDefault();
       event.stopPropagation();
       event.stopPropagation();
 
 
-      const current = this.element.querySelector('li:focus, li:focus-within'),
+      const current = this.element.querySelector(`li:focus${supportsFocusWithin ? ', li:focus-within' : ''}`),
         next = current ? current.nextSibling : this.element.querySelector('li:first-child'),
         next = current ? current.nextSibling : this.element.querySelector('li:first-child'),
         previous = current ? current.previousSibling : null
         previous = current ? current.previousSibling : null
       ;
       ;
 
 
-      if (event.key === 'ArrowUp' && previous) {
+      if (event.which === 38 && previous) { // if (event.key === 'ArrowUp' && previous) {
         previous.focus();
         previous.focus();
       }
       }
-      else if (event.key === 'ArrowDown' && next) {
+      else if (event.which === 40 && next) { // else if (event.key === 'ArrowDown' && next) {
         next.focus();
         next.focus();
       }
       }
     };
     };
@@ -68,7 +69,9 @@ export default class List extends Element {
 
 
     this.#items = collection.map((entry) => new Item(entry));
     this.#items = collection.map((entry) => new Item(entry));
 
 
-    this.element.append(...this.#items.map((item) => item.element));
+    [...this.#items.map((item) => item.element)]
+      .forEach((element) => this.element.appendChild(element))
+    ;
 
 
     this.loading(false);
     this.loading(false);
 
 

+ 43 - 25
src/lib/UI/NativeDOM/List/Item.js

@@ -1,6 +1,7 @@
 import * as BasicLightbox from 'basiclightbox';
 import * as BasicLightbox from 'basiclightbox';
 import Element from '../Element.js';
 import Element from '../Element.js';
 import Prism from 'prismjs';
 import Prism from 'prismjs';
+import joinPath from '../../../joinPath.js';
 
 
 export default class Item extends Element {
 export default class Item extends Element {
   #base64Encoder;
   #base64Encoder;
@@ -22,7 +23,7 @@ export default class Item extends Element {
       ;
       ;
 
 
       return `<style type="text/css">@font-face{font-family:"${fontName}";src:url("${entry.fullPath}") format("${formats[extension] || extension}")}</style>
       return `<style type="text/css">@font-face{font-family:"${fontName}";src:url("${entry.fullPath}") format("${formats[extension] || extension}")}</style>
-<h1 style="font-family:'${fontName}'">${entry.name}</h1>
+<h1 style="font-family:'${fontName}'">${entry.title}</h1>
 <p style="font-family:'${fontName}';font-size:1.5em">${demoText}</p>
 <p style="font-family:'${fontName}';font-size:1.5em">${demoText}</p>
 <p style="font-family:'${fontName}'">${demoText}</p>
 <p style="font-family:'${fontName}'">${demoText}</p>
 <p style="font-family:'${fontName}'"><strong>${demoText}</strong></p>
 <p style="font-family:'${fontName}'"><strong>${demoText}</strong></p>
@@ -59,6 +60,14 @@ export default class Item extends Element {
       this.element.classList.add('loading');
       this.element.classList.add('loading');
     }
     }
 
 
+    if (! entry.del) {
+      this.element.querySelector('.delete').setAttribute('hidden', '');
+    }
+
+    if (! entry.rename) {
+      this.element.querySelector('.rename').setAttribute('hidden', '');
+    }
+
     this.bindEvents();
     this.bindEvents();
   }
   }
 
 
@@ -86,6 +95,7 @@ export default class Item extends Element {
     element.querySelector('[download]').addEventListener('click', (event) => event.stopPropagation());
     element.querySelector('[download]').addEventListener('click', (event) => event.stopPropagation());
 
 
     element.querySelector('.delete').addEventListener('click', (event) => {
     element.querySelector('.delete').addEventListener('click', (event) => {
+      event.preventDefault();
       event.stopPropagation();
       event.stopPropagation();
 
 
       this.del();
       this.del();
@@ -99,16 +109,20 @@ export default class Item extends Element {
     });
     });
 
 
     element.addEventListener('keydown', (event) => {
     element.addEventListener('keydown', (event) => {
-      if (['F2', 'Delete', 'Enter'].includes(event.key)) {
+      if ([113, 46, 13].includes(event.which)) { // if (['F2', 'Delete', 'Enter'].includes(event.key)) {
         event.preventDefault();
         event.preventDefault();
 
 
-        if (event.key === 'F2') {
-          this.rename();
+        if (event.which === 113) { // if (event.key === 'F2') {
+          if (this.#entry.rename) {
+            this.rename();
+          }
         }
         }
-        else if (event.key === 'Delete') {
-          this.del();
+        else if (event.which === 46) { // else if (event.key === 'Delete') {
+          if (this.#entry.del) {
+            this.del();
+          }
         }
         }
-        else if (event.key === 'Enter' && ! this.#entry.directory) {
+        else if (event.which === 13 && ! this.#entry.directory) { // else if (event.key === 'Enter' && ! this.#entry.directory) {
           if (event.shiftKey) {
           if (event.shiftKey) {
             return this.download();
             return this.download();
           }
           }
@@ -129,11 +143,19 @@ export default class Item extends Element {
     this.loading();
     this.loading();
 
 
     // TODO: i18n
     // TODO: i18n
-    if (! confirm(`Are you sure you want to delete '${entry.name}?'`)) {
+    if (! confirm(`Are you sure you want to delete '${entry.title}?'`)) {
+      return this.loading(false);
+    }
+
+    this.trigger('delete', entry.fullPath, entry);
+  }
+
+  download() {
+    if (this.#entry.directory) {
       return;
       return;
     }
     }
 
 
-    this.trigger('delete', entry.fullPath, entry.path);
+    this.element.querySelector('[download]').click();
   }
   }
 
 
   loading(loading = true) {
   loading(loading = true) {
@@ -144,14 +166,6 @@ export default class Item extends Element {
     this.element.classList.remove('loading');
     this.element.classList.remove('loading');
   }
   }
 
 
-  download() {
-    if (this.#entry.directory) {
-      return;
-    }
-
-    this.element.querySelector('[download]').click();
-  }
-
   open() {
   open() {
     const entry = this.#entry;
     const entry = this.#entry;
 
 
@@ -163,7 +177,7 @@ export default class Item extends Element {
 
 
     const launchLightbox = (lightboxContent, onShow) => {
     const launchLightbox = (lightboxContent, onShow) => {
       const escapeListener = (event) => {
       const escapeListener = (event) => {
-          if (event.key === 'Escape') {
+          if (event.which === 27) { // if (event.key === 'Escape') {
             lightbox.close();
             lightbox.close();
           }
           }
         },
         },
@@ -182,8 +196,7 @@ export default class Item extends Element {
       ;
       ;
 
 
       lightbox.show();
       lightbox.show();
-    }
-    ;
+    };
 
 
     if (['video', 'audio', 'image', 'font'].includes(entry.type)) {
     if (['video', 'audio', 'image', 'font'].includes(entry.type)) {
       this.trigger('check', entry.fullPath, () => {
       this.trigger('check', entry.fullPath, () => {
@@ -210,8 +223,13 @@ export default class Item extends Element {
   }
   }
 
 
   rename() {
   rename() {
-    const entry = this.#entry,
-      node = this.element,
+    const entry = this.#entry;
+
+    if (! entry.rename) {
+      throw new TypeError(`'${entry.name}' cannot be renamed.`);
+    }
+
+    const node = this.element,
       title = node.querySelector('.title'),
       title = node.querySelector('.title'),
       input = node.querySelector('input'),
       input = node.querySelector('input'),
       setInputSize = () => {
       setInputSize = () => {
@@ -231,7 +249,7 @@ export default class Item extends Element {
 
 
           unbindListeners();
           unbindListeners();
 
 
-          return this.trigger('move', entry.fullPath, entry.path + input.value);
+          return this.trigger('move', entry.fullPath, joinPath(entry.path, input.value), entry);
         }
         }
 
 
         revert();
         revert();
@@ -254,13 +272,13 @@ export default class Item extends Element {
         save();
         save();
       },
       },
       keyDownListener = (event) => {
       keyDownListener = (event) => {
-        if (event.key === 'Enter') {
+        if (event.which === 13) { // if (event.key === 'Enter') {
           event.stopPropagation();
           event.stopPropagation();
           event.preventDefault();
           event.preventDefault();
 
 
           save();
           save();
         }
         }
-        else if (event.key === 'Escape') {
+        else if (event.which === 27) { // else if (event.key === 'Escape') {
           revert();
           revert();
         }
         }
       },
       },

+ 10 - 0
src/lib/UI/supportsFocusWithin.js

@@ -0,0 +1,10 @@
+export default (() => {
+  try {
+    document.querySelector(':focus-within');
+
+    return true;
+  }
+  catch (e) {
+    return false;
+  }
+})();

+ 7 - 0
src/lib/joinPath.js

@@ -0,0 +1,7 @@
+export const trimSlashes = (piece) => piece.replace(/^\/+|\/+$/g, '');
+
+export const joinPath = (...pieces) => {
+  return `/${pieces.map(trimSlashes).filter((piece) => piece).join('/')}`;
+};
+
+export default joinPath;

文件差異過大導致無法顯示
+ 0 - 0
src/webdav-min.js


+ 1 - 1
src/webdav.js

@@ -2,7 +2,7 @@ import 'basiclightbox/src/styles/main.scss';
 import 'prismjs/themes/prism.css';
 import 'prismjs/themes/prism.css';
 import 'melba-toast/dist/css/Melba.css';
 import 'melba-toast/dist/css/Melba.css';
 import 'webdav-js/assets/scss/style.scss';
 import 'webdav-js/assets/scss/style.scss';
-
+import 'whatwg-fetch'; // IE11 compatibility
 import NativeDOM from './lib/UI/NativeDOM.js';
 import NativeDOM from './lib/UI/NativeDOM.js';
 
 
 const ui = new NativeDOM(document.body);
 const ui = new NativeDOM(document.body);

+ 29 - 9
yarn.lock

@@ -597,6 +597,16 @@
   dependencies:
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
     "@babel/helper-plugin-utils" "^7.0.0"
 
 
+"@babel/plugin-transform-runtime@^7.6.2":
+  version "7.6.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.6.2.tgz#2669f67c1fae0ae8d8bf696e4263ad52cb98b6f8"
+  integrity sha512-cqULw/QB4yl73cS5Y0TZlQSjDvNkzDbu0FurTZyHlJpWE5T3PCMdnyV+xXoH1opr1ldyHODe3QAX3OMAii5NxA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@babel/helper-plugin-utils" "^7.0.0"
+    resolve "^1.8.1"
+    semver "^5.5.1"
+
 "@babel/plugin-transform-shorthand-properties@^7.2.0":
 "@babel/plugin-transform-shorthand-properties@^7.2.0":
   version "7.2.0"
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
@@ -699,10 +709,10 @@
     js-levenshtein "^1.1.3"
     js-levenshtein "^1.1.3"
     semver "^5.5.0"
     semver "^5.5.0"
 
 
-"@babel/runtime@^7.6.3":
-  version "7.6.3"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f"
-  integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==
+"@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2":
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
+  integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.2"
     regenerator-runtime "^0.13.2"
 
 
@@ -2281,6 +2291,11 @@ core-js-compat@^3.1.1:
     browserslist "^4.6.6"
     browserslist "^4.6.6"
     semver "^6.3.0"
     semver "^6.3.0"
 
 
+core-js@3:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.1.tgz#76dd6828412900ab27c8ce0b22e6114d7ce21b18"
+  integrity sha512-KX/dnuY/J8FtEwbnrzmAjUYgLqtk+cxM86hfG60LGiW3MmltIc2yAmDgBgEkfm0blZhUrdr1Zd84J2Y14mLxzg==
+
 core-js@^2.4.0, core-js@^2.5.0:
 core-js@^2.4.0, core-js@^2.5.0:
   version "2.6.9"
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
@@ -4770,9 +4785,9 @@ media-typer@0.3.0:
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
 
 melba-toast@^0.2.1:
 melba-toast@^0.2.1:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/melba-toast/-/melba-toast-0.2.2.tgz#a8d73c90c0480849e3bb6817d3d4d4465b76cda0"
-  integrity sha512-PFynsE8OcwCedPW5JBu3euI2UYABt9m+ocxJqgRNAmA4yJwJfruU/xjbT9aCT8jHesArfwspw3d45Lv2zPCvhw==
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/melba-toast/-/melba-toast-0.2.4.tgz#0e9a6dee8a8050cdedf68836ec830fbb9917d24c"
+  integrity sha512-DZpOyBkN7UMAb1oDzzzuElLYfzVyWed/avxAgj0/sgc26TwDeLLIPrIMjbVW+vsZZElhPvPvxXT+oIUfI8/obw==
   dependencies:
   dependencies:
     "@babel/runtime" "^7.6.3"
     "@babel/runtime" "^7.6.3"
 
 
@@ -6647,7 +6662,7 @@ resolve-url@^0.2.1:
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
 
-resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2:
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.8.1:
   version "1.12.0"
   version "1.12.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
   integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
   integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
@@ -6803,7 +6818,7 @@ select@^1.1.2:
   resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
   resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
   integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
   integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
 
 
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
   version "5.7.1"
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -8075,6 +8090,11 @@ webpack@^4.41.0:
     watchpack "^1.6.0"
     watchpack "^1.6.0"
     webpack-sources "^1.4.1"
     webpack-sources "^1.4.1"
 
 
+whatwg-fetch@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
 which-module@^1.0.0:
 which-module@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"

部分文件因文件數量過多而無法顯示