Forráskód Böngészése

Increased the size of the item buttons.
Add a `Tree` component to enable copy and move operations again. Fixes #111.
Combine path-related functions.
Ensure `HTTP` operations always return a response.
Minor changes to adhere to WebDAV spec.

dom111 2 éve
szülő
commit
7444915b04

+ 3 - 1
TODO.md

@@ -4,9 +4,11 @@
 - [x] Support keyboard navigation whilst overlay is visible
 - [x] Maybe a refactor...
 - [x] Add eventMap to `Event` object. - Replaced with `typed-event-emitter`
+- [x] Add functionality for copying and moving files and directories
 - [ ] 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
 - [ ] ReactJS implementation
 - [ ] VueJS implementation
+- [ ] Add selection checkboxes and bulk operations
+- [ ] Break `Item` down further

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
assets/css/style-min.css


+ 162 - 77
assets/css/style.css

@@ -340,105 +340,195 @@ main ul {
   margin: 0;
   padding: 0 5px;
 }
-main ul li {
+.upload {
+  border: 1px solid #eee;
+  border-radius: 5px;
+  color: #999;
+  font-size: 1.5em;
+  font-weight: 700;
+  margin: 0 20px;
+  padding: 10px 0;
+  text-align: center;
+  transition: 0.5s;
+}
+.upload .create-directory {
+  color: #22a;
+  font-size: inherit;
+  text-decoration: underline;
+}
+.is-touch .upload .droppable,
+.no-drag-drop .upload .droppable {
+  display: none;
+}
+.upload [type='file'] {
+  max-width: 100%;
+}
+.basicLightbox .basicLightbox__placeholder {
+  max-height: 95vh;
+  max-width: 95vw;
+  overflow: auto;
+  padding: 0 1em;
+}
+.basicLightbox.font .basicLightbox__placeholder,
+.basicLightbox.text .basicLightbox__placeholder {
+  background: #fff;
+}
+body:not([data-disable-checkerboard]) .basicLightbox.image img {
+  pointer-events: all;
+}
+body:not([data-disable-checkerboard]) .basicLightbox.image img:hover {
+  background: #eee
+    url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill-opacity=".25" ><rect x="200" width="200" height="200" /><rect y="200" width="200" height="200" /></svg>');
+  background-size: 30px 30px;
+}
+.tree {
+  padding: 0.5rem;
+}
+.leaf {
+  cursor: pointer;
+  margin: 0.1rem 0;
+  padding-left: 1rem;
+}
+.leaf .name {
+  border: 1px dotted transparent;
+  display: inline-block;
+  padding: 0.1rem;
+}
+.leaf > .toggle {
+  display: inline-block;
+  text-align: center;
+  width: 1rem;
+}
+.leaf[aria-selected='true'] > .name {
+  background-color: #0000c033;
+  border-color: #0009;
+  font-weight: 700;
+}
+.leaf:not(.hasChildren):not(.hasNoChildren) > .toggle:before {
+  content: '\22a1';
+}
+.leaf.hasChildren[aria-expanded='false'] > .children {
+  display: none;
+}
+.leaf.hasChildren[aria-expanded='false'] > .toggle:before {
+  content: '\229e';
+}
+.leaf.hasChildren[aria-expanded='true'] > .toggle:before {
+  content: '\229f';
+}
+.tree .leaf .name {
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC);
+  background-position: left center;
+  background-repeat: no-repeat;
+  padding-left: 1.2rem;
+}
+main > ul > li {
+  align-items: center;
   background: none no-repeat left center;
   border-top: 1px solid #eee;
   cursor: pointer;
-  display: block;
+  display: flex;
+  flex-direction: row;
+  height: 2rem;
+  justify-content: start;
+  list-style: none;
   overflow: hidden;
-  padding: 5px 0 5px 5px;
 }
