listing.ts 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. showDialog
  6. } from 'jupyter-js-domutils';
  7. import {
  8. IContentsModel
  9. } from 'jupyter-js-services';
  10. import * as moment
  11. from 'moment';
  12. import * as arrays
  13. from 'phosphor-arrays';
  14. import {
  15. IDisposable
  16. } from 'phosphor-disposable';
  17. import {
  18. Drag, DropAction, DropActions, IDragEvent, MimeData
  19. } from 'phosphor-dragdrop';
  20. import {
  21. Message
  22. } from 'phosphor-messaging';
  23. import {
  24. Widget
  25. } from 'phosphor-widget';
  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. let content = this.contentNode;
  649. if (content.contains(target)) {
  650. this._handleFileSelect(event);
  651. }
  652. }
  653. /**
  654. * Handle the `'scroll'` event for the widget.
  655. */
  656. private _evtScroll(event: MouseEvent): void {
  657. this.headerNode.scrollLeft = this.contentNode.scrollLeft;
  658. }
  659. /**
  660. * Handle the `'mousedown'` event for the widget.
  661. */
  662. private _evtMousedown(event: MouseEvent): void {
  663. // Blur the edit node if necessary.
  664. if (this._editNode.parentNode) {
  665. if (this._editNode !== event.target as HTMLElement) {
  666. this._editNode.focus();
  667. this._editNode.blur();
  668. clearTimeout(this._selectTimer);
  669. } else {
  670. return;
  671. }
  672. }
  673. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  674. if (index == -1) {
  675. return;
  676. }
  677. this._softSelection = '';
  678. let items = this._model.sortedItems;
  679. let selected = this._model.getSelected();
  680. if (selected.indexOf(items[index].name) == -1) {
  681. this._softSelection = items[index].name;
  682. }
  683. // Left mouse press for drag start.
  684. if (event.button === 0) {
  685. this._dragData = { pressX: event.clientX, pressY: event.clientY,
  686. index: index };
  687. document.addEventListener('mouseup', this, true);
  688. document.addEventListener('mousemove', this, true);
  689. }
  690. if (event.button !== 0) {
  691. clearTimeout(this._selectTimer);
  692. }
  693. }
  694. /**
  695. * Handle the `'mouseup'` event for the widget.
  696. */
  697. private _evtMouseup(event: MouseEvent): void {
  698. if (event.button !== 0 || !this._drag) {
  699. document.removeEventListener('mousemove', this, true);
  700. document.removeEventListener('mouseup', this, true);
  701. return;
  702. }
  703. event.preventDefault();
  704. event.stopPropagation();
  705. }
  706. /**
  707. * Handle the `'mousemove'` event for the widget.
  708. */
  709. private _evtMousemove(event: MouseEvent): void {
  710. event.preventDefault();
  711. event.stopPropagation();
  712. // Bail if we are the one dragging.
  713. if (this._drag) {
  714. return;
  715. }
  716. // Check for a drag initialization.
  717. let data = this._dragData;
  718. let dx = Math.abs(event.clientX - data.pressX);
  719. let dy = Math.abs(event.clientY - data.pressY);
  720. if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
  721. return;
  722. }
  723. this._startDrag(data.index, event.clientX, event.clientY);
  724. }
  725. /**
  726. * Handle the `'keydown'` event for the widget.
  727. */
  728. private _evtKeydown(event: KeyboardEvent): void {
  729. switch (event.keyCode) {
  730. case 38: // Up arrow
  731. this.selectPrevious(event.shiftKey);
  732. event.stopPropagation();
  733. event.preventDefault();
  734. break;
  735. case 40: // Down arrow
  736. this.selectNext(event.shiftKey);
  737. event.stopPropagation();
  738. event.preventDefault();
  739. break;
  740. }
  741. }
  742. /**
  743. * Handle the `'dblclick'` event for the widget.
  744. */
  745. private _evtDblClick(event: MouseEvent): void {
  746. // Do nothing if it's not a left mouse press.
  747. if (event.button !== 0) {
  748. return;
  749. }
  750. // Do nothing if any modifier keys are pressed.
  751. if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
  752. return;
  753. }
  754. // Stop the event propagation.
  755. event.preventDefault();
  756. event.stopPropagation();
  757. clearTimeout(this._selectTimer);
  758. this._noSelectTimer = setTimeout(() => {
  759. this._noSelectTimer = -1;
  760. }, RENAME_DURATION);
  761. this._editNode.blur();
  762. // Find a valid double click target.
  763. let target = event.target as HTMLElement;
  764. let i = arrays.findIndex(this._items, node => node.contains(target));
  765. if (i === -1) {
  766. return;
  767. }
  768. let item = this._model.sortedItems[i];
  769. this._model.open(item.name).catch(error =>
  770. utils.showErrorMessage(this, 'File Open Error', error)
  771. );
  772. }
  773. /**
  774. * Handle the `'p-dragenter'` event for the widget.
  775. */
  776. private _evtDragEnter(event: IDragEvent): void {
  777. if (event.mimeData.hasData(utils.CONTENTS_MIME)) {
  778. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  779. if (index === -1) {
  780. return;
  781. }
  782. let target = this._items[index];
  783. if (!target.classList.contains(FOLDER_TYPE_CLASS)) {
  784. return;
  785. }
  786. if (target.classList.contains(SELECTED_CLASS)) {
  787. return;
  788. }
  789. target.classList.add(utils.DROP_TARGET_CLASS);
  790. event.preventDefault();
  791. event.stopPropagation();
  792. }
  793. }
  794. /**
  795. * Handle the `'p-dragleave'` event for the widget.
  796. */
  797. private _evtDragLeave(event: IDragEvent): void {
  798. event.preventDefault();
  799. event.stopPropagation();
  800. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  801. if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  802. }
  803. /**
  804. * Handle the `'p-dragover'` event for the widget.
  805. */
  806. private _evtDragOver(event: IDragEvent): void {
  807. event.preventDefault();
  808. event.stopPropagation();
  809. event.dropAction = event.proposedAction;
  810. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  811. if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  812. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  813. this._items[index].classList.add(utils.DROP_TARGET_CLASS);
  814. }
  815. /**
  816. * Handle the `'p-drop'` event for the widget.
  817. */
  818. private _evtDrop(event: IDragEvent): void {
  819. event.preventDefault();
  820. event.stopPropagation();
  821. if (event.proposedAction === DropAction.None) {
  822. event.dropAction = DropAction.None;
  823. return;
  824. }
  825. if (!event.mimeData.hasData(utils.CONTENTS_MIME)) {
  826. return;
  827. }
  828. event.dropAction = event.proposedAction;
  829. let target = event.target as HTMLElement;
  830. while (target && target.parentElement) {
  831. if (target.classList.contains(utils.DROP_TARGET_CLASS)) {
  832. target.classList.remove(utils.DROP_TARGET_CLASS);
  833. break;
  834. }
  835. target = target.parentElement;
  836. }
  837. // Get the path based on the target node.
  838. let index = this._items.indexOf(target);
  839. let items = this._model.sortedItems;
  840. var path = items[index].name + '/';
  841. // Move all of the items.
  842. let promises: Promise<IContentsModel>[] = [];
  843. for (let item of items) {
  844. if (!this._softSelection && !this._model.isSelected(item.name)) {
  845. continue;
  846. }
  847. if (this._softSelection !== item.name) {
  848. continue;
  849. }
  850. var name = item.name;
  851. var newPath = path + name;
  852. promises.push(this._model.rename(name, newPath).catch(error => {
  853. if (error.xhr) {
  854. error.message = `${error.xhr.statusText} ${error.xhr.status}`;
  855. }
  856. if (error.message.indexOf('409') !== -1) {
  857. let options = {
  858. title: 'Overwrite file?',
  859. host: this.parent.node,
  860. body: `"${newPath}" already exists, overwrite?`
  861. }
  862. return showDialog(options).then(button => {
  863. if (button.text === 'OK') {
  864. return this._model.delete(newPath).then(() => {
  865. return this._model.rename(name, newPath);
  866. });
  867. }
  868. });
  869. }
  870. }));
  871. }
  872. Promise.all(promises).then(
  873. () => this._model.refresh(),
  874. error => utils.showErrorMessage(this, 'Move Error', error)
  875. );
  876. }
  877. /**
  878. * Start a drag event.
  879. */
  880. private _startDrag(index: number, clientX: number, clientY: number): void {
  881. let selectedNames = this._model.getSelected();
  882. let source = this._items[index];
  883. let items = this._model.sortedItems;
  884. let item: IContentsModel = null;
  885. // If the source node is not selected, use just that node.
  886. if (!source.classList.contains(SELECTED_CLASS)) {
  887. item = items[index];
  888. selectedNames = [item.name];
  889. } else if (selectedNames.length === 1) {
  890. let name = selectedNames[0];
  891. item = arrays.find(items, (value, index) => value.name === name);
  892. }
  893. // Create the drag image.
  894. var dragImage = source.cloneNode(true) as HTMLElement;
  895. dragImage.removeChild(dragImage.lastChild);
  896. if (selectedNames.length > 1) {
  897. let text = utils.findElement(dragImage, ITEM_TEXT_CLASS);
  898. text.textContent = '(' + selectedNames.length + ')'
  899. }
  900. // Set up the drag event.
  901. this._drag = new Drag({
  902. dragImage: dragImage,
  903. mimeData: new MimeData(),
  904. supportedActions: DropActions.Move,
  905. proposedAction: DropAction.Move
  906. });
  907. this._drag.mimeData.setData(utils.CONTENTS_MIME, null);
  908. if (this._widgetFactory && item && item.type !== 'directory') {
  909. this._drag.mimeData.setData(FACTORY_MIME, () => {
  910. return this._widgetFactory(item);
  911. });
  912. }
  913. // Start the drag and remove the mousemove listener.
  914. this._drag.start(clientX, clientY).then(action => {
  915. console.log('action', action);
  916. this._drag = null;
  917. });
  918. document.removeEventListener('mousemove', this, true);
  919. }
  920. /**
  921. * Handle selection on a file node.
  922. */
  923. private _handleFileSelect(event: MouseEvent): void {
  924. // Fetch common variables.
  925. let items = this._model.sortedItems;
  926. let nodes = this._items;
  927. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  928. clearTimeout(this._selectTimer);
  929. let name = items[index].name;
  930. let selected = this._model.getSelected();
  931. // Handle toggling.
  932. if (event.metaKey || event.ctrlKey) {
  933. if (this._model.isSelected(name)) {
  934. this._model.deselect(name);
  935. } else {
  936. this._model.select(name);
  937. }
  938. // Handle multiple select.
  939. } else if (event.shiftKey) {
  940. this._handleMultiSelect(selected, index);
  941. // Default to selecting the only the item.
  942. } else {
  943. // Handle a rename.
  944. if (selected.length === 1 && selected[0] === name) {
  945. this._selectTimer = setTimeout(() => {
  946. if (this._noSelectTimer === -1) {
  947. this._doRename();
  948. }
  949. }, RENAME_DURATION);
  950. }
  951. this._model.clearSelected();
  952. this._model.select(name);
  953. }
  954. this._isCut = false;
  955. this.update();
  956. }
  957. /**
  958. * Handle a multiple select on a file item node.
  959. */
  960. private _handleMultiSelect(selected: string[], index: number): void {
  961. // Find the "nearest selected".
  962. let items = this._model.sortedItems;
  963. let nearestIndex = -1;
  964. for (let i = 0; i < this._items.length; i++) {
  965. if (i === index) {
  966. continue;
  967. }
  968. let name = items[i].name;
  969. if (selected.indexOf(name) !== -1) {
  970. if (nearestIndex === -1) {
  971. nearestIndex = i;
  972. } else {
  973. if (Math.abs(index - i) < Math.abs(nearestIndex - i)) {
  974. nearestIndex = i;
  975. }
  976. }
  977. }
  978. }
  979. // Default to the first element (and fill down).
  980. if (nearestIndex === -1) {
  981. nearestIndex = 0;
  982. }
  983. // Select the rows between the current and the nearest selected.
  984. for (let i = 0; i < this._items.length; i++) {
  985. if (nearestIndex >= i && index <= i ||
  986. nearestIndex <= i && index >= i) {
  987. this._model.select(items[i].name);
  988. }
  989. }
  990. }
  991. /**
  992. * Get the currently selected items.
  993. */
  994. private _getSelectedItems(): IContentsModel[] {
  995. let items = this._model.sortedItems;
  996. if (!this._softSelection) {
  997. return items.filter(item => this._model.isSelected(item.name));
  998. }
  999. return items.filter(item => item.name === this._softSelection);
  1000. }
  1001. /**
  1002. * Copy the selected items, and optionally cut as well.
  1003. */
  1004. private _copy(): void {
  1005. this._clipboard = []
  1006. for (var item of this._getSelectedItems()) {
  1007. let row = arrays.find(this._items, row => {
  1008. let text = utils.findElement(row, ITEM_TEXT_CLASS);
  1009. return text.textContent === item.name;
  1010. });
  1011. if (item.type !== 'directory') {
  1012. // Store the absolute path of the item.
  1013. this._clipboard.push('/' + item.path)
  1014. }
  1015. }
  1016. this.update();
  1017. }
  1018. /**
  1019. * Allow the user to rename item on a given row.
  1020. */
  1021. private _doRename(): Promise<string> {
  1022. let listing = utils.findElement(this.node, CONTENT_CLASS);
  1023. let items = this._model.sortedItems;
  1024. let name = this._model.getSelected()[0];
  1025. let index = arrays.findIndex(items, (value, index) => value.name === name);
  1026. let row = this._items[index];
  1027. let fileCell = utils.findElement(row, FILE_TYPE_CLASS);
  1028. let text = utils.findElement(row, ITEM_TEXT_CLASS);
  1029. let original = text.textContent;
  1030. if (!fileCell) {
  1031. return;
  1032. }
  1033. return Private.doRename(fileCell as HTMLElement, text, this._editNode).then(changed => {
  1034. if (!changed) {
  1035. return original;
  1036. }
  1037. let newPath = text.textContent;
  1038. return this._model.rename(original, newPath).catch(error => {
  1039. if (error.xhr) {
  1040. error.message = `${error.xhr.status}: error.statusText`;
  1041. }
  1042. if (error.message.indexOf('409') !== -1 ||
  1043. error.message.indexOf('already exists') !== -1) {
  1044. let options = {
  1045. title: 'Overwrite file?',
  1046. host: this.parent.node,
  1047. body: `"${newPath}" already exists, overwrite?`
  1048. }
  1049. return showDialog(options).then(button => {
  1050. if (button.text === 'OK') {
  1051. return this._model.delete(newPath).then(() => {
  1052. return this._model.rename(original, newPath).then(() => {
  1053. this._model.refresh();
  1054. });
  1055. });
  1056. } else {
  1057. text.textContent = original;
  1058. }
  1059. });
  1060. }
  1061. }).catch(error => {
  1062. utils.showErrorMessage(this, 'Rename Error', error);
  1063. return original;
  1064. }).then(() => {
  1065. this._model.refresh();
  1066. return text.textContent;
  1067. });
  1068. });
  1069. }
  1070. /**
  1071. * Select a given item.
  1072. */
  1073. private _selectItem(index: number, keepExisting: boolean) {
  1074. // Selected the given row(s)
  1075. let items = this._model.sortedItems;
  1076. if (!keepExisting) {
  1077. this._model.clearSelected();
  1078. }
  1079. let name = items[index].name;
  1080. this._model.select(name);
  1081. Private.scrollIfNeeded(this.contentNode, this._items[index]);
  1082. this._isCut = false;
  1083. }
  1084. /**
  1085. * Handle the `refreshed` signal from the model.
  1086. */
  1087. private _onModelRefreshed(): void {
  1088. this.update();
  1089. }
  1090. /**
  1091. * Handle the `selectionChanged` signal from the model.
  1092. */
  1093. private _onSelectionChanged(): void {
  1094. this.update();
  1095. }
  1096. private _model: FileBrowserModel = null;
  1097. private _editNode: HTMLInputElement = null;
  1098. private _items: HTMLElement[] = [];
  1099. private _drag: Drag = null;
  1100. private _dragData: { pressX: number, pressY: number, index: number } = null;
  1101. private _selectTimer = -1;
  1102. private _noSelectTimer = -1;
  1103. private _isCut = false;
  1104. private _prevPath = '';
  1105. private _clipboard: string[] = [];
  1106. private _widgetFactory: (model: IContentsModel) => Widget = null;
  1107. private _softSelection = '';
  1108. }
  1109. /**
  1110. * The namespace for the listing private data.
  1111. */
  1112. namespace Private {
  1113. /**
  1114. * Handle editing text on a node.
  1115. *
  1116. * @returns Boolean indicating whether the name changed.
  1117. */
  1118. export
  1119. function doRename(parent: HTMLElement, text: HTMLElement, edit: HTMLInputElement): Promise<boolean> {
  1120. let changed = true;
  1121. parent.replaceChild(edit, text);
  1122. edit.value = text.textContent;
  1123. edit.focus();
  1124. let index = edit.value.lastIndexOf('.');
  1125. if (index === -1) {
  1126. edit.setSelectionRange(0, edit.value.length);
  1127. } else {
  1128. edit.setSelectionRange(0, index);
  1129. }
  1130. return new Promise<boolean>((resolve, reject) => {
  1131. edit.onblur = () => {
  1132. parent.replaceChild(text, edit);
  1133. if (text.textContent === edit.value) {
  1134. changed = false;
  1135. }
  1136. if (changed) text.textContent = edit.value;
  1137. resolve(changed);
  1138. }
  1139. edit.onkeydown = (event: KeyboardEvent) => {
  1140. switch (event.keyCode) {
  1141. case 13: // Enter
  1142. event.stopPropagation();
  1143. event.preventDefault();
  1144. edit.blur();
  1145. break;
  1146. case 27: // Escape
  1147. event.stopPropagation();
  1148. event.preventDefault();
  1149. changed = false;
  1150. edit.blur();
  1151. break;
  1152. }
  1153. }
  1154. });
  1155. }
  1156. /**
  1157. * Scroll an element into view if needed.
  1158. *
  1159. * @param area - The scroll area element.
  1160. *
  1161. * @param elem - The element of interest.
  1162. */
  1163. export
  1164. function scrollIfNeeded(area: HTMLElement, elem: HTMLElement): void {
  1165. let ar = area.getBoundingClientRect();
  1166. let er = elem.getBoundingClientRect();
  1167. if (er.top < ar.top) {
  1168. area.scrollTop -= ar.top - er.top;
  1169. } else if (er.bottom > ar.bottom) {
  1170. area.scrollTop += er.bottom - ar.bottom;
  1171. }
  1172. }
  1173. }