NativeDOM.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import Container from './NativeDOM/Container.js';
  2. import Footer from './NativeDOM/Footer.js';
  3. import Melba from 'melba-toast';
  4. import UI from './UI.js';
  5. export default class NativeDOM extends UI {
  6. render(container = new Container(), footer = new Footer()) {
  7. this.container.appendChild(container.element);
  8. this.container.appendChild(footer.element);
  9. this.bindEvents();
  10. this.trigger('go');
  11. }
  12. bindEvents(element = this.container) {
  13. const supportsEvent = (eventName) => {
  14. const element = document.createElement('span');
  15. element.setAttribute(`on${eventName}`, '');
  16. return typeof element[`on${eventName}`] === 'function';
  17. },
  18. isTouch = supportsEvent('touchstart'),
  19. supportsDragDrop = supportsEvent('dragstart') && supportsEvent('drop')
  20. ;
  21. // DOM events
  22. if (isTouch) {
  23. this.container.classList.add('is-touch');
  24. }
  25. if (! supportsDragDrop) {
  26. this.container.classList.add('no-drag-drop');
  27. }
  28. window.addEventListener('popstate', () => {
  29. this.trigger('go');
  30. });
  31. if (supportsDragDrop) {
  32. ['dragenter', 'dragover'].forEach((eventName) => {
  33. element.addEventListener(eventName, (event) => {
  34. event.preventDefault();
  35. event.stopPropagation();
  36. element.classList.add('active');
  37. });
  38. });
  39. ['dragleave', 'drop'].forEach((eventName) => {
  40. element.addEventListener(eventName, (event) => {
  41. event.preventDefault();
  42. event.stopPropagation();
  43. element.classList.remove('active');
  44. });
  45. });
  46. element.addEventListener('drop', async (event) => {
  47. const {files} = event.dataTransfer;
  48. for (const file of files) {
  49. this.trigger('upload', location.pathname, file);
  50. }
  51. });
  52. }
  53. // global listeners
  54. this.on('error', ({method, url, response}) => {
  55. new Melba({
  56. content: `${method} ${url} failed: ${response.statusText} (${response.status})`,
  57. type: 'error'
  58. });
  59. });
  60. // local events
  61. this.on('upload', async (path, file) => {
  62. const collection = await this.dav.list(path),
  63. [existingFile] = collection.filter((entry) => entry.name === file.name)
  64. ;
  65. if (existingFile) {
  66. // TODO: nicer notification
  67. // TODO: i18m
  68. if (! confirm(`A file called '${existingFile.title}' already exists, would you like to overwrite it?`)) {
  69. return false;
  70. }
  71. }
  72. await this.dav.upload(path, file);
  73. });
  74. this.on('upload:success', async (path, file) => {
  75. new Melba({
  76. content: `'${file.title}' has been successfully uploaded.`,
  77. type: 'success',
  78. hide: 5
  79. });
  80. });
  81. this.on('move', async (source, destination, entry) => {
  82. await this.dav.move(source, destination, entry);
  83. });
  84. this.on('move:success', (source, destination, entry) => {
  85. const [, destinationUrl, destinationFile] = destination.match(/^(.*)\/([^/]+\/?)$/),
  86. destinationPath = destinationUrl && destinationUrl.replace(
  87. `${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ''}`,
  88. ''
  89. )
  90. ;
  91. if (entry.path === destinationPath) {
  92. return new Melba({
  93. content: `'${entry.title}' successfully renamed to '${decodeURIComponent(destinationFile)}'.`,
  94. type: 'success',
  95. hide: 5
  96. });
  97. }
  98. new Melba({
  99. content: `'${entry.title}' successfully moved to '${decodeURIComponent(destinationPath)}'.`,
  100. type: 'success',
  101. hide: 5
  102. });
  103. });
  104. this.on('delete', async (path, entry) => {
  105. await this.dav.del(path, entry);
  106. });
  107. this.on('delete:success', (path, entry) => {
  108. new Melba({
  109. content: `'${entry.title}' has been deleted.`,
  110. type: 'success',
  111. hide: 5
  112. });
  113. });
  114. this.on('get', async (file, callback) => {
  115. const response = await this.dav.get(file);
  116. callback(response && await response.text());
  117. });
  118. this.on('check', async (uri, callback, failure) => {
  119. const response = await this.dav.check(uri);
  120. if (response && response.ok && callback) {
  121. callback(response);
  122. return;
  123. }
  124. if (failure) {
  125. failure();
  126. }
  127. });
  128. this.on('create-directory', async (fullPath, directoryName, path) => {
  129. await this.dav.mkcol(fullPath, directoryName, path);
  130. });
  131. this.on('mkcol:success', (fullPath, directoryName) => {
  132. new Melba({
  133. content: `'${directoryName}' has been created.`,
  134. type: 'success',
  135. hide: 5
  136. });
  137. });
  138. this.on('go', async (path = location.pathname, bypassCache = false, failure = null) => {
  139. const prevPath = location.pathname;
  140. this.trigger('list:update:request', path);
  141. // TODO: store the collection to allow manipulation
  142. const collection = await this.dav.list(path, bypassCache);
  143. if (! collection) {
  144. this.trigger('list:update:failed');
  145. if (failure) {
  146. failure();
  147. }
  148. return;
  149. }
  150. this.trigger('list:update:success', collection);
  151. if (path !== prevPath) {
  152. history.pushState(history.state, path, path);
  153. }
  154. document.title = `${decodeURIComponent(path)} | WebDAV`;
  155. });
  156. }
  157. }