-main ul li:hover {
+main > ul > li .title {
+  padding-left: 0.5rem;
+}
+main > ul > li:hover {
   background-color: #fafafa;
 }
-main ul li:first-child {
+main > ul > li:first-child {
   border-top: 0;
 }
-main ul li.active {
+main > ul > li.active {
   color: #000;
 }
-main ul li.loading {
+main > ul > li.loading {
   background-size: contain;
 }
-main ul li.loading * {
+main > ul > li.loading * {
   pointer-events: none;
 }
-main ul li .size {
+main > ul > li .size {
   color: #aaa;
   display: inline-block;
-  margin: 0 10px;
-}
-main ul li .copy,
-main ul li .move,
-main ul li .rename,
-main ul li .delete,
-main ul li [download] {
+  margin: 0 1rem;
+  flex: 1 1;
+}
+main > ul > li .copy,
+main > ul > li .move,
+main > ul > li .rename,
+main > ul > li .delete,
+main > ul > li [download] {
   background: none no-repeat center center;
   float: right;
   height: 16px;
-  margin: 0 5px;
   overflow: hidden;
+  padding: 0.4rem;
   text-indent: 26px;
   white-space: nowrap;
   width: 16px;
 }
-main ul li .copy {
+main > ul > li .copy {
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ1SURBVBgZBcFNiJVVGADg5z3fmTujY5NZBANBIVkZ2J9IKkZFGKRuglq1KaqVtoqoVbSKFi1LoY2QEVSbcqiFWWJqPxL044wUGESQVqJOKerce7/z9jyRmba++tXTy2YmnyphPaYQIJBBNuPWfls8l1/EfxdeOrJnxxAgMtO2148d2ffC+rWlxMqkkwBkQjp7aeT97xf99cfS5ZPzv6w6umfHElQoXdw+qN3KhX90JYIgG30243G6Muo9tOYa999WfdfOLs92x4UHd3163eG3ti8ViIgVmdkNumKiUIOu0AURFIFmdmZgx4ZZt9w6uazOTO+FAklAQQlKhBKhRCgRShfOnL/i5hUjd64Kz2+6XjfRPQkVIJPaEUJGaH1SQu0YZHHqXBq2sdaGHlg9KWoZQ4VMEjWKlBJRQiAb2RUGlBZa66RCFFAh0RBBCIlENiY6QBTRhyypIROo0MZk0hDITFAKWqhdkkGSQt/oG1ChtZSZJCkBSCCEE79+Yv7UnIuXLxiNR8rwnsomFfpGn2SjAUjQkuPzHzp98XMPb9ngplVrHFr42OX5ubpx1943K7Rxaple+2EopBZkBo2MNL3wnie2P6ovvbtntzp48iMb1232+6n9OyuMx72+Z3Zmwn03Fi3pkz5oyWffnjERKzy29lnw4iPvmDuxG/unKoyXWhu3lsNefPNnr0VKAVpy/tK/Fk5/7afTR72yda83DjxjqpuEqxVGV/u/pwfdDS+vG05nZpE0wLXLqn2Lzzn287s237XF3IndBlEd/fEwvB2ZacPOgzvHo3w8Iu5NuRxAkkhpovug1u5Q5SoGfWurDxzf/eW2/wEnITFm/fHryQAAAABJRU5ErkJggg==);
 }
-main ul li .move {
+main > ul > li .move {
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ2SURBVBgZBcFLiJVlGADg5/3+b87cbLyFNBJ4oexGQYqIi6hFQambgohoE0aLUqGCaBcuonWLUFe1CIJolWCLaiK1C0FUREpRBgmWNpqi4XjOnP97e57ITI+8fuLZ6bnJZ0rYhikECGSQzbi1M1cu5UJcvfzqycN7RgCRmXa9+dXJ9w5su6uUWJV0EoBMSIv/LXv/uyvOnx1eP/3zL2u+PLxnCBVKF3cMarfq1D+6EkGQjT6b8TgtLfceuv0mO7ZU37bFmWx3Xn5w/7HVx9/ePSwQESsysxt0xUShBl2hCyIoAs383MCe7fM23jY5Xedm34UCSUBBCUqEEqFEKBFKF/7+d8mGFcvuXhOe37lWN9E9CRUgk9oRQkZofVJC7Rhk8fulNGpjrY08sHlS1DKGCpkkahQpJaKEQDayKwwoLbTWSYUooEKiIYIQEolsTHSAKKIPWVJDJlChjcmkIZCZoBS0ULskgySFvtE3oEJrKTNJUgKQQAj950eMFg5ZPvebU+vW2zH9WGWnCn2jT7LRACRoyY2FI6ZOfeC+p54zuekeSz99YubkQv304YkDFdo4tUwHfxgJqQWZQSMjPX30Lbv3vmDqzBeceMPMylU2b9jg+1/z5Qrjca/vmZ+bsHVd0ZI+6YOWrL7yp6lbNrHrFQD14LyuxcYK42Fr49Zy1ItvzvVapBSgJetXzrv+4zGzR180XDrvOq5d7fSdvyos3+gvzA66m1+7dzSbmUXSACunq4vn9zt9/B23rp5WuwnXFsf+uNBJ/aHITNv3fbZvvJyPR8T9KWcAJImUHh0eq1sXP+zWDi/G1cHc8Oxgy8cvffT1E/8D2iAtJW5RUGAAAAAASUVORK5CYII=);
 }
-main ul li .rename {
+main > ul > li .rename {
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAFUSURBVDjLrZM/SAJxGIZdWwuDlnCplkAEm1zkaIiGFFpyMIwGK5KGoK2lphDKkMDg3LLUSIJsSKhIi+684CokOtTiMizCGuzEU5K3vOEgKvtBDe/2Pc8H3x8NAM1fQlx4H9M3pcOWp6TXWmM8A7j0629v1nraiAVC0IrrwATKIgs5xyG5QiE+Z4iQdoeU2oAsnqCSO1NSTu+D9VhqRLD8nIB8F0Q2MgmJDyipCzjvYJkIfpN2UBLG8MpP4dxvQ3ZzGuyyBQ2H+AnOOCBd9aL6soh81A5hyYSGWyCFvxUcerqI4S+CvYVOFPMHxLAq8I3qdHVY5LbBhJzEsCrwutpRFBlUHy6wO2tEYtWAzLELPN2P03kjfj3luqDycV2F8AgefWbEnVqEHa2IznSD6BdsVDNStB0lfh0FPoQjdx8RrAqGzC0YprSgxzsUMOY2bf37N/6Ud1Vc9yYcH50CAAAAAElFTkSuQmCC);
 }
-main ul li .delete {
+main > ul > li .delete {
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==);
 }
-main ul li [download] {
+main > ul > li [download] {
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKYSURBVBgZBcFNiFZlGADQ87z3zjfjzDiMk5VimERmBmFpKgVRVATRQLRo5aaNhFA7pZW0qV1Ci0BoUVRQtLc2UmQaJVhBVChUhP2YDuow5sz3c7/36Zx46uipF9fNTR4oYQ82oC+QQVZdrb8tX83PY+X6kdPHF4cAAPHMG2dOf/jKnp2lxHymBiAT0tJ/Ix+dW/bvxcHqLz+dXzhzfHEAAG1pYkevbeZ/vqIpJWRlnFXXpbXR2OPb19t/T+tsXZrOeu/1x17+dMOpt58dAJSImM3MptcUbdAUmiCCIlBtnutZ3LvZtrsn17VzM+8BQEkCCkqEEqFEKBFKEy5dW3Pn7Mh9C+Hgw7doJpoXAKCFTNoGEeo4KaFt6GXx69U0rJ1ahx69a1K0pQOANpNEG0WVooTA+ycuWD/J6rCzYbbnnytrPivs2jQ7dfidH/P6ypovfvh7f5uoiKAIiaysrfY9sW+H53bPOvHtX44e2AkAPvhySa1xttSOTCoyE6kUVgcj12504OS5ywD6HSsDlpaHIqq21pzKTBIASSmhSUbj6tih3WpNiUaabhkMBlZuDrWjYfZHNade+24oI2UGlYWJoj8Yef2TP5SgG1d1XCUyk2R6omq7bmw8ZvPchAduLcbJODh7IQ3GnaZMIIWxiJA1ZVZdNzIcdtpuUGtXaw7H4ptLnRTgxurIRLQOLa7X1SEKSUY11fa8+fFFV1duakf98eWZXrPx1fuHM1mzVMCRU6kxoVrz7vcv2Ti9VSiWVv90cO9biimrg6E2Io89ffir5yPiwZQL6Eu2b52d2raJ22dveHLXPvO9LRSW+1vcNtOZ7tFGiMwEAAAPHTr59fY71j0yPzlvqp3T1ZGoVeRYjVXnL/7uysrI/62cRssvlMuuAAAAAElFTkSuQmCC);
 }
-main ul li input {
+main > ul > li input {
   border: 0;
+  margin-left: 0.5rem;
   padding: 0;
   font-size: 1rem;
 }
-main ul li.directory:before {
+main > ul > li.directory:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC);
 }
-main ul li.directory .size,
-main ul li.directory [download] {
+main > ul > li.directory [download] {
   display: none;
 }
-main ul li.file:before {
+main > ul > li.file:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC4SURBVCjPdZFbDsIgEEWnrsMm7oGGfZrohxvU+Iq1TyjU60Bf1pac4Yc5YS4ZAtGWBMk/drQBOVwJlZrWYkLhsB8UV9K0BUrPGy9cWbng2CtEEUmLGppPjRwpbixUKHBiZRS0p+ZGhvs4irNEvWD8heHpbsyDXznPhYFOyTjJc13olIqzZCHBouE0FRMUjA+s1gTjaRgVFpqRwC8mfoXPPEVPS7LbRaJL2y7bOifRCTEli3U7BMWgLzKlW/CuebZPAAAAAElFTkSuQmCC);
 }
-main ul li.file.image:before {
+main > ul > li.file.image:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIcSURBVDjLjZO/T1NhFIafc+/trdRaYk1KUEEWjXHRaCSik+E/cDHGzYXB2YHRhMRFY1SYmRgYHZ3VxIRFDYMraMC2hrbQXm7v9+M4UGobiOEk7/adN+9zvnNEVQEQkYvAGBDy/6oBm6rqAVBVeia30jRtGmOctVaPU5qmuri4+AaYAgJVHTKYNsa4drutnU6nr1arpY1GQ6vVqlprdXt7W5eWlvomMv/uw6tSofB4p+NOF0biYtc48tEAhXiuTZzh/s1xyuUyWZbhvWdlZeXt3Nzca14sf6zW6nXf7uzrcfq9s6sLy5+1Xq8fQQKmo1ZCvlAoyo+tXT5tPGO09IckM2zWznH3/AJ3rl5ACInjmGazifceay2VSgWASISSBaz3FIs1RnJlPF18vEG1keDVk1lLFEWICM45wvAfYqTKriqje0lGI01x2qFtuuwkKQ26oEKcCwnDEBFBRA6HfmBw8JWwl3o2ti7j8+u0TUKzcYkrY/n+wyAIEJEjSxEglLyH5r7j+tg8T1oVZr8GzE69JIoiFMiM7zeHYUgQBAMJVBGU77+eYoxhLcvIxnNk6w8xxvDo3hqH+yIieO+HEkQB/qe6bPL5g/cckCkDiBhjOJULhlCGDJIkXX2z+m3GeW4UCnExyxxxHIIOLNLk2WP5AaQXTYDb1tovgHCy8lEUzQS9g1LAO+f2AX+SZudcAjgZOOeJ3jkHJ0zggNpfYEZnU63wHeoAAAAASUVORK5CYII=);
 }
-main ul li.file.py:before,
-main ul li.file.css:before,
-main ul li.file.js:before,
-main ul li.file.xml:before {
+main > ul > li.file.py:before,
+main > ul > li.file.css:before,
+main > ul > li.file.js:before,
+main > ul > li.file.xml:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHtSURBVDjLjZM9T9tQFIYpQ5eOMBKlW6eWIQipa8RfQKQghEAKqZgKFQgmFn5AWyVDCipVQZC2EqBWlEqdO2RCpAssQBRsx1+1ndix8wFvfW6wcUhQsfTI0j33PD7n+N4uAF2E+/S5RFwG/8Njl24/LyCIOI6j1+v1y0ajgU64cSSTybdBSVAwSMmmacKyLB/DMKBpGkRRZBJBEJBKpXyJl/yABLTBtm1Uq1X2JsrlMnRdhyRJTFCpVEAfSafTTUlQoFs1luxBAkoolUqQZbmtJTYTT/AoHInOfpcwtVtkwcSBgrkDGYph+60oisIq4Xm+VfB0+U/P0Lvj3NwPGfHPTcHMvoyFXwpe7UmQtAqTUCU0D1VVbwTPVk5jY19Fe3ZfQny7CE51WJDXqpjeEUHr45ki9rIqa4dmQiJfMLItGEs/FcQ2ucbRmdnSYy5vYWyLx/w3EaMfLmBaDpMQvuDJ65PY8Dpnz3wpYmLtApzcrIAqmfrEgdZH1grY/a36w6Xz0DKD8ES25/niYS6+wWE8mWfByY8cXmYEJFYLkHUHtVqNQcltAvoLD3v7o/FUHsNvzlnwxfsCEukC/ho3yUHaBN5Buo17Ojtyl+DqrnvQgUtfcC0ZcAdkUeA+ye7eMru9AUGIJPe4zh509UP/AAfNypi8oj/mAAAAAElFTkSuQmCC);
 }
-main ul li.file.log:before,
-main ul li.file.txt:before,
-main ul li.file.nfo:before {
+main > ul > li.file.log:before,
+main > ul > li.file.txt:before,
+main > ul > li.file.nfo:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADoSURBVBgZBcExblNBGAbA2ceegTRBuIKOgiihSZNTcC5LUHAihNJR0kGKCDcYJY6D3/77MdOinTvzAgCw8ysThIvn/VojIyMjIyPP+bS1sUQIV2s95pBDDvmbP/mdkft83tpYguZq5Jh/OeaYh+yzy8hTHvNlaxNNczm+la9OTlar1UdA/+C2A4trRCnD3jS8BB1obq2Gk6GU6QbQAS4BUaYSQAf4bhhKKTFdAzrAOwAxEUAH+KEM01SY3gM6wBsEAQB0gJ+maZoC3gI6iPYaAIBJsiRmHU0AALOeFC3aK2cWAACUXe7+AwO0lc9eTHYTAAAAAElFTkSuQmCC);
 }
-main ul li.file.rb:before {
+main > ul > li.file.rb:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIESURBVDjLjZNPTxNBGIexid9CEr8DBr8CHEiMVoomJiQkxBIM3dgIiaIESJTGGpVtyXIzHhoM4SIe9KAnEi4clQtJEczWFrbdP93d7s7u/JwZ7XYJBdnkyRxmfs/MvO9OD4AeDvuuMPoY/f/hKiMR5WKCvlarpRNCwiAI0A02D1mW38QlcUE/DzebTdi2HWEYBhqNBqrVqpBUKhUUCoVI0g5f4gK+wHVdeJ4nRo5lWdB1HbVaTQgcxwHfRFGUvxIuCKYfzmqZyZ2wKIO8fQ3/1Uv4Sy/QWliAO/sU9qMZmFMS3HfvT1xJ1ITOZJ9RpQi6+RH0y2fQb19BP23CVhRo+TysXA71+XkcMIk6fAfHK6tQVfWEoESXngNra0C5DHZJYGMDZiaD35IEi41qOo3vc3MoJ1Ooj92HpmkdQZiVEsHUAzl88hjY3gYIAdbXYQ0MoDo4CH1kBHssvH8jCf3eGKzDXzBNsyNoF/HH7WSJZLPA7i6wtQVnaAhmKoXjxUX8vDkMY3Qcnm6IInJOCS4nEte9QhF+RhInIRMTcFhYvZWCcXcUPmsl7w6H/w+nBFEb5SLc8TTo8jLq7M4m25mHfd8X8PC5AtHrXB5NdmwRrnfCcc4VCEnpA8jREasp6cpZAnrWO+hCGAn+Sa6xAtl84iJhttYSrzcm6OWSCzznNvzp9/4BgwKvG3Zq1eoAAAAASUVORK5CYII=);
 }
-main ul li.file.sql:before {
+main > ul > li.file.sql:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHVSURBVDjLjZPLaiJBFIZNHmJWCeQdMuT1Mi/gYlARBRUkao+abHUhmhgU0QHtARVxJ0bxhvfGa07Of5Iu21yYFPyLrqrz1f+f6rIRkQ3icca6ZF39RxesU1VnAVyuVqvJdrvd73Y7+ky8Tk6n87cVYgVcoXixWNByuVSaTqc0Ho+p1+sJpNvtksvlUhCb3W7/cf/w+BSLxfapVIqSySRlMhnSdZ2GwyHN53OaTCbU7/cFYBgG4RCPx/MKub27+1ur1Xqj0YjW6zWxCyloNBqUSCSkYDab0WAw+BBJeqLFtQpvGoFqAlAEaZomuc0ocAQnnU7nALiJ3uh8whgnttttarVaVCgUpCAUCgnQhMAJ+gG3CsDZa7xh1mw2ZbFSqYgwgsGgbDQhcIWeAHSIoP1pcGeNarUqgFKpJMLw+/0q72azkYhmPAWIRmM6AGbXc7kc5fN5AXi9XgWACwAguLEAojrfsVGv1yV/sVikcrksAIfDIYUQHEAoPgLwT3GdzWYNdBfXh3xwApDP5zsqtkoBwuHwaSAQ+OV2u//F43GKRCLEc5ROpwVoOngvBXj7jU/wwZPPX72DT7RXgDfIT27QEgvfKea9c3m9FsA5IN94zqbw9M9fAEuW+zzj8uLvAAAAAElFTkSuQmCC);
 }
-main ul li.file.html:before {
+main > ul > li.file.html:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJwSURBVDjLjZPdT1JhHMetvyO3/gfLKy+68bLV2qIAq7UyG6IrdRPL5hs2U5FR0MJIAqZlh7BVViI1kkyyiPkCyUtztQYTYbwJE8W+Pc8pjofK1dk+OxfP+X3O83srAVBCIc8eQhmh/B/sJezm4niCsvX19cTm5uZWPp/H3yDnUKvVKr6ELyinwWtra8hkMhzJZBLxeBwrKyusJBwOQ6PRcJJC8K4DJ/dXM04DOswNqNOLybsRo9N6LCy7kUgkEIlEWEE2mwX9iVar/Smhglqd8IREKwya3qhg809gPLgI/XsrOp/IcXVMhqnFSayurv6RElsT6ZCoov5u1fzUVwvcKRdefVuEKRCA3OFHv2MOxtlBdFuaMf/ZhWg0yt4kFAoVCZS3Hd1gkpOwRt9h0LOES3YvamzPcdF7A6rlPrSbpbhP0kmlUmw9YrHYtoDku2T6pEZ/2ICXEQ8kTz+g2TkNceAKKv2nIHachn6qBx1MI5t/Op1mRXzBd31AiRafBp1vZyEcceGCzQ6p24yjEzocGT6LUacS0iExcrkcK6Fsp6AXLRnmFOjyPMIZixPHmAAOGxZQec2OQyo7zpm6cNN6GZ2kK1RAofPAr8GA4oUMrdNNkIw/wPFhDwSjX3Dwlg0CQy96HreiTlcFZsaAjY0NNvh3QUXtHeHcoKMNA7NjqLd8xHmzDzXDRvRO1KHtngTyhzL4SHeooAAnKMxBtUYQbGWa0Dc+AsWzSVy3qkjeItLCFsz4XoNMaRFFAm4SyTXbmQa2YHQSGacR/pAXO+zGFif4JdlHCpShBzstEz+YfJtmt5cnKKWS/1jnAnT1S38AGTynUFUTzJcAAAAASUVORK5CYII=);
 }
-main ul li.file.php:before {
+main > ul > li.file.php:before {
   content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGsSURBVDjLjZNLSwJRFICtFv2AgggS2vQLDFvVpn0Pi4iItm1KItvWJqW1pYsRemyyNILARbZpm0WtrJ0kbmbUlHmr4+t0z60Z7oSSAx935txzvrlPBwA4EPKMEVwE9z+ME/qtOkbgqtVqUqPRaDWbTegE6YdQKBRkJazAjcWapoGu6xayLIMoilAoFKhEEAQIh8OWxCzuQwEmVKtVMAyDtoiqqiBJEhSLRSqoVCqAP+E47keCAvfU5sDQ8MRs/OYNtr1x2PXdwuJShLLljcFlNAW5HA9khLYp0TUhSYMLHm7PLEDS7zyw3ybRqyfg+TyBtwl2sDP1nKWFiUSazFex3tk45sXjL1Aul20CGTs+syVY37igBbwg03eMsfH9gwSsrZ+Doig2QZsdNiZmMkVrKmwc18azHKELyQrOMEHTDJp8HXu1hostG8dY8PiRngdWMEq467ZwbDxwlIR8XrQLcBvn5k9Gpmd8fn/gHlZWT20C/D4k8eTDB3yVFKjX6xSbgD1If8G970Q3QbvbPehAyxL8SibJEdaxo5dikqvS28sInCjp4Tqb4NV3fgPirZ4pD4KS4wAAAABJRU5ErkJggg==);
 }
-main ul li .progress {
+main > ul > li .progress {
   border: 1px solid #eee;
   display: inline-block;
   float: left;
@@ -446,55 +536,50 @@ main ul li .progress {
   margin: 2px 0 2px 2px;
   width: 100px;
 }
-main ul li .progress .meter {
+main > ul > li .progress .meter {
   background: #0c0;
   display: block;
   height: 7px;
   width: 0;
 }
-main ul li .cancel-upload {
+main > ul > li .cancel-upload {
   color: #900;
   margin: -1px 0 0 5px;
 }
-.upload {
-  border: 1px solid #eee;
-  border-radius: 5px;
-  color: #999;
-  font-size: 1.5em;
-  font-weight: 700;
-  margin: 0 20px;
-  padding: 10px 0;
-  text-align: center;
-  transition: 0.5s;
-}
-.upload .create-directory {
-  color: #22a;
-  font-size: inherit;
-  text-decoration: underline;
-}
-.is-touch .upload .droppable,
-.no-drag-drop .upload .droppable {
-  display: none;
-}
-.upload [type='file'] {
-  max-width: 100%;
+dialog {
+  background: rgba(0, 0, 0, 0.4);
+  border: 0;
+  height: 100vh;
+  justify-content: center;
+  left: 0;
+  position: fixed;
+  top: 0;
+  width: 100vw;
+  z-index: 2;
 }
-.basicLightbox .basicLightbox__placeholder {
-  max-height: 95vh;
-  max-width: 95vw;
-  overflow: auto;
-  padding: 0 1em;
+dialog[open] {
+  display: flex;
 }
-.basicLightbox.font .basicLightbox__placeholder,
-.basicLightbox.text .basicLightbox__placeholder {
+dialog .content {
   background: #fff;
+  border: 0;
+  border-radius: 1rem;
+  box-shadow: #333 0 0 0.25rem;
+  height: fit-content;
+  padding: 1rem;
+  position: fixed;
+  top: 10vh;
+  width: fit-content;
 }
-body:not([data-disable-checkerboard]) .basicLightbox.image img {
-  pointer-events: all;
+.tree-view-modal .tree {
+  max-height: 80vh;
+  min-height: 40vh;
+  overflow-y: scroll;
 }
-body:not([data-disable-checkerboard]) .basicLightbox.image img:hover {
-  background: #eee
-    url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill-opacity=".25" ><rect x="200" width="200" height="200" /><rect y="200" width="200" height="200" /></svg>');
-  background-size: 30px 30px;
+footer {
+  padding-top: 1rem;
+}
+footer button[type='submit'] {
+  float: right;
 }
 /*# sourceMappingURL=webdav.css.map */

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 2 - 3
assets/css/webdav.css.map


+ 14 - 14
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "webdav-js",
-  "version": "2.3.0",
+  "version": "2.4.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "webdav-js",
-      "version": "2.3.0",
+      "version": "2.4.0",
       "license": "MIT",
       "dependencies": {
         "@dom111/element": "^0.1.0",
@@ -2117,9 +2117,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.241",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.241.tgz",
-      "integrity": "sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==",
+      "version": "1.4.242",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.242.tgz",
+      "integrity": "sha512-nPdgMWtjjWGCtreW/2adkrB2jyHjClo9PtVhR6rW+oxa4E4Wom642Tn+5LslHP3XPL5MCpkn5/UEY60EXylNeQ==",
       "dev": true
     },
     "node_modules/emittery": {
@@ -5521,9 +5521,9 @@
       }
     },
     "node_modules/supports-hyperlinks": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
-      "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+      "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
       "dev": true,
       "dependencies": {
         "has-flag": "^4.0.0",
@@ -7860,9 +7860,9 @@
       }
     },
     "electron-to-chromium": {
-      "version": "1.4.241",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.241.tgz",
-      "integrity": "sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==",
+      "version": "1.4.242",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.242.tgz",
+      "integrity": "sha512-nPdgMWtjjWGCtreW/2adkrB2jyHjClo9PtVhR6rW+oxa4E4Wom642Tn+5LslHP3XPL5MCpkn5/UEY60EXylNeQ==",
       "dev": true
     },
     "emittery": {
@@ -10320,9 +10320,9 @@
       }
     },
     "supports-hyperlinks": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
-      "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+      "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
       "dev": true,
       "requires": {
         "has-flag": "^4.0.0",

+ 2 - 2
package.json

@@ -1,7 +1,7 @@
 {
   "name": "webdav-js",
-  "version": "2.3.0",
-  "description": "WebDAV functionality intended for use as a bookmarklet or to make a simple Apache webserver an interactive WebDAV environment.",
+  "version": "2.4.0",
+  "description": "WebDAV functionality intended for use as a bookmarklet or to make a simple webserver an interactive WebDAV environment.",
   "repository": {
     "type": "git",
     "url": "git@github.com:dom111/webdav-js.git"

+ 3 - 4
src/components/Footer.ts

@@ -1,12 +1,11 @@
 import Element, { on, s } from '@dom111/element';
+import joinPath, { trailingSlash } from '../lib/joinPath';
 import DAV from '../lib/DAV';
 import Entry from '../lib/Entry';
 import State from '../lib/State';
 import handleFileUpload from '../lib/handleFileUpload';
-import joinPath from '../lib/joinPath';
 import { success } from 'melba-toast';
 import { t } from 'i18next';
-import trailingSlash from '../lib/trailingSlash';
 
 export default class Footer extends Element {
   #dav: DAV;
@@ -66,9 +65,9 @@ export default class Footer extends Element {
   }
 
   async handleCreateDirectory(fullPath: string, directoryName: string) {
-    const result = await this.#dav.mkcol(fullPath);
+    const result = await this.#dav.createDirectory(fullPath);
 
-    if (!result) {
+    if (!result.ok) {
       return;
     }
 

+ 169 - 0
src/components/Item.scss

@@ -0,0 +1,169 @@
+main > ul > li {
+  align-items: center;
+  background: none no-repeat left center;
+  border-top: 1px solid #eee;
+  cursor: pointer;
+  display: flex;
+  flex-direction: row;
+  height: 2rem;
+  justify-content: start;
+  list-style: none;
+  overflow: hidden;
+
+  .title {
+    padding-left: 0.5rem;
+  }
+
+  &:hover {
+    background-color: #fafafa;
+  }
+
+  &:first-child {
+    border-top: 0;
+  }
+
+  &.active {
+    color: #000;
+  }
+
+  &.loading {
+    background-size: contain;
+
+    * {
+      pointer-events: none;
+    }
+  }
+
+  .size {
+    color: #aaa;
+    display: inline-block;
+    margin: 0 1rem;
+    flex: 1 1;
+  }
+
+  .copy,
+  .move,
+  .rename,
+  .delete,
+  [download] {
+    background: none no-repeat center center;
+    float: right;
+    height: 16px;
+    overflow: hidden;
+    padding: 0.4rem;
+    text-indent: 26px;
+    white-space: nowrap;
+    width: 16px;
+  }
+
+  .copy {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ1SURBVBgZBcFNiJVVGADg5z3fmTujY5NZBANBIVkZ2J9IKkZFGKRuglq1KaqVtoqoVbSKFi1LoY2QEVSbcqiFWWJqPxL044wUGESQVqJOKerce7/z9jyRmba++tXTy2YmnyphPaYQIJBBNuPWfls8l1/EfxdeOrJnxxAgMtO2148d2ffC+rWlxMqkkwBkQjp7aeT97xf99cfS5ZPzv6w6umfHElQoXdw+qN3KhX90JYIgG30243G6Muo9tOYa999WfdfOLs92x4UHd3163eG3ti8ViIgVmdkNumKiUIOu0AURFIFmdmZgx4ZZt9w6uazOTO+FAklAQQlKhBKhRCgRShfOnL/i5hUjd64Kz2+6XjfRPQkVIJPaEUJGaH1SQu0YZHHqXBq2sdaGHlg9KWoZQ4VMEjWKlBJRQiAb2RUGlBZa66RCFFAh0RBBCIlENiY6QBTRhyypIROo0MZk0hDITFAKWqhdkkGSQt/oG1ChtZSZJCkBSCCEE79+Yv7UnIuXLxiNR8rwnsomFfpGn2SjAUjQkuPzHzp98XMPb9ngplVrHFr42OX5ubpx1943K7Rxaple+2EopBZkBo2MNL3wnie2P6ovvbtntzp48iMb1232+6n9OyuMx72+Z3Zmwn03Fi3pkz5oyWffnjERKzy29lnw4iPvmDuxG/unKoyXWhu3lsNefPNnr0VKAVpy/tK/Fk5/7afTR72yda83DjxjqpuEqxVGV/u/pwfdDS+vG05nZpE0wLXLqn2Lzzn287s237XF3IndBlEd/fEwvB2ZacPOgzvHo3w8Iu5NuRxAkkhpovug1u5Q5SoGfWurDxzf/eW2/wEnITFm/fHryQAAAABJRU5ErkJggg==);
+  }
+
+  .move {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ2SURBVBgZBcFLiJVlGADg5/3+b87cbLyFNBJ4oexGQYqIi6hFQambgohoE0aLUqGCaBcuonWLUFe1CIJolWCLaiK1C0FUREpRBgmWNpqi4XjOnP97e57ITI+8fuLZ6bnJZ0rYhikECGSQzbi1M1cu5UJcvfzqycN7RgCRmXa9+dXJ9w5su6uUWJV0EoBMSIv/LXv/uyvOnx1eP/3zL2u+PLxnCBVKF3cMarfq1D+6EkGQjT6b8TgtLfceuv0mO7ZU37bFmWx3Xn5w/7HVx9/ePSwQESsysxt0xUShBl2hCyIoAs383MCe7fM23jY5Xedm34UCSUBBCUqEEqFEKBFKF/7+d8mGFcvuXhOe37lWN9E9CRUgk9oRQkZofVJC7Rhk8fulNGpjrY08sHlS1DKGCpkkahQpJaKEQDayKwwoLbTWSYUooEKiIYIQEolsTHSAKKIPWVJDJlChjcmkIZCZoBS0ULskgySFvtE3oEJrKTNJUgKQQAj950eMFg5ZPvebU+vW2zH9WGWnCn2jT7LRACRoyY2FI6ZOfeC+p54zuekeSz99YubkQv304YkDFdo4tUwHfxgJqQWZQSMjPX30Lbv3vmDqzBeceMPMylU2b9jg+1/z5Qrjca/vmZ+bsHVd0ZI+6YOWrL7yp6lbNrHrFQD14LyuxcYK42Fr49Zy1ItvzvVapBSgJetXzrv+4zGzR180XDrvOq5d7fSdvyos3+gvzA66m1+7dzSbmUXSACunq4vn9zt9/B23rp5WuwnXFsf+uNBJ/aHITNv3fbZvvJyPR8T9KWcAJImUHh0eq1sXP+zWDi/G1cHc8Oxgy8cvffT1E/8D2iAtJW5RUGAAAAAASUVORK5CYII=);
+  }
+
+  .rename {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAFUSURBVDjLrZM/SAJxGIZdWwuDlnCplkAEm1zkaIiGFFpyMIwGK5KGoK2lphDKkMDg3LLUSIJsSKhIi+684CokOtTiMizCGuzEU5K3vOEgKvtBDe/2Pc8H3x8NAM1fQlx4H9M3pcOWp6TXWmM8A7j0629v1nraiAVC0IrrwATKIgs5xyG5QiE+Z4iQdoeU2oAsnqCSO1NSTu+D9VhqRLD8nIB8F0Q2MgmJDyipCzjvYJkIfpN2UBLG8MpP4dxvQ3ZzGuyyBQ2H+AnOOCBd9aL6soh81A5hyYSGWyCFvxUcerqI4S+CvYVOFPMHxLAq8I3qdHVY5LbBhJzEsCrwutpRFBlUHy6wO2tEYtWAzLELPN2P03kjfj3luqDycV2F8AgefWbEnVqEHa2IznSD6BdsVDNStB0lfh0FPoQjdx8RrAqGzC0YprSgxzsUMOY2bf37N/6Ud1Vc9yYcH50CAAAAAElFTkSuQmCC);
+  }
+
+  .delete {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==);
+  }
+
+  [download] {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKYSURBVBgZBcFNiFZlGADQ87z3zjfjzDiMk5VimERmBmFpKgVRVATRQLRo5aaNhFA7pZW0qV1Ci0BoUVRQtLc2UmQaJVhBVChUhP2YDuow5sz3c7/36Zx46uipF9fNTR4oYQ82oC+QQVZdrb8tX83PY+X6kdPHF4cAAPHMG2dOf/jKnp2lxHymBiAT0tJ/Ix+dW/bvxcHqLz+dXzhzfHEAAG1pYkevbeZ/vqIpJWRlnFXXpbXR2OPb19t/T+tsXZrOeu/1x17+dMOpt58dAJSImM3MptcUbdAUmiCCIlBtnutZ3LvZtrsn17VzM+8BQEkCCkqEEqFEKBFKEy5dW3Pn7Mh9C+Hgw7doJpoXAKCFTNoGEeo4KaFt6GXx69U0rJ1ahx69a1K0pQOANpNEG0WVooTA+ycuWD/J6rCzYbbnnytrPivs2jQ7dfidH/P6ypovfvh7f5uoiKAIiaysrfY9sW+H53bPOvHtX44e2AkAPvhySa1xttSOTCoyE6kUVgcj12504OS5ywD6HSsDlpaHIqq21pzKTBIASSmhSUbj6tih3WpNiUaabhkMBlZuDrWjYfZHNade+24oI2UGlYWJoj8Yef2TP5SgG1d1XCUyk2R6omq7bmw8ZvPchAduLcbJODh7IQ3GnaZMIIWxiJA1ZVZdNzIcdtpuUGtXaw7H4ptLnRTgxurIRLQOLa7X1SEKSUY11fa8+fFFV1duakf98eWZXrPx1fuHM1mzVMCRU6kxoVrz7vcv2Ti9VSiWVv90cO9biimrg6E2Io89ffir5yPiwZQL6Eu2b52d2raJ22dveHLXPvO9LRSW+1vcNtOZ7tFGiMwEAAAPHTr59fY71j0yPzlvqp3T1ZGoVeRYjVXnL/7uysrI/62cRssvlMuuAAAAAElFTkSuQmCC);
+  }
+
+  input {
+    border: 0;
+    margin-left: 0.5rem;
+    padding: 0;
+    font-size: 1rem;
+  }
+
+  &.directory {
+    &::before {
+      content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC);
+    }
+
+    [download] {
+      display: none;
+    }
+  }
+
+  &.file {
+    &::before {
+      content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC4SURBVCjPdZFbDsIgEEWnrsMm7oGGfZrohxvU+Iq1TyjU60Bf1pac4Yc5YS4ZAtGWBMk/drQBOVwJlZrWYkLhsB8UV9K0BUrPGy9cWbng2CtEEUmLGppPjRwpbixUKHBiZRS0p+ZGhvs4irNEvWD8heHpbsyDXznPhYFOyTjJc13olIqzZCHBouE0FRMUjA+s1gTjaRgVFpqRwC8mfoXPPEVPS7LbRaJL2y7bOifRCTEli3U7BMWgLzKlW/CuebZPAAAAAElFTkSuQmCC);
+    }
+
+    &.image {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIcSURBVDjLjZO/T1NhFIafc+/trdRaYk1KUEEWjXHRaCSik+E/cDHGzYXB2YHRhMRFY1SYmRgYHZ3VxIRFDYMraMC2hrbQXm7v9+M4UGobiOEk7/adN+9zvnNEVQEQkYvAGBDy/6oBm6rqAVBVeia30jRtGmOctVaPU5qmuri4+AaYAgJVHTKYNsa4drutnU6nr1arpY1GQ6vVqlprdXt7W5eWlvomMv/uw6tSofB4p+NOF0biYtc48tEAhXiuTZzh/s1xyuUyWZbhvWdlZeXt3Nzca14sf6zW6nXf7uzrcfq9s6sLy5+1Xq8fQQKmo1ZCvlAoyo+tXT5tPGO09IckM2zWznH3/AJ3rl5ACInjmGazifceay2VSgWASISSBaz3FIs1RnJlPF18vEG1keDVk1lLFEWICM45wvAfYqTKriqje0lGI01x2qFtuuwkKQ26oEKcCwnDEBFBRA6HfmBw8JWwl3o2ti7j8+u0TUKzcYkrY/n+wyAIEJEjSxEglLyH5r7j+tg8T1oVZr8GzE69JIoiFMiM7zeHYUgQBAMJVBGU77+eYoxhLcvIxnNk6w8xxvDo3hqH+yIieO+HEkQB/qe6bPL5g/cckCkDiBhjOJULhlCGDJIkXX2z+m3GeW4UCnExyxxxHIIOLNLk2WP5AaQXTYDb1tovgHCy8lEUzQS9g1LAO+f2AX+SZudcAjgZOOeJ3jkHJ0zggNpfYEZnU63wHeoAAAAASUVORK5CYII=);
+      }
+    }
+
+    &.py,
+    &.css,
+    &.js,
+    &.xml {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHtSURBVDjLjZM9T9tQFIYpQ5eOMBKlW6eWIQipa8RfQKQghEAKqZgKFQgmFn5AWyVDCipVQZC2EqBWlEqdO2RCpAssQBRsx1+1ndix8wFvfW6wcUhQsfTI0j33PD7n+N4uAF2E+/S5RFwG/8Njl24/LyCIOI6j1+v1y0ajgU64cSSTybdBSVAwSMmmacKyLB/DMKBpGkRRZBJBEJBKpXyJl/yABLTBtm1Uq1X2JsrlMnRdhyRJTFCpVEAfSafTTUlQoFs1luxBAkoolUqQZbmtJTYTT/AoHInOfpcwtVtkwcSBgrkDGYph+60oisIq4Xm+VfB0+U/P0Lvj3NwPGfHPTcHMvoyFXwpe7UmQtAqTUCU0D1VVbwTPVk5jY19Fe3ZfQny7CE51WJDXqpjeEUHr45ki9rIqa4dmQiJfMLItGEs/FcQ2ucbRmdnSYy5vYWyLx/w3EaMfLmBaDpMQvuDJ65PY8Dpnz3wpYmLtApzcrIAqmfrEgdZH1grY/a36w6Xz0DKD8ES25/niYS6+wWE8mWfByY8cXmYEJFYLkHUHtVqNQcltAvoLD3v7o/FUHsNvzlnwxfsCEukC/ho3yUHaBN5Buo17Ojtyl+DqrnvQgUtfcC0ZcAdkUeA+ye7eMru9AUGIJPe4zh509UP/AAfNypi8oj/mAAAAAElFTkSuQmCC);
+      }
+    }
+
+    &.log,
+    &.txt,
+    &.nfo {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADoSURBVBgZBcExblNBGAbA2ceegTRBuIKOgiihSZNTcC5LUHAihNJR0kGKCDcYJY6D3/77MdOinTvzAgCw8ysThIvn/VojIyMjIyPP+bS1sUQIV2s95pBDDvmbP/mdkft83tpYguZq5Jh/OeaYh+yzy8hTHvNlaxNNczm+la9OTlar1UdA/+C2A4trRCnD3jS8BB1obq2Gk6GU6QbQAS4BUaYSQAf4bhhKKTFdAzrAOwAxEUAH+KEM01SY3gM6wBsEAQB0gJ+maZoC3gI6iPYaAIBJsiRmHU0AALOeFC3aK2cWAACUXe7+AwO0lc9eTHYTAAAAAElFTkSuQmCC);
+      }
+    }
+
+    &.rb {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIESURBVDjLjZNPTxNBGIexid9CEr8DBr8CHEiMVoomJiQkxBIM3dgIiaIESJTGGpVtyXIzHhoM4SIe9KAnEi4clQtJEczWFrbdP93d7s7u/JwZ7XYJBdnkyRxmfs/MvO9OD4AeDvuuMPoY/f/hKiMR5WKCvlarpRNCwiAI0A02D1mW38QlcUE/DzebTdi2HWEYBhqNBqrVqpBUKhUUCoVI0g5f4gK+wHVdeJ4nRo5lWdB1HbVaTQgcxwHfRFGUvxIuCKYfzmqZyZ2wKIO8fQ3/1Uv4Sy/QWliAO/sU9qMZmFMS3HfvT1xJ1ITOZJ9RpQi6+RH0y2fQb19BP23CVhRo+TysXA71+XkcMIk6fAfHK6tQVfWEoESXngNra0C5DHZJYGMDZiaD35IEi41qOo3vc3MoJ1Ooj92HpmkdQZiVEsHUAzl88hjY3gYIAdbXYQ0MoDo4CH1kBHssvH8jCf3eGKzDXzBNsyNoF/HH7WSJZLPA7i6wtQVnaAhmKoXjxUX8vDkMY3Qcnm6IInJOCS4nEte9QhF+RhInIRMTcFhYvZWCcXcUPmsl7w6H/w+nBFEb5SLc8TTo8jLq7M4m25mHfd8X8PC5AtHrXB5NdmwRrnfCcc4VCEnpA8jREasp6cpZAnrWO+hCGAn+Sa6xAtl84iJhttYSrzcm6OWSCzznNvzp9/4BgwKvG3Zq1eoAAAAASUVORK5CYII=);
+      }
+    }
+
+    &.sql {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHVSURBVDjLjZPLaiJBFIZNHmJWCeQdMuT1Mi/gYlARBRUkao+abHUhmhgU0QHtARVxJ0bxhvfGa07Of5Iu21yYFPyLrqrz1f+f6rIRkQ3icca6ZF39RxesU1VnAVyuVqvJdrvd73Y7+ky8Tk6n87cVYgVcoXixWNByuVSaTqc0Ho+p1+sJpNvtksvlUhCb3W7/cf/w+BSLxfapVIqSySRlMhnSdZ2GwyHN53OaTCbU7/cFYBgG4RCPx/MKub27+1ur1Xqj0YjW6zWxCyloNBqUSCSkYDab0WAw+BBJeqLFtQpvGoFqAlAEaZomuc0ocAQnnU7nALiJ3uh8whgnttttarVaVCgUpCAUCgnQhMAJ+gG3CsDZa7xh1mw2ZbFSqYgwgsGgbDQhcIWeAHSIoP1pcGeNarUqgFKpJMLw+/0q72azkYhmPAWIRmM6AGbXc7kc5fN5AXi9XgWACwAguLEAojrfsVGv1yV/sVikcrksAIfDIYUQHEAoPgLwT3GdzWYNdBfXh3xwApDP5zsqtkoBwuHwaSAQ+OV2u//F43GKRCLEc5ROpwVoOngvBXj7jU/wwZPPX72DT7RXgDfIT27QEgvfKea9c3m9FsA5IN94zqbw9M9fAEuW+zzj8uLvAAAAAElFTkSuQmCC);
+      }
+    }
+
+    &.html {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJwSURBVDjLjZPdT1JhHMetvyO3/gfLKy+68bLV2qIAq7UyG6IrdRPL5hs2U5FR0MJIAqZlh7BVViI1kkyyiPkCyUtztQYTYbwJE8W+Pc8pjofK1dk+OxfP+X3O83srAVBCIc8eQhmh/B/sJezm4niCsvX19cTm5uZWPp/H3yDnUKvVKr6ELyinwWtra8hkMhzJZBLxeBwrKyusJBwOQ6PRcJJC8K4DJ/dXM04DOswNqNOLybsRo9N6LCy7kUgkEIlEWEE2mwX9iVar/Smhglqd8IREKwya3qhg809gPLgI/XsrOp/IcXVMhqnFSayurv6RElsT6ZCoov5u1fzUVwvcKRdefVuEKRCA3OFHv2MOxtlBdFuaMf/ZhWg0yt4kFAoVCZS3Hd1gkpOwRt9h0LOES3YvamzPcdF7A6rlPrSbpbhP0kmlUmw9YrHYtoDku2T6pEZ/2ICXEQ8kTz+g2TkNceAKKv2nIHachn6qBx1MI5t/Op1mRXzBd31AiRafBp1vZyEcceGCzQ6p24yjEzocGT6LUacS0iExcrkcK6Fsp6AXLRnmFOjyPMIZixPHmAAOGxZQec2OQyo7zpm6cNN6GZ2kK1RAofPAr8GA4oUMrdNNkIw/wPFhDwSjX3Dwlg0CQy96HreiTlcFZsaAjY0NNvh3QUXtHeHcoKMNA7NjqLd8xHmzDzXDRvRO1KHtngTyhzL4SHeooAAnKMxBtUYQbGWa0Dc+AsWzSVy3qkjeItLCFsz4XoNMaRFFAm4SyTXbmQa2YHQSGacR/pAXO+zGFif4JdlHCpShBzstEz+YfJtmt5cnKKWS/1jnAnT1S38AGTynUFUTzJcAAAAASUVORK5CYII=);
+      }
+    }
+
+    &.php {
+      &::before {
+        content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGsSURBVDjLjZNLSwJRFICtFv2AgggS2vQLDFvVpn0Pi4iItm1KItvWJqW1pYsRemyyNILARbZpm0WtrJ0kbmbUlHmr4+t0z60Z7oSSAx935txzvrlPBwA4EPKMEVwE9z+ME/qtOkbgqtVqUqPRaDWbTegE6YdQKBRkJazAjcWapoGu6xayLIMoilAoFKhEEAQIh8OWxCzuQwEmVKtVMAyDtoiqqiBJEhSLRSqoVCqAP+E47keCAvfU5sDQ8MRs/OYNtr1x2PXdwuJShLLljcFlNAW5HA9khLYp0TUhSYMLHm7PLEDS7zyw3ybRqyfg+TyBtwl2sDP1nKWFiUSazFex3tk45sXjL1Aul20CGTs+syVY37igBbwg03eMsfH9gwSsrZ+Doig2QZsdNiZmMkVrKmwc18azHKELyQrOMEHTDJp8HXu1hostG8dY8PiRngdWMEq467ZwbDxwlIR8XrQLcBvn5k9Gpmd8fn/gHlZWT20C/D4k8eTDB3yVFKjX6xSbgD1If8G970Q3QbvbPehAyxL8SibJEdaxo5dikqvS28sInCjp4Tqb4NV3fgPirZ4pD4KS4wAAAABJRU5ErkJggg==);
+      }
+    }
+  }
+
+  .progress {
+    border: 1px solid #eee;
+    display: inline-block;
+    float: left;
+    height: 7px;
+    margin: 2px 0 2px 2px;
+    width: 100px;
+
+    .meter {
+      background: #0c0;
+      display: block;
+      height: 7px;
+      width: 0;
+    }
+  }
+
+  .cancel-upload {
+    color: #900;
+    margin: -1px 0 0 5px;
+  }
+}

