listing.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. IContentsModel
  6. } from 'jupyter-js-services';
  7. import * as moment
  8. from 'moment';
  9. import * as arrays
  10. from 'phosphor-arrays';
  11. import {
  12. IDisposable
  13. } from 'phosphor-disposable';
  14. import {
  15. Drag, DropAction, DropActions, IDragEvent, MimeData
  16. } from 'phosphor-dragdrop';
  17. import {
  18. Message
  19. } from 'phosphor-messaging';
  20. import {
  21. Widget
  22. } from 'phosphor-widget';
  23. import {
  24. showDialog
  25. } from '../dialog';
  26. import {
  27. FileBrowserModel
  28. } from './model';
  29. import * as utils
  30. from './utils';
  31. import {
  32. SELECTED_CLASS
  33. } from './utils';
  34. /**
  35. * The class name added to DirListing widget.
  36. */
  37. const DIR_LISTING_CLASS = 'jp-DirListing';
  38. /**
  39. * The class name added to a dir listing header node.
  40. */
  41. const HEADER_CLASS = 'jp-DirListing-header';
  42. /**
  43. * The class name added to a dir listing list header cell.
  44. */
  45. const HEADER_ITEM_CLASS = 'jp-DirListing-headerItem';
  46. /**
  47. * The class name added to a header cell text node.
  48. */
  49. const HEADER_ITEM_TEXT_CLASS = 'jp-DirListing-headerItemText';
  50. /**
  51. * The class name added to a header cell icon node.
  52. */
  53. const HEADER_ITEM_ICON_CLASS = 'jp-DirListing-headerItemIcon';
  54. /**
  55. * The class name added to the dir listing content node.
  56. */
  57. const CONTENT_CLASS = 'jp-DirListing-content';
  58. /**
  59. * The class name added to dir listing content item.
  60. */
  61. const ITEM_CLASS = 'jp-DirListing-item';
  62. /**
  63. * The class name added to the listing item text cell.
  64. */
  65. const ITEM_TEXT_CLASS = 'jp-DirListing-itemText';
  66. /**
  67. * The class name added to the listing item icon cell.
  68. */
  69. const ITEM_ICON_CLASS = 'jp-DirListing-itemIcon';
  70. /**
  71. * The class name added to the listing item modified cell.
  72. */
  73. const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
  74. /**
  75. * The class name added to the dir listing editor node.
  76. */
  77. const EDITOR_CLASS = 'jp-DirListing-editor';
  78. /**
  79. * The class name added to the name column header cell.
  80. */
  81. const NAME_ID_CLASS = 'jp-id-name';
  82. /**
  83. * The class name added to the modified column header cell.
  84. */
  85. const MODIFIED_ID_CLASS = 'jp-id-modified';
  86. /**
  87. * The class name added to a file type content item.
  88. */
  89. const FILE_TYPE_CLASS = 'jp-type-file';
  90. /**
  91. * The class name added to a folder type content item.
  92. */
  93. const FOLDER_TYPE_CLASS = 'jp-type-folder';
  94. /**
  95. * The class name added to a notebook type content item.
  96. */
  97. const NOTEBOOK_TYPE_CLASS = 'jp-type-notebook';
  98. /**
  99. * The class name added to the widget when there are items on the clipboard.
  100. */
  101. const CLIPBOARD_CLASS = 'jp-mod-clipboard';
  102. /**
  103. * The class name added to cut rows.
  104. */
  105. const CUT_CLASS = 'jp-mod-cut';
  106. /**
  107. * The class name added when there are more than one selected rows.
  108. */
  109. const MULTI_SELECTED_CLASS = 'jp-mod-multiSelected';
  110. /**
  111. * The class name added to indicate running notebook.
  112. */
  113. const RUNNING_CLASS = 'jp-mod-running';
  114. /**
  115. * The class name added for a decending sort.
  116. */
  117. const DESCENDING_CLASS = 'jp-mod-descending';
  118. /**
  119. * The minimum duration for a rename select in ms.
  120. */
  121. const RENAME_DURATION = 500;
  122. /**
  123. * The threshold in pixels to start a drag event.
  124. */
  125. const DRAG_THRESHOLD = 5;
  126. /**
  127. * The factory MIME type supported by phosphor dock panels.
  128. */
  129. const FACTORY_MIME = 'application/x-phosphor-widget-factory';
  130. /**
  131. * A widget which hosts a file list area.
  132. */
  133. export
  134. class DirListing extends Widget {
  135. /**
  136. * Create the DOM node for a dir listing.
  137. */
  138. static createNode(): HTMLElement {
  139. let node = document.createElement('div');
  140. let content = document.createElement('ul');
  141. let header = this.createHeaderNode();
  142. content.className = CONTENT_CLASS;
  143. node.appendChild(header);
  144. node.appendChild(content);
  145. node.tabIndex = 1;
  146. return node;
  147. }
  148. /**
  149. * Create the header node for a dir listing.
  150. *
  151. * @returns A new DOM node to use as the dir listing header.
  152. *
  153. * #### Notes
  154. * This method may be reimplemented to create custom headers.
  155. */
  156. static createHeaderNode(): HTMLElement {
  157. let node = document.createElement('div');
  158. let name = createItemNode('Name');
  159. let modified = createItemNode('Last Modified');
  160. node.className = HEADER_CLASS;
  161. name.classList.add(NAME_ID_CLASS);
  162. name.classList.add(SELECTED_CLASS);
  163. modified.classList.add(MODIFIED_ID_CLASS);
  164. node.appendChild(name);
  165. node.appendChild(modified);
  166. return node;
  167. function createItemNode(label: string): HTMLElement {
  168. let node = document.createElement('div');
  169. let text = document.createElement('span');
  170. let icon = document.createElement('span');
  171. node.className = HEADER_ITEM_CLASS;
  172. text.className = HEADER_ITEM_TEXT_CLASS;
  173. icon.className = HEADER_ITEM_ICON_CLASS;
  174. text.textContent = label;
  175. node.appendChild(text);
  176. node.appendChild(icon);
  177. return node;
  178. }
  179. }
  180. /**
  181. * Create a new item node for a dir listing.
  182. *
  183. * @returns A new DOM node to use as a content item.
  184. *
  185. * #### Notes
  186. * This method may be reimplemented to create custom items.
  187. */
  188. static createItemNode(): HTMLElement {
  189. let node = document.createElement('li');
  190. let icon = document.createElement('span');
  191. let text = document.createElement('span');
  192. let modified = document.createElement('span');
  193. node.className = ITEM_CLASS;
  194. icon.className = ITEM_ICON_CLASS;
  195. text.className = ITEM_TEXT_CLASS;
  196. modified.className = ITEM_MODIFIED_CLASS;
  197. node.appendChild(icon);
  198. node.appendChild(text);
  199. node.appendChild(modified);
  200. return node;
  201. }
  202. /**
  203. * Update an item node to reflect the current state of a model.
  204. *
  205. * @param node - A node created by a call to [[createItemNode]].
  206. *
  207. * @param model - The model object to use for the item state.
  208. *
  209. * #### Notes
  210. * This is called automatically when the item should be updated.
  211. *
  212. * If the [[createItemNode]] method is reimplemented, this method
  213. * should also be reimplemented so that the item state is properly
  214. * updated.
  215. */
  216. static updateItemNode(node: HTMLElement, model: IContentsModel) {
  217. let icon = node.firstChild as HTMLElement;
  218. let text = icon.nextSibling as HTMLElement;
  219. let modified = text.nextSibling as HTMLElement;
  220. let type: string;
  221. switch (model.type) {
  222. case 'directory':
  223. type = FOLDER_TYPE_CLASS;
  224. break;
  225. case 'notebook':
  226. type = NOTEBOOK_TYPE_CLASS;
  227. break;
  228. default:
  229. type = FILE_TYPE_CLASS;
  230. break;
  231. }
  232. let modText = '';
  233. let modTitle = '';
  234. if (model.last_modified) {
  235. let time = moment(model.last_modified).fromNow();
  236. modText = time === 'a few seconds ago' ? 'seconds ago' : time;
  237. modTitle = moment(model.last_modified).format("YYYY-MM-DD HH:mm");
  238. }
  239. node.className = `${ITEM_CLASS} ${type}`;
  240. text.textContent = model.name;
  241. modified.textContent = modText;
  242. modified.title = modTitle;
  243. }
  244. /**
  245. * Construct a new file browser directory listing widget.
  246. *
  247. * @param model - The file browser view model.
  248. */
  249. constructor(model: FileBrowserModel) {
  250. super();
  251. this.addClass(DIR_LISTING_CLASS);
  252. this._model = model;
  253. this._model.refreshed.connect(this._onModelRefreshed, this);
  254. this._model.selectionChanged.connect(this._onSelectionChanged, this);
  255. this._editNode = document.createElement('input');
  256. this._editNode.className = EDITOR_CLASS;
  257. }
  258. /**
  259. * Dispose of the resources held by the directory listing.
  260. */
  261. dispose(): void {
  262. this._model = null;
  263. this._items = null;
  264. this._editNode = null;
  265. this._drag = null;
  266. this._dragData = null;
  267. super.dispose();
  268. }
  269. /**
  270. * Get the widget factory for the widget.
  271. */
  272. get widgetFactory(): (model: IContentsModel) => Widget {
  273. return this._widgetFactory;
  274. }
  275. /**
  276. * Set the widget factory for the widget.
  277. */
  278. set widgetFactory(factory: (model: IContentsModel) => Widget) {
  279. this._widgetFactory = factory;
  280. }
  281. /**
  282. * Get the dir listing header node.
  283. *
  284. * #### Notes
  285. * This is the node which holds the header cells.
  286. *
  287. * Modifying this node directly can lead to undefined behavior.
  288. *
  289. * This is a read-only property.
  290. */
  291. get headerNode(): HTMLElement {
  292. return utils.findElement(this.node, HEADER_CLASS);
  293. }
  294. /**
  295. * Get the dir listing content node.
  296. *
  297. * #### Notes
  298. * This is the node which holds the item nodes.
  299. *
  300. * Modifying this node directly can lead to undefined behavior.
  301. *
  302. * This is a read-only property.
  303. */
  304. get contentNode(): HTMLElement {
  305. return utils.findElement(this.node, CONTENT_CLASS);
  306. }
  307. /**
  308. * Rename the first currently selected item.
  309. */
  310. rename(): Promise<string> {
  311. return this._doRename();
  312. }
  313. /**
  314. * Cut the selected items.
  315. */
  316. cut(): void {
  317. this._isCut = true;
  318. this._copy();
  319. }
  320. /**
  321. * Copy the selected items.
  322. */
  323. copy(): void {
  324. this._copy();
  325. }
  326. /**
  327. * Paste the items from the clipboard.
  328. */
  329. paste(): Promise<void> {
  330. if (!this._clipboard.length) {
  331. return;
  332. }
  333. let promises: Promise<IContentsModel>[] = [];
  334. for (let path of this._clipboard) {
  335. if (this._isCut) {
  336. let parts = path.split('/');
  337. let name = parts[parts.length - 1];
  338. promises.push(this._model.rename(path, name));
  339. } else {
  340. promises.push(this._model.copy(path, '.'));
  341. }
  342. }
  343. // Remove any cut modifiers.
  344. for (let item of this._items) {
  345. item.classList.remove(CUT_CLASS);
  346. }
  347. this._clipboard = [];
  348. this._isCut = false;
  349. this.removeClass(CLIPBOARD_CLASS);
  350. return Promise.all(promises).then(
  351. () => this._model.refresh(),
  352. error => utils.showErrorMessage(this, 'Paste Error', error)
  353. );
  354. }
  355. /**
  356. * Delete the currently selected item(s).
  357. */
  358. delete(): Promise<void> {
  359. let promises: Promise<void>[] = [];
  360. let items = this._model.sortedItems;
  361. if (this._softSelection) {
  362. promises.push(this._model.delete(this._softSelection));
  363. } else {
  364. for (let item of items) {
  365. if (this._model.isSelected(item.name)) {
  366. promises.push(this._model.delete(item.name));
  367. }
  368. }
  369. }
  370. return Promise.all(promises).then(
  371. () => this._model.refresh(),
  372. error => utils.showErrorMessage(this, 'Delete file', error)
  373. );
  374. }
  375. /**
  376. * Duplicate the currently selected item(s).
  377. */
  378. duplicate(): Promise<void> {
  379. let promises: Promise<IContentsModel>[] = [];
  380. for (let item of this._getSelectedItems()) {
  381. if (item.type !== 'directory') {
  382. promises.push(this._model.copy(item.path, this._model.path));
  383. }
  384. }
  385. return Promise.all(promises).then(
  386. () => this._model.refresh(),
  387. error => utils.showErrorMessage(this, 'Duplicate file', error)
  388. );
  389. }
  390. /**
  391. * Download the currently selected item(s).
  392. */
  393. download(): Promise<void> {
  394. for (let item of this._getSelectedItems()) {
  395. if (item.type !== 'directory') {
  396. return this._model.download(item.path).catch(error =>
  397. utils.showErrorMessage(this, 'Download file', error)
  398. );
  399. }
  400. }
  401. }
  402. /**
  403. * Shut down kernels on the applicable currently selected items.
  404. */
  405. shutdownKernels(): Promise<void> {
  406. let promises: Promise<void>[] = [];
  407. let items = this._model.sortedItems;
  408. let paths = items.map(item => item.path);
  409. for (let sessionId of this._model.sessionIds) {
  410. let index = paths.indexOf(sessionId.notebook.path);
  411. if (!this._softSelection && this._model.isSelected(items[index].name)) {
  412. promises.push(this._model.shutdown(sessionId));
  413. } else if (this._softSelection === items[index].name) {
  414. promises.push(this._model.shutdown(sessionId));
  415. }
  416. }
  417. return Promise.all(promises).then(
  418. () => this._model.refresh(),
  419. error => utils.showErrorMessage(this, 'Shutdown kernel', error)
  420. );
  421. }
  422. /**
  423. * Select next item.
  424. *
  425. * @param keepExisting - Whether to keep the current selection and add to it.
  426. */
  427. selectNext(keepExisting = false): void {
  428. let index = -1;
  429. let selected = this._model.getSelected();
  430. let items = this._model.sortedItems;
  431. if (selected.length === 1 || keepExisting) {
  432. // Select the next item.
  433. let name = selected[selected.length - 1];
  434. index = arrays.findIndex(items, (value, index) => value.name === name);
  435. index += 1;
  436. if (index === this._items.length) index = 0;
  437. } else if (selected.length === 0) {
  438. // Select the first item.
  439. index = 0;
  440. } else {
  441. // Select the last selected item.
  442. let name = selected[selected.length - 1];
  443. index = arrays.findIndex(items, (value, index) => value.name === name);
  444. }
  445. if (index !== -1) this._selectItem(index, keepExisting);
  446. }
  447. /**
  448. * Select previous item.
  449. *
  450. * @param keepExisting - Whether to keep the current selection and add to it.
  451. */
  452. selectPrevious(keepExisting = false): void {
  453. let index = -1;
  454. let selected = this._model.getSelected();
  455. let items = this._model.sortedItems;
  456. if (selected.length === 1 || keepExisting) {
  457. // Select the previous item.
  458. let name = selected[0];
  459. index = arrays.findIndex(items, (value, index) => value.name === name);
  460. index -= 1;
  461. if (index === -1) index = this._items.length - 1;
  462. } else if (selected.length === 0) {
  463. // Select the last item.
  464. index = this._items.length - 1;
  465. } else {
  466. // Select the first selected item.
  467. let name = selected[0];
  468. index = arrays.findIndex(items, (value, index) => value.name === name);
  469. }
  470. if (index !== -1) this._selectItem(index, keepExisting);
  471. }
  472. /**
  473. * Handle the DOM events for the directory listing.
  474. *
  475. * @param event - The DOM event sent to the widget.
  476. *
  477. * #### Notes
  478. * This method implements the DOM `EventListener` interface and is
  479. * called in response to events on the panel's DOM node. It should
  480. * not be called directly by user code.
  481. */
  482. handleEvent(event: Event): void {
  483. switch (event.type) {
  484. case 'mousedown':
  485. this._evtMousedown(event as MouseEvent);
  486. break;
  487. case 'mouseup':
  488. this._evtMouseup(event as MouseEvent);
  489. break;
  490. case 'mousemove':
  491. this._evtMousemove(event as MouseEvent);
  492. break;
  493. case 'keydown':
  494. this._evtKeydown(event as KeyboardEvent);
  495. break;
  496. case 'click':
  497. this._evtClick(event as MouseEvent);
  498. break
  499. case 'dblclick':
  500. this._evtDblClick(event as MouseEvent);
  501. break;
  502. case 'scroll':
  503. this._evtScroll(event as MouseEvent);
  504. break;
  505. case 'p-dragenter':
  506. this._evtDragEnter(event as IDragEvent);
  507. break;
  508. case 'p-dragleave':
  509. this._evtDragLeave(event as IDragEvent);
  510. break;
  511. case 'p-dragover':
  512. this._evtDragOver(event as IDragEvent);
  513. break;
  514. case 'p-drop':
  515. this._evtDrop(event as IDragEvent);
  516. break;
  517. }
  518. }
  519. /**
  520. * A message handler invoked on an `'after-attach'` message.
  521. */
  522. protected onAfterAttach(msg: Message): void {
  523. super.onAfterAttach(msg);
  524. let node = this.node;
  525. let content = utils.findElement(node, CONTENT_CLASS);
  526. node.addEventListener('mousedown', this);
  527. node.addEventListener('keydown', this);
  528. node.addEventListener('click', this);
  529. node.addEventListener('dblclick', this);
  530. content.addEventListener('scroll', this);
  531. content.addEventListener('p-dragenter', this);
  532. content.addEventListener('p-dragleave', this);
  533. content.addEventListener('p-dragover', this);
  534. content.addEventListener('p-drop', this);
  535. }
  536. /**
  537. * A message handler invoked on a `'before-detach'` message.
  538. */
  539. protected onBeforeDetach(msg: Message): void {
  540. super.onBeforeDetach(msg);
  541. let node = this.node;
  542. let content = utils.findElement(node, CONTENT_CLASS);
  543. node.removeEventListener('mousedown', this);
  544. node.removeEventListener('keydown', this);
  545. node.removeEventListener('click', this);
  546. node.removeEventListener('dblclick', this);
  547. content.removeEventListener('scroll', this);
  548. content.removeEventListener('p-dragenter', this);
  549. content.removeEventListener('p-dragleave', this);
  550. content.removeEventListener('p-dragover', this);
  551. content.removeEventListener('p-drop', this);
  552. document.removeEventListener('mousemove', this, true);
  553. document.removeEventListener('mouseup', this, true);
  554. }
  555. /**
  556. * A handler invoked on an `'update-request'` message.
  557. */
  558. protected onUpdateRequest(msg: Message): void {
  559. // Fetch common variables.
  560. let items = this._model.sortedItems;
  561. let nodes = this._items;
  562. let content = utils.findElement(this.node, CONTENT_CLASS);
  563. let subtype = this.constructor as typeof DirListing;
  564. this.removeClass(MULTI_SELECTED_CLASS);
  565. this.removeClass(SELECTED_CLASS);
  566. // Remove any excess item nodes.
  567. while (nodes.length > items.length) {
  568. let node = nodes.pop();
  569. content.removeChild(node);
  570. }
  571. // Add any missing item nodes.
  572. while (nodes.length < items.length) {
  573. let node = subtype.createItemNode();
  574. nodes.push(node);
  575. content.appendChild(node);
  576. }
  577. // Update the node states to match the model contents.
  578. for (let i = 0, n = items.length; i < n; ++i) {
  579. subtype.updateItemNode(nodes[i], items[i]);
  580. if (this._model.isSelected(items[i].name)) {
  581. nodes[i].classList.add(SELECTED_CLASS);
  582. if (this._isCut && this._model.path === this._prevPath) {
  583. nodes[i].classList.add(CUT_CLASS);
  584. }
  585. }
  586. }
  587. // Handle the selectors on the widget node.
  588. let selectedNames = this._model.getSelected();
  589. if (selectedNames.length > 1) {
  590. this.addClass(MULTI_SELECTED_CLASS);
  591. }
  592. if (selectedNames.length) {
  593. this.addClass(SELECTED_CLASS);
  594. }
  595. // Handle notebook session statuses.
  596. let paths = items.map(item => item.path);
  597. for (let sessionId of this._model.sessionIds) {
  598. let index = paths.indexOf(sessionId.notebook.path);
  599. let node = this._items[index];
  600. node.classList.add(RUNNING_CLASS);
  601. node.title = sessionId.kernel.name;
  602. }
  603. this._prevPath = this._model.path;
  604. }
  605. /**
  606. * Handle the `'click'` event for the widget.
  607. */
  608. private _evtClick(event: MouseEvent) {
  609. this._softSelection = '';
  610. let target = event.target as HTMLElement;
  611. let header = this.headerNode;
  612. if (header.contains(target)) {
  613. let children = header.getElementsByClassName(HEADER_ITEM_CLASS);
  614. let name = children[0] as HTMLElement;
  615. let modified = children[1] as HTMLElement;
  616. if (name.contains(target)) {
  617. if (this._model.sortKey === 'name') {
  618. let flag = !this._model.sortAscending;
  619. this._model.sortAscending = flag;
  620. if (flag) name.classList.remove(DESCENDING_CLASS);
  621. else name.classList.add(DESCENDING_CLASS);
  622. } else {
  623. this._model.sortKey = 'name';
  624. this._model.sortAscending = true;
  625. name.classList.remove(DESCENDING_CLASS);
  626. }
  627. name.classList.add(SELECTED_CLASS);
  628. modified.classList.remove(SELECTED_CLASS);
  629. modified.classList.remove(DESCENDING_CLASS);
  630. } else if (modified.contains(target)) {
  631. if (this._model.sortKey === 'last_modified') {
  632. let flag = !this._model.sortAscending;
  633. this._model.sortAscending = flag;
  634. if (flag) modified.classList.remove(DESCENDING_CLASS);
  635. else modified.classList.add(DESCENDING_CLASS);
  636. } else {
  637. this._model.sortKey = 'last_modified';
  638. this._model.sortAscending = true;
  639. modified.classList.remove(DESCENDING_CLASS);
  640. }
  641. modified.classList.add(SELECTED_CLASS);
  642. name.classList.remove(SELECTED_CLASS);
  643. name.classList.remove(DESCENDING_CLASS);
  644. }
  645. this.update();
  646. return;
  647. }
  648. // Bail if editing.
  649. if (this._editNode.contains(target)) {
  650. return;
  651. }
  652. let content = this.contentNode;
  653. if (content.contains(target)) {
  654. this._handleFileSelect(event);
  655. }
  656. }
  657. /**
  658. * Handle the `'scroll'` event for the widget.
  659. */
  660. private _evtScroll(event: MouseEvent): void {
  661. this.headerNode.scrollLeft = this.contentNode.scrollLeft;
  662. }
  663. /**
  664. * Handle the `'mousedown'` event for the widget.
  665. */
  666. private _evtMousedown(event: MouseEvent): void {
  667. // Bail if clicking within the edit node
  668. if (event.target === this._editNode) {
  669. return;
  670. }
  671. // Blur the edit node if necessary.
  672. if (this._editNode.parentNode) {
  673. if (this._editNode !== event.target as HTMLElement) {
  674. this._editNode.focus();
  675. this._editNode.blur();
  676. clearTimeout(this._selectTimer);
  677. } else {
  678. return;
  679. }
  680. }
  681. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  682. if (index == -1) {
  683. return;
  684. }
  685. this._softSelection = '';
  686. let items = this._model.sortedItems;
  687. let selected = this._model.getSelected();
  688. if (selected.indexOf(items[index].name) == -1) {
  689. this._softSelection = items[index].name;
  690. }
  691. // Left mouse press for drag start.
  692. if (event.button === 0) {
  693. this._dragData = { pressX: event.clientX, pressY: event.clientY,
  694. index: index };
  695. document.addEventListener('mouseup', this, true);
  696. document.addEventListener('mousemove', this, true);
  697. }
  698. if (event.button !== 0) {
  699. clearTimeout(this._selectTimer);
  700. }
  701. }
  702. /**
  703. * Handle the `'mouseup'` event for the widget.
  704. */
  705. private _evtMouseup(event: MouseEvent): void {
  706. if (event.button !== 0 || !this._drag) {
  707. document.removeEventListener('mousemove', this, true);
  708. document.removeEventListener('mouseup', this, true);
  709. return;
  710. }
  711. event.preventDefault();
  712. event.stopPropagation();
  713. }
  714. /**
  715. * Handle the `'mousemove'` event for the widget.
  716. */
  717. private _evtMousemove(event: MouseEvent): void {
  718. event.preventDefault();
  719. event.stopPropagation();
  720. // Bail if we are the one dragging.
  721. if (this._drag) {
  722. return;
  723. }
  724. // Check for a drag initialization.
  725. let data = this._dragData;
  726. let dx = Math.abs(event.clientX - data.pressX);
  727. let dy = Math.abs(event.clientY - data.pressY);
  728. if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
  729. return;
  730. }
  731. this._startDrag(data.index, event.clientX, event.clientY);
  732. }
  733. /**
  734. * Handle the `'keydown'` event for the widget.
  735. */
  736. private _evtKeydown(event: KeyboardEvent): void {
  737. switch (event.keyCode) {
  738. case 38: // Up arrow
  739. this.selectPrevious(event.shiftKey);
  740. event.stopPropagation();
  741. event.preventDefault();
  742. break;
  743. case 40: // Down arrow
  744. this.selectNext(event.shiftKey);
  745. event.stopPropagation();
  746. event.preventDefault();
  747. break;
  748. }
  749. }
  750. /**
  751. * Handle the `'dblclick'` event for the widget.
  752. */
  753. private _evtDblClick(event: MouseEvent): void {
  754. // Do nothing if it's not a left mouse press.
  755. if (event.button !== 0) {
  756. return;
  757. }
  758. // Do nothing if any modifier keys are pressed.
  759. if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
  760. return;
  761. }
  762. // Stop the event propagation.
  763. event.preventDefault();
  764. event.stopPropagation();
  765. clearTimeout(this._selectTimer);
  766. this._noSelectTimer = setTimeout(() => {
  767. this._noSelectTimer = -1;
  768. }, RENAME_DURATION);
  769. this._editNode.blur();
  770. // Find a valid double click target.
  771. let target = event.target as HTMLElement;
  772. let i = arrays.findIndex(this._items, node => node.contains(target));
  773. if (i === -1) {
  774. return;
  775. }
  776. let item = this._model.sortedItems[i];
  777. this._model.open(item.name).catch(error =>
  778. utils.showErrorMessage(this, 'File Open Error', error)
  779. );
  780. }
  781. /**
  782. * Handle the `'p-dragenter'` event for the widget.
  783. */
  784. private _evtDragEnter(event: IDragEvent): void {
  785. if (event.mimeData.hasData(utils.CONTENTS_MIME)) {
  786. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  787. if (index === -1) {
  788. return;
  789. }
  790. let target = this._items[index];
  791. if (!target.classList.contains(FOLDER_TYPE_CLASS)) {
  792. return;
  793. }
  794. if (target.classList.contains(SELECTED_CLASS)) {
  795. return;
  796. }
  797. target.classList.add(utils.DROP_TARGET_CLASS);
  798. event.preventDefault();
  799. event.stopPropagation();
  800. }
  801. }
  802. /**
  803. * Handle the `'p-dragleave'` event for the widget.
  804. */
  805. private _evtDragLeave(event: IDragEvent): void {
  806. event.preventDefault();
  807. event.stopPropagation();
  808. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  809. if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  810. }
  811. /**
  812. * Handle the `'p-dragover'` event for the widget.
  813. */
  814. private _evtDragOver(event: IDragEvent): void {
  815. event.preventDefault();
  816. event.stopPropagation();
  817. event.dropAction = event.proposedAction;
  818. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  819. if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  820. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  821. this._items[index].classList.add(utils.DROP_TARGET_CLASS);
  822. }
  823. /**
  824. * Handle the `'p-drop'` event for the widget.
  825. */
  826. private _evtDrop(event: IDragEvent): void {
  827. event.preventDefault();
  828. event.stopPropagation();
  829. if (event.proposedAction === DropAction.None) {
  830. event.dropAction = DropAction.None;
  831. return;
  832. }
  833. if (!event.mimeData.hasData(utils.CONTENTS_MIME)) {
  834. return;
  835. }
  836. event.dropAction = event.proposedAction;
  837. let target = event.target as HTMLElement;
  838. while (target && target.parentElement) {
  839. if (target.classList.contains(utils.DROP_TARGET_CLASS)) {
  840. target.classList.remove(utils.DROP_TARGET_CLASS);
  841. break;
  842. }
  843. target = target.parentElement;
  844. }
  845. // Get the path based on the target node.
  846. let index = this._items.indexOf(target);
  847. let items = this._model.sortedItems;
  848. var path = items[index].name + '/';
  849. // Move all of the items.
  850. let promises: Promise<IContentsModel>[] = [];
  851. for (let item of items) {
  852. if (!this._softSelection && !this._model.isSelected(item.name)) {
  853. continue;
  854. }
  855. if (this._softSelection !== item.name) {
  856. continue;
  857. }
  858. var name = item.name;
  859. var newPath = path + name;
  860. promises.push(this._model.rename(name, newPath).catch(error => {
  861. if (error.xhr) {
  862. error.message = `${error.xhr.statusText} ${error.xhr.status}`;
  863. }
  864. if (error.message.indexOf('409') !== -1) {
  865. let options = {
  866. title: 'Overwrite file?',
  867. host: this.parent.node,
  868. body: `"${newPath}" already exists, overwrite?`
  869. }
  870. return showDialog(options).then(button => {
  871. if (button.text === 'OK') {
  872. return this._model.delete(newPath).then(() => {
  873. return this._model.rename(name, newPath);
  874. });
  875. }
  876. });
  877. }
  878. }));
  879. }
  880. Promise.all(promises).then(
  881. () => this._model.refresh(),
  882. error => utils.showErrorMessage(this, 'Move Error', error)
  883. );
  884. }
  885. /**
  886. * Start a drag event.
  887. */
  888. private _startDrag(index: number, clientX: number, clientY: number): void {
  889. let selectedNames = this._model.getSelected();
  890. let source = this._items[index];
  891. let items = this._model.sortedItems;
  892. let item: IContentsModel = null;
  893. // If the source node is not selected, use just that node.
  894. if (!source.classList.contains(SELECTED_CLASS)) {
  895. item = items[index];
  896. selectedNames = [item.name];
  897. } else if (selectedNames.length === 1) {
  898. let name = selectedNames[0];
  899. item = arrays.find(items, (value, index) => value.name === name);
  900. }
  901. // Create the drag image.
  902. var dragImage = source.cloneNode(true) as HTMLElement;
  903. dragImage.removeChild(dragImage.lastChild);
  904. if (selectedNames.length > 1) {
  905. let text = utils.findElement(dragImage, ITEM_TEXT_CLASS);
  906. text.textContent = '(' + selectedNames.length + ')'
  907. }
  908. // Set up the drag event.
  909. this._drag = new Drag({
  910. dragImage: dragImage,
  911. mimeData: new MimeData(),
  912. supportedActions: DropActions.Move,
  913. proposedAction: DropAction.Move
  914. });
  915. this._drag.mimeData.setData(utils.CONTENTS_MIME, null);
  916. if (this._widgetFactory && item && item.type !== 'directory') {
  917. this._drag.mimeData.setData(FACTORY_MIME, () => {
  918. return this._widgetFactory(item);
  919. });
  920. }
  921. // Start the drag and remove the mousemove listener.
  922. this._drag.start(clientX, clientY).then(action => {
  923. console.log('action', action);
  924. this._drag = null;
  925. });
  926. document.removeEventListener('mousemove', this, true);
  927. }
  928. /**
  929. * Handle selection on a file node.
  930. */
  931. private _handleFileSelect(event: MouseEvent): void {
  932. // Fetch common variables.
  933. let items = this._model.sortedItems;
  934. let nodes = this._items;
  935. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  936. clearTimeout(this._selectTimer);
  937. let name = items[index].name;
  938. let selected = this._model.getSelected();
  939. // Handle toggling.
  940. if (event.metaKey || event.ctrlKey) {
  941. if (this._model.isSelected(name)) {
  942. this._model.deselect(name);
  943. } else {
  944. this._model.select(name);
  945. }
  946. // Handle multiple select.
  947. } else if (event.shiftKey) {
  948. this._handleMultiSelect(selected, index);
  949. // Default to selecting the only the item.
  950. } else {
  951. // Handle a rename.
  952. if (selected.length === 1 && selected[0] === name) {
  953. this._selectTimer = setTimeout(() => {
  954. if (this._noSelectTimer === -1) {
  955. this._doRename();
  956. }
  957. }, RENAME_DURATION);
  958. }
  959. this._model.clearSelected();
  960. this._model.select(name);
  961. }
  962. this._isCut = false;
  963. this.update();
  964. }
  965. /**
  966. * Handle a multiple select on a file item node.
  967. */
  968. private _handleMultiSelect(selected: string[], index: number): void {
  969. // Find the "nearest selected".
  970. let items = this._model.sortedItems;
  971. let nearestIndex = -1;
  972. for (let i = 0; i < this._items.length; i++) {
  973. if (i === index) {
  974. continue;
  975. }
  976. let name = items[i].name;
  977. if (selected.indexOf(name) !== -1) {
  978. if (nearestIndex === -1) {
  979. nearestIndex = i;
  980. } else {
  981. if (Math.abs(index - i) < Math.abs(nearestIndex - i)) {
  982. nearestIndex = i;
  983. }
  984. }
  985. }
  986. }
  987. // Default to the first element (and fill down).
  988. if (nearestIndex === -1) {
  989. nearestIndex = 0;
  990. }
  991. // Select the rows between the current and the nearest selected.
  992. for (let i = 0; i < this._items.length; i++) {
  993. if (nearestIndex >= i && index <= i ||
  994. nearestIndex <= i && index >= i) {
  995. this._model.select(items[i].name);
  996. }
  997. }
  998. }
  999. /**
  1000. * Get the currently selected items.
  1001. */
  1002. private _getSelectedItems(): IContentsModel[] {
  1003. let items = this._model.sortedItems;
  1004. if (!this._softSelection) {
  1005. return items.filter(item => this._model.isSelected(item.name));
  1006. }
  1007. return items.filter(item => item.name === this._softSelection);
  1008. }
  1009. /**
  1010. * Copy the selected items, and optionally cut as well.
  1011. */
  1012. private _copy(): void {
  1013. this._clipboard = []
  1014. for (var item of this._getSelectedItems()) {
  1015. let row = arrays.find(this._items, row => {
  1016. let text = utils.findElement(row, ITEM_TEXT_CLASS);
  1017. return text.textContent === item.name;
  1018. });
  1019. if (item.type !== 'directory') {
  1020. // Store the absolute path of the item.
  1021. this._clipboard.push('/' + item.path)
  1022. }
  1023. }
  1024. this.update();
  1025. }
  1026. /**
  1027. * Allow the user to rename item on a given row.
  1028. */
  1029. private _doRename(): Promise<string> {
  1030. let listing = utils.findElement(this.node, CONTENT_CLASS);
  1031. let items = this._model.sortedItems;
  1032. let name = this._softSelection || this._model.getSelected()[0];
  1033. let index = arrays.findIndex(items, (value, index) => value.name === name);
  1034. let row = this._items[index];
  1035. let text = utils.findElement(row, ITEM_TEXT_CLASS);
  1036. let original = text.textContent;
  1037. return Private.doRename(row, text, this._editNode).then(changed => {
  1038. if (!changed) {
  1039. return original;
  1040. }
  1041. let newPath = text.textContent;
  1042. return this._model.rename(original, newPath).catch(error => {
  1043. if (error.xhr) {
  1044. error.message = `${error.xhr.status}: error.statusText`;
  1045. }
  1046. if (error.message.indexOf('409') !== -1 ||
  1047. error.message.indexOf('already exists') !== -1) {
  1048. let options = {
  1049. title: 'Overwrite file?',
  1050. host: this.parent.node,
  1051. body: `"${newPath}" already exists, overwrite?`
  1052. }
  1053. return showDialog(options).then(button => {
  1054. if (button.text === 'OK') {
  1055. return this._model.delete(newPath).then(() => {
  1056. return this._model.rename(original, newPath).then(() => {
  1057. this._model.refresh();
  1058. });
  1059. });
  1060. } else {
  1061. text.textContent = original;
  1062. }
  1063. });
  1064. }
  1065. }).catch(error => {
  1066. utils.showErrorMessage(this, 'Rename Error', error);
  1067. return original;
  1068. }).then(() => {
  1069. this._model.refresh();
  1070. return text.textContent;
  1071. });
  1072. });
  1073. }
  1074. /**
  1075. * Select a given item.
  1076. */
  1077. private _selectItem(index: number, keepExisting: boolean) {
  1078. // Selected the given row(s)
  1079. let items = this._model.sortedItems;
  1080. if (!keepExisting) {
  1081. this._model.clearSelected();
  1082. }
  1083. let name = items[index].name;
  1084. this._model.select(name);
  1085. Private.scrollIfNeeded(this.contentNode, this._items[index]);
  1086. this._isCut = false;
  1087. }
  1088. /**
  1089. * Handle the `refreshed` signal from the model.
  1090. */
  1091. private _onModelRefreshed(): void {
  1092. this.update();
  1093. }
  1094. /**
  1095. * Handle the `selectionChanged` signal from the model.
  1096. */
  1097. private _onSelectionChanged(): void {
  1098. this.update();
  1099. }
  1100. private _model: FileBrowserModel = null;
  1101. private _editNode: HTMLInputElement = null;
  1102. private _items: HTMLElement[] = [];
  1103. private _drag: Drag = null;
  1104. private _dragData: { pressX: number, pressY: number, index: number } = null;
  1105. private _selectTimer = -1;
  1106. private _noSelectTimer = -1;
  1107. private _isCut = false;
  1108. private _prevPath = '';
  1109. private _clipboard: string[] = [];
  1110. private _widgetFactory: (model: IContentsModel) => Widget = null;
  1111. private _softSelection = '';
  1112. }
  1113. /**
  1114. * The namespace for the listing private data.
  1115. */
  1116. namespace Private {
  1117. /**
  1118. * Handle editing text on a node.
  1119. *
  1120. * @returns Boolean indicating whether the name changed.
  1121. */
  1122. export
  1123. function doRename(parent: HTMLElement, text: HTMLElement, edit: HTMLInputElement): Promise<boolean> {
  1124. let changed = true;
  1125. parent.replaceChild(edit, text);
  1126. edit.value = text.textContent;
  1127. edit.focus();
  1128. let index = edit.value.lastIndexOf('.');
  1129. if (index === -1) {
  1130. edit.setSelectionRange(0, edit.value.length);
  1131. } else {
  1132. edit.setSelectionRange(0, index);
  1133. }
  1134. return new Promise<boolean>((resolve, reject) => {
  1135. edit.onblur = () => {
  1136. parent.replaceChild(text, edit);
  1137. if (text.textContent === edit.value) {
  1138. changed = false;
  1139. }
  1140. if (changed) text.textContent = edit.value;
  1141. resolve(changed);
  1142. }
  1143. edit.onkeydown = (event: KeyboardEvent) => {
  1144. switch (event.keyCode) {
  1145. case 13: // Enter
  1146. event.stopPropagation();
  1147. event.preventDefault();
  1148. edit.blur();
  1149. break;
  1150. case 27: // Escape
  1151. event.stopPropagation();
  1152. event.preventDefault();
  1153. changed = false;
  1154. edit.blur();
  1155. break;
  1156. }
  1157. }
  1158. });
  1159. }
  1160. /**
  1161. * Scroll an element into view if needed.
  1162. *
  1163. * @param area - The scroll area element.
  1164. *
  1165. * @param elem - The element of interest.
  1166. */
  1167. export
  1168. function scrollIfNeeded(area: HTMLElement, elem: HTMLElement): void {
  1169. let ar = area.getBoundingClientRect();
  1170. let er = elem.getBoundingClientRect();
  1171. if (er.top < ar.top) {
  1172. area.scrollTop -= ar.top - er.top;
  1173. } else if (er.bottom > ar.bottom) {
  1174. area.scrollTop += er.bottom - ar.bottom;
  1175. }
  1176. }
  1177. }