NativeDOM.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import Container from './NativeDOM/Container';
  2. import Footer from './NativeDOM/Footer';
  3. import Melba from 'melba-toast';
  4. import UI from './UI';
  5. import i18next from 'i18next';
  6. import trailingSlash from '../trailingSlash';
  7. export default class NativeDOM extends UI {
  8. render(container = new Container(), footer = new Footer()) {
  9. this.container.appendChild(container.element);
  10. this.container.appendChild(footer.element);
  11. this.bindEvents();
  12. this.trigger('go');
  13. }
  14. bindEvents(element = this.container) {
  15. const supportsEvent = (eventName) => {
  16. const element = document.createElement('span');
  17. element.setAttribute(`on${eventName}`, '');
  18. return typeof element[`on${eventName}`] === 'function';
  19. },
  20. isTouch = supportsEvent('touchstart'),
  21. supportsDragDrop = supportsEvent('dragstart') && supportsEvent('drop'),
  22. updateTitle = (title) => {
  23. if (document.title !== title) {
  24. document.title = title;
  25. }
  26. },
  27. updatePath = (path) => {
  28. if (location.pathname !== path) {
  29. history.pushState(history.state, path, path);
  30. }
  31. };
  32. // DOM events
  33. if (isTouch) {
  34. this.container.classList.add('is-touch');
  35. }
  36. if (!supportsDragDrop) {
  37. this.container.classList.add('no-drag-drop');
  38. }
  39. window.addEventListener('popstate', () => {
  40. const url = location.pathname;
  41. element.dispatchEvent(
  42. new CustomEvent('preview:close', {
  43. bubbles: true,
  44. detail: {
  45. preview: true,
  46. },
  47. })
  48. );
  49. if (url.endsWith('/')) {
  50. return this.trigger('go');
  51. }
  52. const path = url.replace(/[^/]+$/, '');
  53. this.trigger('go', path, {
  54. bypassPushState: true,
  55. success: () =>
  56. this.container
  57. .querySelector(`main ul li[data-full-path="${url}"]`)
  58. ?.dispatchEvent(new CustomEvent('click')),
  59. });
  60. // trigger opening file
  61. });
  62. if (supportsDragDrop) {
  63. ['dragenter', 'dragover'].forEach((eventName) => {
  64. element.addEventListener(eventName, (event) => {
  65. event.preventDefault();
  66. event.stopPropagation();
  67. element.classList.add('active');
  68. });
  69. });
  70. ['dragleave', 'drop'].forEach((eventName) => {
  71. element.addEventListener(eventName, (event) => {
  72. event.preventDefault();
  73. event.stopPropagation();
  74. element.classList.remove('active');
  75. });
  76. });
  77. element.addEventListener('drop', async (event) => {
  78. const { files } = event.dataTransfer;
  79. for (const file of files) {
  80. this.trigger('upload', location.pathname, file);
  81. }
  82. });
  83. }
  84. // global listeners
  85. this.on('error', ({ method, url, response }) => {
  86. new Melba({
  87. content: i18next.t('failure', {
  88. interpolation: {
  89. escapeValue: false,
  90. },
  91. method,
  92. url,
  93. statusText: response.statusText,
  94. status: response.status,
  95. }),
  96. type: 'error',
  97. });
  98. });
  99. // local events
  100. this.on('upload', async (path, file) => {
  101. const collection = await this.dav.list(path),
  102. [existingFile] = collection.filter((entry) => entry.name === file.name);
  103. if (existingFile) {
  104. // TODO: nicer notification
  105. // TODO: i18m
  106. if (
  107. !confirm(
  108. i18next.t('overwriteFileConfirmation', {
  109. file: existingFile.title,
  110. })
  111. )
  112. ) {
  113. return false;
  114. }
  115. }
  116. await this.dav.upload(path, file);
  117. });
  118. this.on('upload:success', async (path, file) => {
  119. new Melba({
  120. content: i18next.t('successfullyUploaded', {
  121. interpolation: {
  122. escapeValue: false,
  123. },
  124. file: file.name,
  125. }),
  126. type: 'success',
  127. hide: 5,
  128. });
  129. });
  130. this.on('move', async (source, destination, entry) => {
  131. await this.dav.move(source, destination, entry);
  132. });
  133. this.on('move:success', (source, destination, entry) => {
  134. const [, destinationUrl, destinationFile] =
  135. destination.match(/^(.*)\/([^/]+\/?)$/),
  136. destinationPath =
  137. destinationUrl &&
  138. destinationUrl.replace(
  139. `${location.protocol}//${location.hostname}${
  140. location.port ? `:${location.port}` : ''
  141. }`,
  142. ''
  143. );
  144. if (entry.path === destinationPath || entry.directory) {
  145. return new Melba({
  146. content: i18next.t('successfullyRenamed', {
  147. interpolation: {
  148. escapeValue: false,
  149. },
  150. from: entry.title,
  151. to: decodeURIComponent(destinationFile),
  152. }),
  153. type: 'success',
  154. hide: 5,
  155. });
  156. }
  157. new Melba({
  158. content: i18next.t('successfullyMoved', {
  159. interpolation: {
  160. escapeValue: false,
  161. },
  162. from: entry.title,
  163. to: decodeURIComponent(destinationPath),
  164. }),
  165. type: 'success',
  166. hide: 5,
  167. });
  168. });
  169. this.on('delete', async (path, entry) => {
  170. await this.dav.del(path, entry);
  171. });
  172. this.on('delete:success', (path, entry) => {
  173. new Melba({
  174. content: i18next.t('successfullyDeleted', {
  175. interpolation: {
  176. escapeValue: false,
  177. },
  178. file: entry.title,
  179. }),
  180. type: 'success',
  181. hide: 5,
  182. });
  183. });
  184. this.on('get', async (file, callback) => {
  185. const response = await this.dav.get(file);
  186. callback(response && (await response.text()));
  187. });
  188. this.on('check', async (uri, callback, failure) => {
  189. const response = await this.dav.check(uri);
  190. if (response && response.ok && callback) {
  191. callback(response);
  192. return;
  193. }
  194. if (failure) {
  195. failure();
  196. }
  197. });
  198. this.on('create-directory', async (fullPath, directoryName, path) => {
  199. await this.dav.mkcol(fullPath, directoryName, path);
  200. });
  201. this.on('mkcol:success', (fullPath, directoryName) => {
  202. new Melba({
  203. content: i18next.t('successfullyCreated', {
  204. interpolation: {
  205. escapeValue: false,
  206. },
  207. directoryName,
  208. }),
  209. type: 'success',
  210. hide: 5,
  211. });
  212. });
  213. this.on(
  214. 'go',
  215. async (
  216. path = location.pathname,
  217. {
  218. bypassCache = false,
  219. bypassPushState = false,
  220. failure = null,
  221. success = null,
  222. } = {}
  223. ) => {
  224. const prevPath = location.pathname;
  225. this.trigger('list:update:request', path);
  226. // TODO: store the collection to allow manipulation
  227. const collection = await this.dav.list(path, bypassCache);
  228. if (!collection) {
  229. this.trigger('list:update:failed');
  230. if (failure) {
  231. failure();
  232. }
  233. return;
  234. }
  235. this.trigger('list:update:success', collection);
  236. if (!bypassPushState) {
  237. updatePath(path);
  238. }
  239. updateTitle(`${decodeURIComponent(path)} | WebDAV`);
  240. if (success) {
  241. success(collection);
  242. }
  243. }
  244. );
  245. this.on('preview:opened', (entry) => {
  246. document.body.classList.add('preview-open');
  247. this.container
  248. .querySelector(`[data-full-path="${entry.fullPath}"]`)
  249. ?.focus();
  250. updatePath(entry.fullPath);
  251. updateTitle(`${decodeURIComponent(entry.fullPath)} | WebDAV`);
  252. });
  253. this.on('preview:closed', (entry, { preview = false } = {}) => {
  254. if (preview) {
  255. return;
  256. }
  257. const path = trailingSlash(entry.path);
  258. document.body.classList.remove('preview-open');
  259. updatePath(path);
  260. updateTitle(`${decodeURIComponent(path)} | WebDAV`);
  261. });
  262. }
  263. }