+ 146 - 11
src/components/Item.ts

@@ -7,11 +7,12 @@ import Element, {
   removeClass,
   s,
 } from '@dom111/element';
+import joinPath, { trailingSlash } from '../lib/joinPath';
 import DAV from '../lib/DAV';
 import Entry from '../lib/Entry';
 import Prism from 'prismjs';
 import State from '../lib/State';
-import joinPath from '../lib/joinPath';
+import TreeViewModal from './TreeViewModal';
 import previewItems from '../lib/previewItems';
 import { success } from 'melba-toast';
 import { t } from 'i18next';
@@ -22,13 +23,13 @@ const template = (entry: Entry): string => `<li tabindex="0" data-full-path="${
   <span class="title">${entry.title}</span>
   <input type="text" name="rename" class="hidden" readonly>
   <span class="size">${entry.displaySize}</span>
-  <a href="#" title="${t('delete')} (␡)" class="delete"></a>
-  <!--<a href="#" title="Move" class="move"></a>-->
-  <a href="#" title="${t('rename')} (F2)" class="rename"></a>
-  <!--<a href="#" title="Copy" class="copy"></a>-->
   <a href="${entry.fullPath}" download="${entry.name}" title="${t(
   'download'
 )} (⇧+⏎)"></a>
+  <a href="#" title="${t('copy')}" class="copy"></a>
+  <a href="#" title="${t('rename')} (F2)" class="rename"></a>
+  <a href="#" title="${t('move')}" class="move"></a>
+  <a href="#" title="${t('delete')} (␡)" class="delete"></a>
 </li>`;
 
 export default class Item extends Element {
@@ -91,10 +92,18 @@ export default class Item extends Element {
       this.addClass('loading');
     }
 
+    if (!entry.copy) {
+      this.query('.copy').setAttribute('hidden', '');
+    }
+
     if (!entry.del) {
       this.query('.delete').setAttribute('hidden', '');
     }
 
+    if (!entry.move) {
+      this.query('.move').setAttribute('hidden', '');
+    }
+
     if (!entry.rename) {
       this.query('.rename').setAttribute('hidden', '');
     }
@@ -145,6 +154,20 @@ export default class Item extends Element {
       this.rename();
     });
 
+    on(this.query('.copy'), 'click', (event): void => {
+      event.stopPropagation();
+      event.preventDefault();
+
+      this.copy();
+    });
+
+    on(this.query('.move'), 'click', (event): void => {
+      event.stopPropagation();
+      event.preventDefault();
+
+      this.move();
+    });
+
     this.on('keydown', (event: KeyboardEvent): void => {
       if (['F2', 'Delete', 'Enter'].includes(event.key)) {
         event.preventDefault();
@@ -170,6 +193,62 @@ export default class Item extends Element {
     });
   }
 
+  async copy(): Promise<void> {
+    const entry = this.#entry,
+      modal = new TreeViewModal(
+        t('copyItemTitle', {
+          file: entry.title,
+        })
+      );
+
+    modal.open();
+
+    const target = await modal.value();
+
+    modal.close();
+
+    if (!target) {
+      return;
+    }
+
+    const destination = joinPath(target, entry.name),
+      checkResponse = await this.#dav.check(destination, true);
+
+    if (
+      checkResponse.ok &&
+      !confirm(
+        t('overwriteFileConfirmation', {
+          file: entry.name,
+        })
+      )
+    ) {
+      return;
+    }
+
+    const response = await this.#dav.copy(
+      entry.fullPath,
+      destination,
+      entry,
+      true
+    );
+
+    if (!response.ok) {
+      return;
+    }
+
+    this.#dav.invalidateCache(trailingSlash(target));
+
+    success(
+      t('successfullyCopied', {
+        interpolation: {
+          escapeValue: false,
+        },
+        from: entry.title,
+        to: destination,
+      })
+    );
+  }
+
   async del(): Promise<void> {
     const entry = this.#entry;
 
@@ -193,7 +272,7 @@ export default class Item extends Element {
 
     const response = await this.#dav.del(entry.fullPath);
 
-    if (!response) {
+    if (!response.ok) {
       return;
     }
 
@@ -218,6 +297,63 @@ export default class Item extends Element {
     emit(this.query<HTMLAnchorElement>('[download]'), new MouseEvent('click'));
   }
 
+  async move(): Promise<void> {
+    const entry = this.#entry,
+      modal = new TreeViewModal(
+        t('moveItemTitle', {
+          file: entry.title,
+        })
+      );
+
+    modal.open();
+
+    const target = await modal.value();
+
+    modal.close();
+
+    if (!target) {
+      return;
+    }
+
+    const destination = joinPath(target, entry.name),
+      checkResponse = await this.#dav.check(destination, true);
+
+    if (
+      checkResponse.ok &&
+      !confirm(
+        t('overwriteFileConfirmation', {
+          file: entry.name,
+        })
+      )
+    ) {
+      return;
+    }
+
+    const response = await this.#dav.move(
+      entry.fullPath,
+      destination,
+      entry,
+      true
+    );
+
+    if (!response.ok) {
+      return;
+    }
+
+    this.#dav.invalidateCache(trailingSlash(target));
+    this.#state.update(true);
+
+    success(
+      t('successfullyMoved', {
+        interpolation: {
+          escapeValue: false,
+        },
+        from: entry.title,
+        to: destination,
+      })
+    );
+  }
+
   async open(): Promise<void> {
     if (this.hasClass('open')) {
       return;
@@ -225,11 +361,10 @@ export default class Item extends Element {
 
     this.addClass('open', 'loading');
 
-    const entry = this.#entry;
-
-    const response = await this.#dav.check(entry.fullPath);
+    const entry = this.#entry,
+      response = await this.#dav.check(entry.fullPath);
 
-    if (!response) {
+    if (!response.ok) {
       this.removeClass('open', 'loading');
 
       return;
@@ -360,7 +495,7 @@ export default class Item extends Element {
               entry
             );
 
-          if (!result) {
+          if (!result.ok) {
             return;
           }
 

+ 27 - 0
src/components/Modal.scss

@@ -0,0 +1,27 @@
+dialog {
+  background: rgba(0, 0, 0, 0.4);
+  border: 0;
+  height: 100vh;
+  justify-content: center;
+  left: 0;
+  position: fixed;
+  top: 0;
+  width: 100vw;
+  z-index: 2;
+
+  &[open] {
+    display: flex;
+  }
+
+  .content {
+    background: #fff;
+    border: 0;
+    border-radius: 1rem;
+    box-shadow: #333 0 0 0.25rem;
+    height: fit-content;
+    padding: 1rem;
+    position: fixed;
+    top: 10vh;
+    width: fit-content;
+  }
+}

+ 62 - 0
src/components/Modal.ts

@@ -0,0 +1,62 @@
+import Element, { CustomEventMap, h } from '@dom111/element';
+
+export class Modal<M extends CustomEventMap = CustomEventMap> extends Element<
+  HTMLDialogElement,
+  M
+> {
+  #contentArea: HTMLElement;
+
+  constructor(...childNodes: Node[]) {
+    super(h('dialog[tabindex="0"]'));
+
+    this.#contentArea = h('.content', ...childNodes);
+
+    this.element().append(this.#contentArea);
+
+    document.body.append(this.element());
+
+    this.bindEvents();
+  }
+
+  append(...childNodes: (Element | Node)[]): void {
+    this.#contentArea.append(
+      ...childNodes.map((node) =>
+        node instanceof Element ? node.element() : node
+      )
+    );
+  }
+
+  private bindEvents(): void {
+    this.on('click', (event) => {
+      if (event.target !== this.element()) {
+        return;
+      }
+
+      this.close();
+    });
+
+    this.on('keydown', (event: KeyboardEvent) => {
+      if (event.key !== 'Escape') {
+        return;
+      }
+
+      this.close();
+    });
+  }
+
+  close(): void {
+    this.element().removeAttribute('open');
+  }
+
+  open(): void {
+    this.element().setAttribute('open', '');
+
+    this.element().focus();
+  }
+
+  setLabel(label: string): void {
+    this.element().setAttribute('aria-label', label);
+  }
+}
+
+export default Modal;

+ 7 - 0
src/components/Tree/DataProvider.ts

@@ -0,0 +1,7 @@
+import Node from './Node';
+
+export interface DataProvider {
+  getChildren(node: Node): Promise<Node[]>;
+}
+
+export default DataProvider;

+ 161 - 0
src/components/Tree/Leaf.ts

@@ -0,0 +1,161 @@
+import Element, { empty, on, s, t } from '@dom111/element';
+import DataProvider from './DataProvider';
+import Node from './Node';
+
+const template = (node: Node): string => `
+<div
+    class="leaf"
+    tabindex="0"
+    aria-selected="false"
+    aria-expanded="false"
+>
+  <span class="toggle"></span>
+  <span class="name"></span>
+  <section class="children"></section>
+</div>
+`;
+
+export type LeafEvents = {
+  'leaf-collapsed': [Leaf];
+  'leaf-deselected': [Leaf];
+  'leaf-expanded': [Leaf];
+  'leaf-selected': [Leaf];
+};
+
+export class Leaf extends Element<HTMLDivElement, LeafEvents> {
+  #dataProvider: DataProvider;
+  #node: Node;
+
+  constructor(node: Node, dataProvider: DataProvider) {
+    super(s(template(node)));
+
+    this.query('.name').append(t(node.name()));
+
+    this.#dataProvider = dataProvider;
+    this.#node = node;
+
+    this.bindEvents();
+    this.update();
+  }
+
+  private bindEvents(): void {
+    const getChildren = async (node: Node) => {
+        if (node.hasChildren() === null) {
+          await this.#dataProvider.getChildren(this.#node);
+        }
+
+        if (node.hasChildren()) {
+          this.#node
+            .children()
+            .forEach((childNode) => this.#dataProvider.getChildren(childNode));
+        }
+      },
+      expand = () => {
+        this.emit(
+          new CustomEvent<[Leaf]>(
+            this.expanded() ? 'leaf-collapsed' : 'leaf-expanded',
+            {
+              bubbles: true,
+              detail: [this],
+            }
+          )
+        );
+
+        this.element().setAttribute(
+          'aria-expanded',
+          this.expanded() ? 'false' : 'true'
+        );
+
+        getChildren(this.#node);
+      },
+      select = () => {
+        this.emit(
+          new CustomEvent<[Leaf]>(
+            this.selected() ? 'leaf-deselected' : 'leaf-selected',
+            {
+              bubbles: true,
+              detail: [this],
+            }
+          )
+        );
+
+        this.element().setAttribute(
+          'aria-selected',
+          this.selected() ? 'false' : 'true'
+        );
+
+        getChildren(this.#node);
+      };
+
+    on(this.toggle(), 'click', (event) => {
+      event.preventDefault();
+      event.stopPropagation();
+
+      expand();
+    });
+
+    this.on('click', (event) => {
+      if (event.button) {
+        return;
+      }
+
+      event.preventDefault();
+      event.stopPropagation();
+
+      select();
+
+      if (
+        this.#node.hasChildren() &&
+        this.element().matches('[aria-expanded="false"]')
+      ) {
+        expand();
+      }
+    });
+
+    this.#node.on('updated', () => this.update());
+  }
+
+  private children(): HTMLDivElement {
+    return this.query<HTMLDivElement>('.children');
+  }
+
+  private expanded(): boolean {
+    return this.element().getAttribute('aria-expanded') === 'true';
+  }
+
+  node(): Node {
+    return this.#node;
+  }
+
+  private selected(): boolean {
+    return this.element().getAttribute('aria-selected') === 'true';
+  }
+
+  private toggle(): HTMLSpanElement {
+    return this.query<HTMLSpanElement>('.toggle');
+  }
+
+  private update(): void {
+    if (this.#node.hasChildren() === null) {
+      return;
+    }
+
+    if (this.#node.hasChildren()) {
+      this.addClass('hasChildren');
+
+      empty(this.children());
+
+      this.#node.children().forEach((child) => {
+        const leaf = new Leaf(child, this.#dataProvider);
+
+        this.children().append(leaf.element());
+      });
+
+      return;
+    }
+
+    this.addClass('hasNoChildren');
+  }
+}
+
+export default Leaf;

+ 51 - 0
src/components/Tree/Node.ts

@@ -0,0 +1,51 @@
+import EventEmitter from '@dom111/typed-event-emitter/EventEmitter';
+
+type NodeEvents = {
+  updated: [];
+};
+
+export class Node extends EventEmitter<NodeEvents> {
+  #fullPath: string[];
+  #children: Node[] | null;
+  #name: string;
+
+  constructor(
+    fullPath: string[],
+    name: string,
+    children: Node[] | null = null
+  ) {
+    super();
+
+    this.#fullPath = fullPath;
+    this.#name = name;
+    this.#children = children;
+  }
+
+  children(): Node[] | null {
+    return this.#children;
+  }
+
+  fullPath(): string[] {
+    return this.#fullPath;
+  }
+
+  hasChildren(): boolean | null {
+    if (this.#children === null) {
+      return null;
+    }
+
+    return this.#children.length > 0;
+  }
+
+  name(): string {
+    return this.#name;
+  }
+
+  setChildren(children: Node[]): void {
+    this.#children = children;
+
+    this.emit('updated');
+  }
+}
+
+export default Node;

+ 77 - 0
src/components/Tree/PlainObject.ts

@@ -0,0 +1,77 @@
+import DataProvider from './DataProvider';
+import Node from './Node';
+
+export class PlainObject implements DataProvider {
+  #object: { [key: string]: any };
+  #seen: Map<any, string[]> = new Map();
+
+  constructor(object: { [key: string]: any }) {
+    this.#object = object;
+  }
+
+  async getChildren(node: Node): Promise<Node[] | null> {
+    if (node.hasChildren() !== null) {
+      return node.children();
+    }
+
+    const object = node.fullPath().reduce((object: any, key) => {
+      if (typeof object === 'object' && object !== null && key in object) {
+        return object[key];
+      }
+
+      return null;
+    }, this.#object);
+
+    if (object === null || typeof object !== 'object') {
+      node.setChildren([]);
+
+      return [];
+    }
+
+    const children = Object.entries(object).map(([key, value]) => {
+      if (value && typeof value === 'object' && this.#seen.has(value)) {
+        const duplicatePath = this.#seen.get(value);
+
+        return new Node(
+          [...node.fullPath(), key],
+          `#<Duplicate: <root>${duplicatePath.reduce((s, piece) => {
+            if (typeof piece === 'number' || /^\d+$/.test(piece)) {
+              return s + `[${piece}]`;
+            }
+
+            if (typeof piece !== 'string') {
+              return s;
+            }
+
+            if (!/\W|^\d/.test(piece)) {
+              return s + `.${piece}`;
+            }
+
+            if (/'/.test(piece)) {
+              return s + `['${piece.replace(/'/g, "\\'")}']`;
+            }
+
+            return s + `['${piece}']`;
+          }, '')}>`,
+          []
+        );
+      }
+
+      this.#seen.set(value, [...node.fullPath(), key]);
+
+      return new Node(
+        [...node.fullPath(), key],
+        key +
+          (['string', 'number', 'boolean'].includes(typeof value)
+            ? ': ' + value
+            : '')
+      );
+    });
+
+    node.setChildren(children);
+
+    return children;
+  }
+}
+
+export default PlainObject;

+ 53 - 0
src/components/Tree/Tree.scss

@@ -0,0 +1,53 @@
+.tree {
+  padding: 0.5rem;
+}
+
+.leaf {
+  cursor: pointer;
+  margin: 0.1rem 0;
+  padding-left: 1rem;
+
+  .name {
+    border: 1px dotted transparent;
+    display: inline-block;
+    padding: 0.1rem;
+  }
+
+  > .toggle {
+    display: inline-block;
+    text-align: center;
+    width: 1rem;
+  }
+
+  &[aria-selected='true'] {
+    > .name {
+      background-color: rgb(0, 0, 192, 0.2);
+      border-color: rgb(0, 0, 0, 0.6);
+      font-weight: bold;
+    }
+  }
+
+  &:not(.hasChildren):not(.hasNoChildren) {
+    > .toggle::before {
+      content: '⊡';
+    }
+  }
+
+  &.hasChildren {
+    &[aria-expanded='false'] {
+      > .children {
+        display: none;
+      }
+
+      > .toggle::before {
+        content: '⊞';
+      }
+    }
+
+    &[aria-expanded='true'] {
+      > .toggle::before {
+        content: '⊟';
+      }
+    }
+  }
+}

+ 73 - 0
src/components/Tree/Tree.ts

@@ -0,0 +1,73 @@
+import Element, { s } from '@dom111/element';
+import Leaf, { LeafEvents } from './Leaf';
+import DataProvider from './DataProvider';
+import Node from './Node';
+
+interface TreeOptions {
+  multiple: boolean;
+  rootLabel: string;
+}
+
+type TreeEvents = LeafEvents & {
+  selected: [Node | null];
+};
+
+export class Tree extends Element<HTMLDivElement, TreeEvents> {
+  #dataProvider: DataProvider;
+  #options: TreeOptions = {
+    multiple: false,
+    rootLabel: '/',
+  };
+  #rootNode: Node;
+  #selected: Node | null = null;
+
+  constructor(dataProvider: DataProvider, options: Partial<TreeOptions> = {}) {
+    super(s(`<div class="tree"></div>`));
+
+    this.#dataProvider = dataProvider;
+    this.#options = {
+      ...this.#options,
+      ...options,
+    };
+    this.#rootNode = new Node([], this.#options.rootLabel);
+
+    this.bindEvents();
+    this.build(this.#rootNode);
+  }
+
+  private bindEvents(): void {
+    this.on('leaf-deselected', () => this.setSelected(null));
+
+    this.on('leaf-selected', ({ detail: [leaf] }) => {
+      if (!this.#options.multiple) {
+        this.clearSelected();
+      }
+
+      this.setSelected(leaf.node());
+    });
+  }
+
+  private async build(node: Node): Promise<void> {
+    this.append(new Leaf(node, this.#dataProvider));
+
+    this.#dataProvider.getChildren(node);
+  }
+
+  private clearSelected(): void {
+    this.queryAll('.leaf[aria-selected="true"]').forEach((leafNode) =>
+      leafNode.setAttribute('aria-selected', 'false')
+    );
+  }
+
+  private setSelected(selected: Node | null): void {
+    this.#selected = selected;
+
+    this.emitCustom('selected', selected);
+  }
+
+  value(): string[] | null {
+    return this.#selected && this.#selected.fullPath();
+  }
+}
+
+export default Tree;

+ 92 - 0
src/components/Tree/WebDAV.ts

@@ -0,0 +1,92 @@
+import { leadingAndTrailingSlash, trimSlashes } from '../../lib/joinPath';
+import DataProvider from './DataProvider';
+import Node from './Node';
+import Response from '../../lib/Response';
+
+interface WebDAVOptions {
+  debug: boolean;
+  depth: number;
+}
+
+export class WebDAV implements DataProvider {
+  #cache: Map<string, Promise<Node[]>> = new Map();
+  #options: WebDAVOptions = {
+    debug: true,
+    depth: 2,
+  };
+
+  constructor(options: Partial<WebDAVOptions> = {}) {
+    this.#options = {
+      ...this.#options,
+      ...options,
+    };
+  }
+
+  async getChildren(
+    node: Node,
+    bypassCache: boolean = false,
+    depth: number = this.#options.depth
+  ): Promise<Node[] | null> {
+    if (depth === 0) {
+      return null;
+    }
+
+    const path =
+      node.fullPath().length === 0
+        ? '/'
+        : leadingAndTrailingSlash(node.fullPath().join('/'));
+
+    if (!this.#cache.has(path) || bypassCache) {
+      this.#cache.set(
+        path,
+        fetch(path, {
+          method: 'PROPFIND',
+          headers: {
+            Depth: '1',
+          },
+        })
+          .then(async (response) => {
+            if (!response.ok) {
+              if (this.#options.debug) {
+                console.error(response);
+              }
+
+              return [];
+            }
+
+            const davResponse = new Response(
+                await response.text()
+              ).responseToPrimitives(),
+              directories = davResponse
+                .filter((entry) => entry.directory && entry.fullPath !== path)
+                .sort((a, b) => (a.fullPath < b.fullPath ? -1 : 1)),
+              children = directories.map((entry) => {
+                const fullPath = trimSlashes(entry.fullPath).split('/');
+
+                const name = decodeURIComponent(fullPath.slice(0).pop()),
+                  childNode = new Node(fullPath, name);
+
+                this.getChildren(childNode, bypassCache, depth - 1);
+
+                return childNode;
+              });
+
+            node.setChildren(children);
+
+            return children;
+          })
+          .catch((e) => {
+            if (this.#options.debug) {
+              console.error(e);
+            }
+
+            return [];
+          })
+      );
+    }
+
+    return this.#cache.get(path);
+  }
+}
+
+export default WebDAV;

+ 13 - 0
src/components/TreeViewModal.scss

@@ -0,0 +1,13 @@
+.tree-view-modal .tree {
+  max-height: 80vh;
+  min-height: 40vh;
+  overflow-y: scroll;
+}
+
+footer {
+  padding-top: 1rem;
+
+  button[type='submit'] {
+    float: right;
+  }
+}

+ 62 - 0
src/components/TreeViewModal.ts

@@ -0,0 +1,62 @@
+import { h, on, t } from '@dom111/element';
+import joinPath, { leadingAndTrailingSlash } from '../lib/joinPath';
+import Modal from './Modal';
+import Tree from './Tree/Tree';
+import WebDAV from './Tree/WebDAV';
+
+type TreeViewModalEvents = {
+  cancelled: [];
+  selected: [string];
+};
+
+export class TreeViewModal extends Modal<TreeViewModalEvents> {
+  #tree: Tree;
+
+  constructor(title: string) {
+    super();
+
+    this.#tree = new Tree(new WebDAV());
+
+    this.append(
+      h('header', h('h2', t(title))),
+      this.#tree,
+      h(
+        'footer',
+        h('button[type="submit"]', t('Choose')),
+        h('button', t('Cancel'))
+      )
+    );
+
+    this.addClass('tree-view-modal');
+
+    this.bindLocalEvents();
+  }
+
+  private bindLocalEvents(): void {
+    on(this.query('button[type="submit"]'), 'click', () => {
+      const value = this.#tree.value();
+
+      if (value === null) {
+        this.emitCustom('selected', null);
+
+        return;
+      }
+
+      this.emitCustom('selected', leadingAndTrailingSlash(joinPath(...value)));
+    });
+
+    on(this.query('button:not([type="submit"])'), 'click', () => {
+      this.emitCustom('cancelled');
+    });
+  }
+
+  async value(): Promise<string | null> {
+    return new Promise<string>((resolve) => {
+      this.on('cancelled', () => resolve(null));
+
+      this.on('selected', ({ detail: [path] }) => resolve(path));
+    });
+  }
+}
+
+export default TreeViewModal;

+ 121 - 34
src/lib/DAV.ts

@@ -1,12 +1,11 @@
+import joinPath, { trailingSlash } from './joinPath';
 import Collection from './Collection';
 import DAVResponse from './Response';
 import Entry from './Entry';
 import HTTP from './HTTP';
 import RequestFailure from './HTTP/RequestFailure';
 import { error } from 'melba-toast';
-import joinPath from './joinPath';
 import { t } from 'i18next';
-import trailingSlash from './trailingSlash';
 
 type ConstructorOptions = {
   bypassCheck?: boolean;
@@ -35,12 +34,22 @@ const emptyCache = (): RequestCache => {
   return cache;
 };
 
-export default class DAV {
+export class DAV {
   #bypassCheck: boolean;
   #cache: RequestCache;
   #http: HTTP;
   #sortDirectoriesFirst: boolean;
 
+  #dropCache = <K extends keyof CachedRequests = keyof CachedRequests>(
+    type: K,
+    uri: string
+  ): void | null => {
+    const lookup = this.#cache.get(type);
+
+    if (lookup.has(uri)) {
+      lookup.delete(uri);
+    }
+  };
   #getCache = <K extends keyof CachedRequests = keyof CachedRequests>(
     type: K,
     uri: string
@@ -71,7 +80,8 @@ export default class DAV {
     lookup.set(uri, value);
   };
   #toastOnFailure = async (
-    func: () => Promise<Response>
+    func: () => Promise<Response>,
+    quiet: boolean = false
   ): Promise<Response> => {
     try {
       return await func();
@@ -80,17 +90,21 @@ export default class DAV {
         throw e;
       }
 
-      error(
-        t('failure', {
-          interpolation: {
-            escapeValue: false,
-          },
-          method: e.method(),
-          url: e.url(),
-          statusText: e.statusText(),
-          status: e.status(),
-        })
-      );
+      if (!quiet) {
+        error(
+          t('failure', {
+            interpolation: {
+              escapeValue: false,
+            },
+            method: e.method(),
+            url: e.url(),
+            statusText: e.statusText(),
+            status: e.status(),
+          })
+        );
+      }
+
+      return e.response();
     }
   };
   #validDestination = (destination: string): string => {
@@ -121,7 +135,14 @@ export default class DAV {
     this.#http = http;
   }
 
-  async check(uri): Promise<{
+  /**
+   * @param uri The URI to perform a HEAD request for
+   * @param quiet Whether or not to invoke the error toast
+   */
+  async check(
+    uri: string,
+    quiet: boolean = false
+  ): Promise<{
     ok: boolean;
     status: number;
   }> {
@@ -132,33 +153,74 @@ export default class DAV {
       });
     }
 
-    return this.#toastOnFailure((): Promise<Response> => this.#http.HEAD(uri));
+    return this.#toastOnFailure(
+      (): Promise<Response> => this.#http.HEAD(uri),
+      quiet
+    );
   }
 
-  async copy(from, to): Promise<Response> {
+  /**
+   * @param from The path to the file or directory to copy
+   * @param to The path of the directory to copy to
+   * @param entry The Entry for the file to copy
+   * @param overwrite
+   */
+  async copy(
+    from: string,
+    to: string,
+    entry: Entry,
+    overwrite: boolean = false
+  ): Promise<Response> {
+    const headers: HeadersInit = {
+      Destination: this.#validDestination(to),
+      Overwrite: overwrite ? 'T' : 'F',
+    };
+
+    if (entry.directory) {
+      headers['Depth'] = 'infinity';
+    }
+
     return this.#toastOnFailure(
       (): Promise<Response> =>
         this.#http.COPY(from, {
-          headers: {
-            Destination: this.#validDestination(to),
-          },
+          headers,
         })
     );
   }
 
-  async del(uri): Promise<Response> {
+  /**
+   * @param fullPath The full path of the directory to create
+   */
+  async createDirectory(fullPath: string): Promise<Response> {
     return this.#toastOnFailure(
-      (): Promise<Response> => this.#http.DELETE(uri)
+      (): Promise<Response> => this.#http.MKCOL(fullPath)
+    );
+  }
+
+  /**
+   * @param uri The URI of the item to delete
+   */
+  async del(uri: string): Promise<Response> {
+    return this.#toastOnFailure(
+      (): Promise<Response> =>
+        this.#http.DELETE(uri, {
+          headers: {
+            Depth: 'infinity',
+          },
+        })
     );
   }
 
-  async get(uri): Promise<string | null> {
+  /**
+   * @param uri The URI of the item to get
+   */
+  async get(uri: string): Promise<string | null> {
     if (!this.#hasCache('GET', uri)) {
       const response = await this.#toastOnFailure(
         (): Promise<Response> => this.#http.GET(uri)
       );
 
-      if (!response || !response.ok) {
+      if (!response.ok) {
         return;
       }
 
@@ -168,7 +230,20 @@ export default class DAV {
     return this.#getCache('GET', uri);
   }
 
-  async list(uri, bypassCache = false): Promise<Collection> {
+  /**
+   * @param path The directory path to invalidate cache for
+   */
+  invalidateCache(path: string): void {
+    if (this.#hasCache('PROPFIND', path)) {
+      this.#dropCache('PROPFIND', path);
+    }
+  }
+
+  /**
+   * @param uri The directory to list
+   * @param bypassCache Force skipping cache
+   */
+  async list(uri: string, bypassCache: boolean = false): Promise<Collection> {
     uri = trailingSlash(uri);
 
     if (!bypassCache && this.#hasCache('PROPFIND', uri)) {
@@ -194,13 +269,18 @@ export default class DAV {
     return collection;
   }
 
-  async mkcol(fullPath) {
-    return this.#toastOnFailure(
-      (): Promise<Response> => this.#http.MKCOL(fullPath)
-    );
-  }
-
-  async move(from: string, to: string, entry: Entry): Promise<Response> {
+  /**
+   * @param from The path to the file or directory to move
+   * @param to The path of the directory to move to
+   * @param entry The Entry object for the file or directory to move
+   * @param overwrite
+   */
+  async move(
+    from: string,
+    to: string,
+    entry: Entry,
+    overwrite: boolean = false
+  ): Promise<Response> {
     const destination = this.#validDestination(to);
 
     return this.#toastOnFailure(
@@ -210,12 +290,17 @@ export default class DAV {
             Destination: entry.directory
               ? trailingSlash(destination)
               : destination,
+            Overwrite: overwrite ? 'T' : 'F',
           },
         })
     );
   }
 
-  async upload(path, file): Promise<Response> {
+  /**
+   * @param path The path to upload the file to
+   * @param file The File object to upload
+   */
+  async upload(path: string, file: File): Promise<Response> {
     const targetFile = joinPath(path, file.name);
 
     return this.#toastOnFailure(
@@ -229,3 +314,5 @@ export default class DAV {
     );
   }
 }
+
+export default DAV;

+ 27 - 13
src/lib/Entry.ts

@@ -1,13 +1,14 @@
+import joinPath, { pathAndName, trailingSlash } from './joinPath';
 import Collection from './Collection';
 import EventEmitter from '@dom111/typed-event-emitter/EventEmitter';
-import joinPath from './joinPath';
-import trailingSlash from './trailingSlash';
 
 type EntryArgs = {
+  copy?: boolean;
   directory?: boolean;
   fullPath?: string;
   title?: string;
   modified?: number;
+  move?: boolean;
   size?: number;
   mimeType?: string;
   del?: boolean;
@@ -21,6 +22,7 @@ type EntryEvents = {
 };
 
 export default class Entry extends EventEmitter<EntryEvents> {
+  #copy: boolean;
   #del: boolean;
   #directory: boolean;
   #displaySize: string;
@@ -28,6 +30,7 @@ export default class Entry extends EventEmitter<EntryEvents> {
   #fullPath: string;
   #mimeType: string;
   #modified: Date;
+  #move: boolean;
   #name: string;
   #path: string;
   #placeholder: boolean;
@@ -39,16 +42,18 @@ export default class Entry extends EventEmitter<EntryEvents> {
   collection: Collection | null;
 
   constructor({
-    directory = false,
     fullPath,
-    title = '',
-    modified,
-    size = 0,
-    mimeType = '',
+    collection = null,
+    copy = true,
     del = true,
-    rename = true,
+    directory = false,
+    mimeType = '',
+    modified,
+    move = true,
     placeholder = false,
-    collection = null,
+    rename = true,
+    size = 0,
+    title = '',
   }: EntryArgs) {
     super();
 
@@ -59,10 +64,12 @@ export default class Entry extends EventEmitter<EntryEvents> {
 
     this.#path = path;
     this.#name = name;
+    this.#copy = copy;
     this.#directory = directory;
     this.#fullPath = fullPath;
     this.#title = title;
     this.#modified = modifiedDate;
+    this.#move = move;
     this.#size = size;
     this.#mimeType = mimeType;
     this.#del = del;
@@ -75,16 +82,15 @@ export default class Entry extends EventEmitter<EntryEvents> {
     return this.update({
       fullPath: trailingSlash(this.path),
       title: '&larr;',
+      copy: false,
       del: false,
+      move: false,
       rename: false,
     });
   }
 
   getFilename(path: string): [string, string] {
-    const pathParts = joinPath(path).split(/\//),
-      file = pathParts.pop();
-
-    return [joinPath(...pathParts), file];
+    return pathAndName(path);
   }
 
   update(properties: EntryArgs = {}): Entry {
@@ -107,6 +113,10 @@ export default class Entry extends EventEmitter<EntryEvents> {
     return newEntry;
   }
 
+  get copy(): boolean {
+    return this.#copy;
+  }
+
   get del(): boolean {
     return this.#del;
   }
@@ -164,6 +174,10 @@ export default class Entry extends EventEmitter<EntryEvents> {
     return this.#modified;
   }
 
+  get move(): boolean {
+    return this.#move;
+  }
+
   get name(): string {
     return this.#name;
   }

+ 5 - 1
src/lib/Response.ts

@@ -41,7 +41,11 @@ export class Response {
     return this.#collection;
   }
 
-  responseToPrimitives(responses: HTMLCollection): EntryObject[] {
+  responseToPrimitives(
+    responses: HTMLCollection = this.#document.getElementsByTagName(
+      'D:response'
+    )
+  ): EntryObject[] {
     return Array.from(responses).map(
       (response): EntryObject => ({
         directory: !!getTag(response, 'D:collection'),

+ 1 - 1
src/lib/handleFileUpload.ts

@@ -50,7 +50,7 @@ export const handleFileUpload = async (
 
   const result = await dav.upload(location.pathname, file);
 
-  if (!result) {
+  if (!result.ok) {
     collection.remove(placeholder);
 
     state.update();

+ 24 - 6
src/lib/joinPath.ts

@@ -1,10 +1,28 @@
+export const joinPath = (...pieces: string[]): string =>
+  leadingSlash(
+    pieces
+      .map(trimSlashes)
+      .filter((piece) => piece)
+      .join('/')
+  );
+
+export const leadingAndTrailingSlash = (text: string): string =>
+  leadingSlash(trailingSlash(text));
+
+export const leadingSlash = (text: string): string =>
+  text.startsWith('/') ? text : `/${text}`;
+
+export const pathAndName = (path: string): [string, string] => {
+  const pathParts = joinPath(path).split(/\//),
+    file = pathParts.pop();
+
+  return [joinPath(...pathParts), file];
+};
+
+export const trailingSlash = (text: string): string =>
+  text.endsWith('/') ? text : `${text}/`;
+
 export const trimSlashes = (piece: string): string =>
   piece.replace(/^\/+|\/+$/g, '');
 
-export const joinPath = (...pieces: string[]): string =>
-  `/${pieces
-    .map(trimSlashes)
-    .filter((piece) => piece)
-    .join('/')}`;
-
 export default joinPath;

+ 0 - 4
src/lib/trailingSlash.ts

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

+ 13 - 161
src/style.scss

@@ -63,167 +63,6 @@ main {
     border-radius: 5px;
     margin: 0;
     padding: 0 5px;
-
-    li {
-      background: none no-repeat left center;
-      border-top: 1px solid #eee;
-      cursor: pointer;
-      display: block;
-      overflow: hidden;
-      padding: 5px 0 5px 5px;
-
-      &:hover {
-        background-color: #fafafa;
-      }
-
-      &:first-child {
-        border-top: 0;
-      }
-
-      &.active {
-        color: #000;
-      }
-
-      &.loading {
-        background-size: contain;
-
-        * {
-          pointer-events: none;
-        }
-      }
-
-      .size {
-        color: #aaa;
-        display: inline-block;
-        margin: 0 10px;
-      }
-
-      .copy,
-      .move,
-      .rename,
-      .delete,
-      [download] {
-        background: none no-repeat center center;
-        float: right;
-        height: 16px;
-        margin: 0 5px;
-        overflow: hidden;
-        text-indent: 26px;
-        white-space: nowrap;
-        width: 16px;
-      }
-
-      .copy {
-        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ1SURBVBgZBcFNiJVVGADg5z3fmTujY5NZBANBIVkZ2J9IKkZFGKRuglq1KaqVtoqoVbSKFi1LoY2QEVSbcqiFWWJqPxL044wUGESQVqJOKerce7/z9jyRmba++tXTy2YmnyphPaYQIJBBNuPWfls8l1/EfxdeOrJnxxAgMtO2148d2ffC+rWlxMqkkwBkQjp7aeT97xf99cfS5ZPzv6w6umfHElQoXdw+qN3KhX90JYIgG30243G6Muo9tOYa999WfdfOLs92x4UHd3163eG3ti8ViIgVmdkNumKiUIOu0AURFIFmdmZgx4ZZt9w6uazOTO+FAklAQQlKhBKhRCgRShfOnL/i5hUjd64Kz2+6XjfRPQkVIJPaEUJGaH1SQu0YZHHqXBq2sdaGHlg9KWoZQ4VMEjWKlBJRQiAb2RUGlBZa66RCFFAh0RBBCIlENiY6QBTRhyypIROo0MZk0hDITFAKWqhdkkGSQt/oG1ChtZSZJCkBSCCEE79+Yv7UnIuXLxiNR8rwnsomFfpGn2SjAUjQkuPzHzp98XMPb9ngplVrHFr42OX5ubpx1943K7Rxaple+2EopBZkBo2MNL3wnie2P6ovvbtntzp48iMb1232+6n9OyuMx72+Z3Zmwn03Fi3pkz5oyWffnjERKzy29lnw4iPvmDuxG/unKoyXWhu3lsNefPNnr0VKAVpy/tK/Fk5/7afTR72yda83DjxjqpuEqxVGV/u/pwfdDS+vG05nZpE0wLXLqn2Lzzn287s237XF3IndBlEd/fEwvB2ZacPOgzvHo3w8Iu5NuRxAkkhpovug1u5Q5SoGfWurDxzf/eW2/wEnITFm/fHryQAAAABJRU5ErkJggg==);
-      }
-
-      .move {
-        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ2SURBVBgZBcFLiJVlGADg5/3+b87cbLyFNBJ4oexGQYqIi6hFQambgohoE0aLUqGCaBcuonWLUFe1CIJolWCLaiK1C0FUREpRBgmWNpqi4XjOnP97e57ITI+8fuLZ6bnJZ0rYhikECGSQzbi1M1cu5UJcvfzqycN7RgCRmXa9+dXJ9w5su6uUWJV0EoBMSIv/LXv/uyvOnx1eP/3zL2u+PLxnCBVKF3cMarfq1D+6EkGQjT6b8TgtLfceuv0mO7ZU37bFmWx3Xn5w/7HVx9/ePSwQESsysxt0xUShBl2hCyIoAs383MCe7fM23jY5Xedm34UCSUBBCUqEEqFEKBFKF/7+d8mGFcvuXhOe37lWN9E9CRUgk9oRQkZofVJC7Rhk8fulNGpjrY08sHlS1DKGCpkkahQpJaKEQDayKwwoLbTWSYUooEKiIYIQEolsTHSAKKIPWVJDJlChjcmkIZCZoBS0ULskgySFvtE3oEJrKTNJUgKQQAj950eMFg5ZPvebU+vW2zH9WGWnCn2jT7LRACRoyY2FI6ZOfeC+p54zuekeSz99YubkQv304YkDFdo4tUwHfxgJqQWZQSMjPX30Lbv3vmDqzBeceMPMylU2b9jg+1/z5Qrjca/vmZ+bsHVd0ZI+6YOWrL7yp6lbNrHrFQD14LyuxcYK42Fr49Zy1ItvzvVapBSgJetXzrv+4zGzR180XDrvOq5d7fSdvyos3+gvzA66m1+7dzSbmUXSACunq4vn9zt9/B23rp5WuwnXFsf+uNBJ/aHITNv3fbZvvJyPR8T9KWcAJImUHh0eq1sXP+zWDi/G1cHc8Oxgy8cvffT1E/8D2iAtJW5RUGAAAAAASUVORK5CYII=);
-      }
-
-      .rename {
-        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAFUSURBVDjLrZM/SAJxGIZdWwuDlnCplkAEm1zkaIiGFFpyMIwGK5KGoK2lphDKkMDg3LLUSIJsSKhIi+684CokOtTiMizCGuzEU5K3vOEgKvtBDe/2Pc8H3x8NAM1fQlx4H9M3pcOWp6TXWmM8A7j0629v1nraiAVC0IrrwATKIgs5xyG5QiE+Z4iQdoeU2oAsnqCSO1NSTu+D9VhqRLD8nIB8F0Q2MgmJDyipCzjvYJkIfpN2UBLG8MpP4dxvQ3ZzGuyyBQ2H+AnOOCBd9aL6soh81A5hyYSGWyCFvxUcerqI4S+CvYVOFPMHxLAq8I3qdHVY5LbBhJzEsCrwutpRFBlUHy6wO2tEYtWAzLELPN2P03kjfj3luqDycV2F8AgefWbEnVqEHa2IznSD6BdsVDNStB0lfh0FPoQjdx8RrAqGzC0YprSgxzsUMOY2bf37N/6Ud1Vc9yYcH50CAAAAAElFTkSuQmCC);
-      }
-
-      .delete {
-        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==);
-      }
-
-      [download] {
-        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKYSURBVBgZBcFNiFZlGADQ87z3zjfjzDiMk5VimERmBmFpKgVRVATRQLRo5aaNhFA7pZW0qV1Ci0BoUVRQtLc2UmQaJVhBVChUhP2YDuow5sz3c7/36Zx46uipF9fNTR4oYQ82oC+QQVZdrb8tX83PY+X6kdPHF4cAAPHMG2dOf/jKnp2lxHymBiAT0tJ/Ix+dW/bvxcHqLz+dXzhzfHEAAG1pYkevbeZ/vqIpJWRlnFXXpbXR2OPb19t/T+tsXZrOeu/1x17+dMOpt58dAJSImM3MptcUbdAUmiCCIlBtnutZ3LvZtrsn17VzM+8BQEkCCkqEEqFEKBFKEy5dW3Pn7Mh9C+Hgw7doJpoXAKCFTNoGEeo4KaFt6GXx69U0rJ1ahx69a1K0pQOANpNEG0WVooTA+ycuWD/J6rCzYbbnnytrPivs2jQ7dfidH/P6ypovfvh7f5uoiKAIiaysrfY9sW+H53bPOvHtX44e2AkAPvhySa1xttSOTCoyE6kUVgcj12504OS5ywD6HSsDlpaHIqq21pzKTBIASSmhSUbj6tih3WpNiUaabhkMBlZuDrWjYfZHNade+24oI2UGlYWJoj8Yef2TP5SgG1d1XCUyk2R6omq7bmw8ZvPchAduLcbJODh7IQ3GnaZMIIWxiJA1ZVZdNzIcdtpuUGtXaw7H4ptLnRTgxurIRLQOLa7X1SEKSUY11fa8+fFFV1duakf98eWZXrPx1fuHM1mzVMCRU6kxoVrz7vcv2Ti9VSiWVv90cO9biimrg6E2Io89ffir5yPiwZQL6Eu2b52d2raJ22dveHLXPvO9LRSW+1vcNtOZ7tFGiMwEAAAPHTr59fY71j0yPzlvqp3T1ZGoVeRYjVXnL/7uysrI/62cRssvlMuuAAAAAElFTkSuQmCC);
-      }
-
-      input {
-        border: 0;
-        padding: 0;
-        font-size: 1rem;
-      }
-
-      &.directory {
-        &::before {
-          content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC);
-        }
-
-        .size,
-        [download] {
-          display: none;
-        }
-      }
-
-      &.file {
-        &::before {
-          content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC4SURBVCjPdZFbDsIgEEWnrsMm7oGGfZrohxvU+Iq1TyjU60Bf1pac4Yc5YS4ZAtGWBMk/drQBOVwJlZrWYkLhsB8UV9K0BUrPGy9cWbng2CtEEUmLGppPjRwpbixUKHBiZRS0p+ZGhvs4irNEvWD8heHpbsyDXznPhYFOyTjJc13olIqzZCHBouE0FRMUjA+s1gTjaRgVFpqRwC8mfoXPPEVPS7LbRaJL2y7bOifRCTEli3U7BMWgLzKlW/CuebZPAAAAAElFTkSuQmCC);
-        }
-
-        &.image {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIcSURBVDjLjZO/T1NhFIafc+/trdRaYk1KUEEWjXHRaCSik+E/cDHGzYXB2YHRhMRFY1SYmRgYHZ3VxIRFDYMraMC2hrbQXm7v9+M4UGobiOEk7/adN+9zvnNEVQEQkYvAGBDy/6oBm6rqAVBVeia30jRtGmOctVaPU5qmuri4+AaYAgJVHTKYNsa4drutnU6nr1arpY1GQ6vVqlprdXt7W5eWlvomMv/uw6tSofB4p+NOF0biYtc48tEAhXiuTZzh/s1xyuUyWZbhvWdlZeXt3Nzca14sf6zW6nXf7uzrcfq9s6sLy5+1Xq8fQQKmo1ZCvlAoyo+tXT5tPGO09IckM2zWznH3/AJ3rl5ACInjmGazifceay2VSgWASISSBaz3FIs1RnJlPF18vEG1keDVk1lLFEWICM45wvAfYqTKriqje0lGI01x2qFtuuwkKQ26oEKcCwnDEBFBRA6HfmBw8JWwl3o2ti7j8+u0TUKzcYkrY/n+wyAIEJEjSxEglLyH5r7j+tg8T1oVZr8GzE69JIoiFMiM7zeHYUgQBAMJVBGU77+eYoxhLcvIxnNk6w8xxvDo3hqH+yIieO+HEkQB/qe6bPL5g/cckCkDiBhjOJULhlCGDJIkXX2z+m3GeW4UCnExyxxxHIIOLNLk2WP5AaQXTYDb1tovgHCy8lEUzQS9g1LAO+f2AX+SZudcAjgZOOeJ3jkHJ0zggNpfYEZnU63wHeoAAAAASUVORK5CYII=);
-          }
-        }
-
-        &.py,
-        &.css,
-        &.js,
-        &.xml {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHtSURBVDjLjZM9T9tQFIYpQ5eOMBKlW6eWIQipa8RfQKQghEAKqZgKFQgmFn5AWyVDCipVQZC2EqBWlEqdO2RCpAssQBRsx1+1ndix8wFvfW6wcUhQsfTI0j33PD7n+N4uAF2E+/S5RFwG/8Njl24/LyCIOI6j1+v1y0ajgU64cSSTybdBSVAwSMmmacKyLB/DMKBpGkRRZBJBEJBKpXyJl/yABLTBtm1Uq1X2JsrlMnRdhyRJTFCpVEAfSafTTUlQoFs1luxBAkoolUqQZbmtJTYTT/AoHInOfpcwtVtkwcSBgrkDGYph+60oisIq4Xm+VfB0+U/P0Lvj3NwPGfHPTcHMvoyFXwpe7UmQtAqTUCU0D1VVbwTPVk5jY19Fe3ZfQny7CE51WJDXqpjeEUHr45ki9rIqa4dmQiJfMLItGEs/FcQ2ucbRmdnSYy5vYWyLx/w3EaMfLmBaDpMQvuDJ65PY8Dpnz3wpYmLtApzcrIAqmfrEgdZH1grY/a36w6Xz0DKD8ES25/niYS6+wWE8mWfByY8cXmYEJFYLkHUHtVqNQcltAvoLD3v7o/FUHsNvzlnwxfsCEukC/ho3yUHaBN5Buo17Ojtyl+DqrnvQgUtfcC0ZcAdkUeA+ye7eMru9AUGIJPe4zh509UP/AAfNypi8oj/mAAAAAElFTkSuQmCC);
-          }
-        }
-
-        &.log,
-        &.txt,
-        &.nfo {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADoSURBVBgZBcExblNBGAbA2ceegTRBuIKOgiihSZNTcC5LUHAihNJR0kGKCDcYJY6D3/77MdOinTvzAgCw8ysThIvn/VojIyMjIyPP+bS1sUQIV2s95pBDDvmbP/mdkft83tpYguZq5Jh/OeaYh+yzy8hTHvNlaxNNczm+la9OTlar1UdA/+C2A4trRCnD3jS8BB1obq2Gk6GU6QbQAS4BUaYSQAf4bhhKKTFdAzrAOwAxEUAH+KEM01SY3gM6wBsEAQB0gJ+maZoC3gI6iPYaAIBJsiRmHU0AALOeFC3aK2cWAACUXe7+AwO0lc9eTHYTAAAAAElFTkSuQmCC);
-          }
-        }
-
-        &.rb {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIESURBVDjLjZNPTxNBGIexid9CEr8DBr8CHEiMVoomJiQkxBIM3dgIiaIESJTGGpVtyXIzHhoM4SIe9KAnEi4clQtJEczWFrbdP93d7s7u/JwZ7XYJBdnkyRxmfs/MvO9OD4AeDvuuMPoY/f/hKiMR5WKCvlarpRNCwiAI0A02D1mW38QlcUE/DzebTdi2HWEYBhqNBqrVqpBUKhUUCoVI0g5f4gK+wHVdeJ4nRo5lWdB1HbVaTQgcxwHfRFGUvxIuCKYfzmqZyZ2wKIO8fQ3/1Uv4Sy/QWliAO/sU9qMZmFMS3HfvT1xJ1ITOZJ9RpQi6+RH0y2fQb19BP23CVhRo+TysXA71+XkcMIk6fAfHK6tQVfWEoESXngNra0C5DHZJYGMDZiaD35IEi41qOo3vc3MoJ1Ooj92HpmkdQZiVEsHUAzl88hjY3gYIAdbXYQ0MoDo4CH1kBHssvH8jCf3eGKzDXzBNsyNoF/HH7WSJZLPA7i6wtQVnaAhmKoXjxUX8vDkMY3Qcnm6IInJOCS4nEte9QhF+RhInIRMTcFhYvZWCcXcUPmsl7w6H/w+nBFEb5SLc8TTo8jLq7M4m25mHfd8X8PC5AtHrXB5NdmwRrnfCcc4VCEnpA8jREasp6cpZAnrWO+hCGAn+Sa6xAtl84iJhttYSrzcm6OWSCzznNvzp9/4BgwKvG3Zq1eoAAAAASUVORK5CYII=);
-          }
-        }
-
-        &.sql {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHVSURBVDjLjZPLaiJBFIZNHmJWCeQdMuT1Mi/gYlARBRUkao+abHUhmhgU0QHtARVxJ0bxhvfGa07Of5Iu21yYFPyLrqrz1f+f6rIRkQ3icca6ZF39RxesU1VnAVyuVqvJdrvd73Y7+ky8Tk6n87cVYgVcoXixWNByuVSaTqc0Ho+p1+sJpNvtksvlUhCb3W7/cf/w+BSLxfapVIqSySRlMhnSdZ2GwyHN53OaTCbU7/cFYBgG4RCPx/MKub27+1ur1Xqj0YjW6zWxCyloNBqUSCSkYDab0WAw+BBJeqLFtQpvGoFqAlAEaZomuc0ocAQnnU7nALiJ3uh8whgnttttarVaVCgUpCAUCgnQhMAJ+gG3CsDZa7xh1mw2ZbFSqYgwgsGgbDQhcIWeAHSIoP1pcGeNarUqgFKpJMLw+/0q72azkYhmPAWIRmM6AGbXc7kc5fN5AXi9XgWACwAguLEAojrfsVGv1yV/sVikcrksAIfDIYUQHEAoPgLwT3GdzWYNdBfXh3xwApDP5zsqtkoBwuHwaSAQ+OV2u//F43GKRCLEc5ROpwVoOngvBXj7jU/wwZPPX72DT7RXgDfIT27QEgvfKea9c3m9FsA5IN94zqbw9M9fAEuW+zzj8uLvAAAAAElFTkSuQmCC);
-          }
-        }
-
-        &.html {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJwSURBVDjLjZPdT1JhHMetvyO3/gfLKy+68bLV2qIAq7UyG6IrdRPL5hs2U5FR0MJIAqZlh7BVViI1kkyyiPkCyUtztQYTYbwJE8W+Pc8pjofK1dk+OxfP+X3O83srAVBCIc8eQhmh/B/sJezm4niCsvX19cTm5uZWPp/H3yDnUKvVKr6ELyinwWtra8hkMhzJZBLxeBwrKyusJBwOQ6PRcJJC8K4DJ/dXM04DOswNqNOLybsRo9N6LCy7kUgkEIlEWEE2mwX9iVar/Smhglqd8IREKwya3qhg809gPLgI/XsrOp/IcXVMhqnFSayurv6RElsT6ZCoov5u1fzUVwvcKRdefVuEKRCA3OFHv2MOxtlBdFuaMf/ZhWg0yt4kFAoVCZS3Hd1gkpOwRt9h0LOES3YvamzPcdF7A6rlPrSbpbhP0kmlUmw9YrHYtoDku2T6pEZ/2ICXEQ8kTz+g2TkNceAKKv2nIHachn6qBx1MI5t/Op1mRXzBd31AiRafBp1vZyEcceGCzQ6p24yjEzocGT6LUacS0iExcrkcK6Fsp6AXLRnmFOjyPMIZixPHmAAOGxZQec2OQyo7zpm6cNN6GZ2kK1RAofPAr8GA4oUMrdNNkIw/wPFhDwSjX3Dwlg0CQy96HreiTlcFZsaAjY0NNvh3QUXtHeHcoKMNA7NjqLd8xHmzDzXDRvRO1KHtngTyhzL4SHeooAAnKMxBtUYQbGWa0Dc+AsWzSVy3qkjeItLCFsz4XoNMaRFFAm4SyTXbmQa2YHQSGacR/pAXO+zGFif4JdlHCpShBzstEz+YfJtmt5cnKKWS/1jnAnT1S38AGTynUFUTzJcAAAAASUVORK5CYII=);
-          }
-        }
-
-        &.php {
-          &::before {
-            content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGsSURBVDjLjZNLSwJRFICtFv2AgggS2vQLDFvVpn0Pi4iItm1KItvWJqW1pYsRemyyNILARbZpm0WtrJ0kbmbUlHmr4+t0z60Z7oSSAx935txzvrlPBwA4EPKMEVwE9z+ME/qtOkbgqtVqUqPRaDWbTegE6YdQKBRkJazAjcWapoGu6xayLIMoilAoFKhEEAQIh8OWxCzuQwEmVKtVMAyDtoiqqiBJEhSLRSqoVCqAP+E47keCAvfU5sDQ8MRs/OYNtr1x2PXdwuJShLLljcFlNAW5HA9khLYp0TUhSYMLHm7PLEDS7zyw3ybRqyfg+TyBtwl2sDP1nKWFiUSazFex3tk45sXjL1Aul20CGTs+syVY37igBbwg03eMsfH9gwSsrZ+Doig2QZsdNiZmMkVrKmwc18azHKELyQrOMEHTDJp8HXu1hostG8dY8PiRngdWMEq467ZwbDxwlIR8XrQLcBvn5k9Gpmd8fn/gHlZWT20C/D4k8eTDB3yVFKjX6xSbgD1If8G970Q3QbvbPehAyxL8SibJEdaxo5dikqvS28sInCjp4Tqb4NV3fgPirZ4pD4KS4wAAAABJRU5ErkJggg==);
-          }
-        }
-      } // main ul li.file
-
-      .progress {
-        border: 1px solid #eee;
-        display: inline-block;
-        float: left;
-        height: 7px;
-        margin: 2px 0 2px 2px;
-        width: 100px;
-
-        .meter {
-          background: #0c0;
-          display: block;
-          height: 7px;
-          width: 0;
-        }
-      }
-
-      .cancel-upload {
-        color: #900;
-        margin: -1px 0 0 5px;
-      }
-    } // main ul li
   } // main ul
 } // main
 
@@ -280,3 +119,16 @@ main {
     }
   }
 }
+
+@import 'components/Tree/Tree';
+
+.tree .leaf .name {
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC);
+  background-position: left center;
+  background-repeat: no-repeat;
+  padding-left: 1.2rem;
+}
+
+@import 'components/Item';
+@import 'components/Modal';
+@import 'components/TreeViewModal';

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
src/webdav-min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
src/webdav.js.map


+ 3 - 0
src/webdav.ts

@@ -12,6 +12,9 @@ import de from '../translations/de.json';
 import en from '../translations/en.json';
 import pt from '../translations/pt.json';
 import { use } from 'i18next';
+import Tree from './components/Tree/Tree';
+import WebDAV from './components/Tree/WebDAV';
+import PlainObject from './components/Tree/PlainObject';
 
 use(LanguageDetector)
   .init({

+ 1 - 1
tests/functional/List.test.ts

@@ -7,8 +7,8 @@ import {
   isElementGone,
   isElementThere,
 } from '../lib/isReady';
-import trailingSlash from '../../src/lib/trailingSlash';
 import * as fs from 'fs';
+import { trailingSlash } from '../../src/lib/joinPath';
 
 const BASE_URL = process.env.BASE_URL ?? 'http://localhost:8080/',
   DESTINATION_FONT_FILE = '/tmp/BlackAndWhitePicture-Regular.ttf';

+ 23 - 4
tests/unit/DAV.test.ts

@@ -29,7 +29,15 @@ describe('DAV', () => {
         (SpyHTTP[methodName] = jest.fn(
           () =>
             new Promise((resolve) =>
-              resolve(SpyHTTPReturns[methodName] ?? null)
+              resolve(
+                SpyHTTPReturns[methodName] ?? {
+                  ok: true,
+                  status: 200,
+                  async text(): Promise<string> {
+                    return '';
+                  },
+                }
+              )
             )
         ))
     );
@@ -72,12 +80,20 @@ describe('DAV', () => {
     const [SpyHTTP, SpyCache] = getSpies(),
       dav = new DAV({}, SpyCache, SpyHTTP);
 
-    dav.copy('/copySource', '/copyDestination');
+    dav.copy(
+      '/copySource',
+      '/copyDestination',
+      new Entry({
+        fullPath: '/copySource',
+      })
+    );
+
     expect(SpyHTTP.COPY).toHaveBeenCalledWith('/copySource', {
       headers: {
         Destination: `${location.protocol}//${location.hostname}${
           location.port ? `:${location.port}` : ''
         }/copyDestination`,
+        Overwrite: 'F',
       },
     });
   });
@@ -87,7 +103,9 @@ describe('DAV', () => {
       dav = new DAV({}, SpyCache, SpyHTTP);
 
     dav.del('/checkDeleteRequest');
-    expect(SpyHTTP.DELETE).toHaveBeenCalledWith('/checkDeleteRequest');
+    expect(SpyHTTP.DELETE).toHaveBeenCalledWith('/checkDeleteRequest', {
+      headers: { Depth: 'infinity' },
+    });
   });
 
   it('should fire a GET request on get', () => {
@@ -131,7 +149,7 @@ describe('DAV', () => {
       dav = new DAV({}, SpyCache, SpyHTTP);
     0;
 
-    dav.mkcol('/checkMkcolRequest');
+    dav.createDirectory('/checkMkcolRequest');
     expect(SpyHTTP.MKCOL).toHaveBeenCalledWith('/checkMkcolRequest');
   });
 
@@ -153,6 +171,7 @@ describe('DAV', () => {
         Destination: `${location.protocol}//${location.hostname}${
           location.port ? `:${location.port}` : ''
         }/moveDestination`,
+        Overwrite: 'F',
       },
     });
   });

+ 1 - 1
tests/unit/DAV/Entry.test.ts

@@ -1,5 +1,5 @@
 import Entry from '../../../src/lib/Entry';
-import trailingSlash from '../../../src/lib/trailingSlash';
+import { trailingSlash } from '../../../src/lib/joinPath';
 
 describe('Entry', () => {
   const directory = new Entry({

+ 18 - 13
translations/en.json

@@ -1,23 +1,28 @@
 {
   "translation": {
-    "pangram": "The quick brown fox jumps over the lazy dog.",
     "alphabet": "Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz",
-    "dropFilesAnywhereToUpload": "Drop files anywhere to upload",
-    "uploadFiles": "upload files",
-    "or": "or",
     "createNewDirectory": "create new directory",
-    "directoryName": "Directory name",
+    "copyItemTitle": "Where do you want to copy '{{file}}' to?",
+    "copy": "Copy",
     "delete": "Delete",
-    "rename": "Rename",
-    "download": "Download",
     "deleteConfirmation": "Are you sure you want to delete '{{file}}'?",
-    "overwriteFileConfirmation": "A file called '{{file}}' already exists, would you like to overwrite it?",
+    "directoryName": "Directory name",
+    "download": "Download",
+    "dropFilesAnywhereToUpload": "Drop files anywhere to upload",
     "failure": "{{method}} {{url}} failed: {{statusText}} ({{status}})",
-    "successfullyUploaded": "'{{file}}' has been successfully uploaded.",
-    "successfullyRenamed": "'{{from}}' successfully renamed to '{{to}}'.",
-    "successfullyMoved": "'{{from}}' successfully moved to '{{to}}'.",
-    "successfullyDeleted": "'{{file}}' has been deleted.",
+    "move": "Move",
+    "moveItemTitle": "Where do you want to move '{{file}}' to?",
+    "or": "or",
+    "overwriteFileConfirmation": "A file called '{{file}}' already exists, would you like to overwrite it?",
+    "pangram": "The quick brown fox jumps over the lazy dog.",
+    "rename": "Rename",
+    "successfullyCopied": "'{{from}}' successfully copied to '{{to}}'.",
     "successfullyCreated": "'{{directoryName}}' has been created.",
-    "title": "{{path}} | WebDAV"
+    "successfullyDeleted": "'{{file}}' has been deleted.",
+    "successfullyMoved": "'{{from}}' successfully moved to '{{to}}'.",
+    "successfullyRenamed": "'{{from}}' successfully renamed to '{{to}}'.",
+    "successfullyUploaded": "'{{file}}' has been successfully uploaded.",
+    "title": "{{path}} | WebDAV",
+    "uploadFiles": "upload files"
   }
 }

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott