listing.ts 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Contents
  5. } from '@jupyterlab/services';
  6. import {
  7. each
  8. } from 'phosphor/lib/algorithm/iteration';
  9. import {
  10. find, findIndex
  11. } from 'phosphor/lib/algorithm/searching';
  12. import {
  13. ISequence
  14. } from 'phosphor/lib/algorithm/sequence';
  15. import {
  16. Message
  17. } from 'phosphor/lib/core/messaging';
  18. import {
  19. MimeData
  20. } from 'phosphor/lib/core/mimedata';
  21. import {
  22. Drag, IDragEvent
  23. } from 'phosphor/lib/dom/dragdrop';
  24. import {
  25. scrollIntoViewIfNeeded
  26. } from 'phosphor/lib/dom/query';
  27. import {
  28. Widget
  29. } from 'phosphor/lib/ui/widget';
  30. import {
  31. humanTime, dateTime
  32. } from '../common/dates';
  33. import {
  34. showDialog
  35. } from '../dialog';
  36. import {
  37. DocumentManager
  38. } from '../docmanager';
  39. import {
  40. IWidgetOpener
  41. } from './browser';
  42. import {
  43. renameFile
  44. } from './dialogs';
  45. import {
  46. FileBrowserModel
  47. } from './model';
  48. import * as utils
  49. from './utils';
  50. import {
  51. SELECTED_CLASS, showErrorMessage
  52. } from './utils';
  53. /**
  54. * The class name added to DirListing widget.
  55. */
  56. const DIR_LISTING_CLASS = 'jp-DirListing';
  57. /**
  58. * The class name added to a dir listing header node.
  59. */
  60. const HEADER_CLASS = 'jp-DirListing-header';
  61. /**
  62. * The class name added to a dir listing list header cell.
  63. */
  64. const HEADER_ITEM_CLASS = 'jp-DirListing-headerItem';
  65. /**
  66. * The class name added to a header cell text node.
  67. */
  68. const HEADER_ITEM_TEXT_CLASS = 'jp-DirListing-headerItemText';
  69. /**
  70. * The class name added to a header cell icon node.
  71. */
  72. const HEADER_ITEM_ICON_CLASS = 'jp-DirListing-headerItemIcon';
  73. /**
  74. * The class name added to the dir listing content node.
  75. */
  76. const CONTENT_CLASS = 'jp-DirListing-content';
  77. /**
  78. * The class name added to dir listing content item.
  79. */
  80. const ITEM_CLASS = 'jp-DirListing-item';
  81. /**
  82. * The class name added to the listing item text cell.
  83. */
  84. const ITEM_TEXT_CLASS = 'jp-DirListing-itemText';
  85. /**
  86. * The class name added to the listing item icon cell.
  87. */
  88. const ITEM_ICON_CLASS = 'jp-DirListing-itemIcon';
  89. /**
  90. * The class name added to the listing item modified cell.
  91. */
  92. const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
  93. /**
  94. * The class name added to the dir listing editor node.
  95. */
  96. const EDITOR_CLASS = 'jp-DirListing-editor';
  97. /**
  98. * The class name added to the name column header cell.
  99. */
  100. const NAME_ID_CLASS = 'jp-id-name';
  101. /**
  102. * The class name added to the modified column header cell.
  103. */
  104. const MODIFIED_ID_CLASS = 'jp-id-modified';
  105. /**
  106. * The class name added to a file type content item.
  107. */
  108. const FILE_TYPE_CLASS = 'jp-type-file';
  109. /**
  110. * The class name added to a folder type content item.
  111. */
  112. const FOLDER_TYPE_CLASS = 'jp-type-folder';
  113. /**
  114. * The class name added to a notebook type content item.
  115. */
  116. const NOTEBOOK_TYPE_CLASS = 'jp-type-notebook';
  117. /**
  118. * The class name added to the widget when there are items on the clipboard.
  119. */
  120. const CLIPBOARD_CLASS = 'jp-mod-clipboard';
  121. /**
  122. * The class name added to cut rows.
  123. */
  124. const CUT_CLASS = 'jp-mod-cut';
  125. /**
  126. * The class name added when there are more than one selected rows.
  127. */
  128. const MULTI_SELECTED_CLASS = 'jp-mod-multiSelected';
  129. /**
  130. * The class name added to indicate running notebook.
  131. */
  132. const RUNNING_CLASS = 'jp-mod-running';
  133. /**
  134. * The class name added for a decending sort.
  135. */
  136. const DESCENDING_CLASS = 'jp-mod-descending';
  137. /**
  138. * The minimum duration for a rename select in ms.
  139. */
  140. const RENAME_DURATION = 500;
  141. /**
  142. * The threshold in pixels to start a drag event.
  143. */
  144. const DRAG_THRESHOLD = 5;
  145. /**
  146. * A boolean indicating whether the platform is Mac.
  147. */
  148. const IS_MAC = !!navigator.platform.match(/Mac/i);
  149. /**
  150. * The factory MIME type supported by phosphor dock panels.
  151. */
  152. const FACTORY_MIME = 'application/vnd.phosphor.widget-factory';
  153. /**
  154. * A widget which hosts a file list area.
  155. */
  156. export
  157. class DirListing extends Widget {
  158. /**
  159. * Construct a new file browser directory listing widget.
  160. *
  161. * @param model - The file browser view model.
  162. */
  163. constructor(options: DirListing.IOptions) {
  164. super({
  165. node: (options.renderer || DirListing.defaultRenderer).createNode()
  166. });
  167. this.addClass(DIR_LISTING_CLASS);
  168. this._model = options.model;
  169. this._model.refreshed.connect(this._onModelRefreshed, this);
  170. this._model.pathChanged.connect(this._onPathChanged, this);
  171. this._editNode = document.createElement('input');
  172. this._editNode.className = EDITOR_CLASS;
  173. this._manager = options.manager;
  174. this._opener = options.opener;
  175. this._renderer = options.renderer || DirListing.defaultRenderer;
  176. let headerNode = utils.findElement(this.node, HEADER_CLASS);
  177. this._renderer.populateHeaderNode(headerNode);
  178. }
  179. /**
  180. * Dispose of the resources held by the directory listing.
  181. */
  182. dispose(): void {
  183. this._model = null;
  184. this._items = null;
  185. this._editNode = null;
  186. this._drag = null;
  187. this._dragData = null;
  188. this._manager = null;
  189. this._opener = null;
  190. super.dispose();
  191. }
  192. /**
  193. * Get the model used by the listing.
  194. *
  195. * #### Notes
  196. * This is a read-only property.
  197. */
  198. get model(): FileBrowserModel {
  199. return this._model;
  200. }
  201. /**
  202. * Get the dir listing header node.
  203. *
  204. * #### Notes
  205. * This is the node which holds the header cells.
  206. *
  207. * Modifying this node directly can lead to undefined behavior.
  208. *
  209. * This is a read-only property.
  210. */
  211. get headerNode(): HTMLElement {
  212. return utils.findElement(this.node, HEADER_CLASS);
  213. }
  214. /**
  215. * Get the dir listing content node.
  216. *
  217. * #### Notes
  218. * This is the node which holds the item nodes.
  219. *
  220. * Modifying this node directly can lead to undefined behavior.
  221. *
  222. * This is a read-only property.
  223. */
  224. get contentNode(): HTMLElement {
  225. return utils.findElement(this.node, CONTENT_CLASS);
  226. }
  227. /**
  228. * The renderer instance used by the directory listing.
  229. *
  230. * #### Notes
  231. * This is a read-only property.
  232. */
  233. get renderer(): DirListing.IRenderer {
  234. return this._renderer;
  235. }
  236. /**
  237. * The the sorted content items.
  238. */
  239. get sortedItems(): Contents.IModel[] {
  240. return this._sortedModels;
  241. }
  242. /**
  243. * The current sort state.
  244. *
  245. * #### Notes
  246. * This is a read-only property.
  247. */
  248. get sortState(): DirListing.ISortState {
  249. return this._sortState;
  250. }
  251. /**
  252. * Sort the items using a sort condition.
  253. */
  254. sort(state: DirListing.ISortState): void {
  255. this._sortedModels = Private.sort(this.model.items, state);
  256. this._sortState = state;
  257. this.update();
  258. }
  259. /**
  260. * Rename the first currently selected item.
  261. */
  262. rename(): Promise<string> {
  263. return this._doRename();
  264. }
  265. /**
  266. * Cut the selected items.
  267. */
  268. cut(): void {
  269. this._isCut = true;
  270. this._copy();
  271. }
  272. /**
  273. * Copy the selected items.
  274. */
  275. copy(): void {
  276. this._copy();
  277. }
  278. /**
  279. * Paste the items from the clipboard.
  280. */
  281. paste(): Promise<void> {
  282. if (!this._clipboard.length) {
  283. return;
  284. }
  285. let promises: Promise<Contents.IModel>[] = [];
  286. for (let path of this._clipboard) {
  287. if (this._isCut) {
  288. let parts = path.split('/');
  289. let name = parts[parts.length - 1];
  290. promises.push(this._model.rename(path, name));
  291. } else {
  292. promises.push(this._model.copy(path, '.'));
  293. }
  294. }
  295. // Remove any cut modifiers.
  296. for (let item of this._items) {
  297. item.classList.remove(CUT_CLASS);
  298. }
  299. this._clipboard = [];
  300. this._isCut = false;
  301. this.removeClass(CLIPBOARD_CLASS);
  302. return Promise.all(promises).then(
  303. () => this._model.refresh(),
  304. error => utils.showErrorMessage(this, 'Paste Error', error)
  305. );
  306. }
  307. /**
  308. * Delete the currently selected item(s).
  309. */
  310. delete(): Promise<void> {
  311. let names: string[] = [];
  312. each(this._model.items, item => {
  313. if (this._selection[item.name]) {
  314. names.push(item.name);
  315. }
  316. });
  317. let message = `Permanently delete these ${names.length} files?`;
  318. if (names.length === 1) {
  319. message = `Permanently delete file "${names[0]}"?`;
  320. }
  321. if (names.length) {
  322. return showDialog({
  323. title: 'Delete file?',
  324. body: message,
  325. okText: 'DELETE'
  326. }).then(result => {
  327. if (result.text === 'DELETE') {
  328. return this._delete(names);
  329. }
  330. });
  331. }
  332. return Promise.resolve(void 0);
  333. }
  334. /**
  335. * Duplicate the currently selected item(s).
  336. */
  337. duplicate(): Promise<void> {
  338. let promises: Promise<Contents.IModel>[] = [];
  339. for (let item of this._getSelectedItems()) {
  340. if (item.type !== 'directory') {
  341. promises.push(this._model.copy(item.path, this._model.path));
  342. }
  343. }
  344. return Promise.all(promises).then(
  345. () => this._model.refresh(),
  346. error => utils.showErrorMessage(this, 'Duplicate file', error)
  347. );
  348. }
  349. /**
  350. * Download the currently selected item(s).
  351. */
  352. download(): void {
  353. for (let item of this._getSelectedItems()) {
  354. if (item.type !== 'directory') {
  355. this._model.download(item.path)
  356. }
  357. }
  358. }
  359. /**
  360. * Shut down kernels on the applicable currently selected items.
  361. */
  362. shutdownKernels(): Promise<void> {
  363. let promises: Promise<void>[] = [];
  364. let items = this.sortedItems;
  365. let paths = items.map(item => item.path);
  366. each(this._model.sessions, session => {
  367. let index = paths.indexOf(session.notebook.path);
  368. if (this._selection[items[index].name]) {
  369. promises.push(this._model.shutdown(session.id));
  370. }
  371. });
  372. return Promise.all(promises).then(
  373. () => this._model.refresh(),
  374. error => utils.showErrorMessage(this, 'Shutdown kernel', error)
  375. );
  376. }
  377. /**
  378. * Select next item.
  379. *
  380. * @param keepExisting - Whether to keep the current selection and add to it.
  381. */
  382. selectNext(keepExisting = false): void {
  383. let index = -1;
  384. let selected = Object.keys(this._selection);
  385. let items = this.sortedItems;
  386. if (selected.length === 1 || keepExisting) {
  387. // Select the next item.
  388. let name = selected[selected.length - 1];
  389. index = findIndex(items, value => value.name === name);
  390. index += 1;
  391. if (index === this._items.length) {
  392. index = 0;
  393. }
  394. } else if (selected.length === 0) {
  395. // Select the first item.
  396. index = 0;
  397. } else {
  398. // Select the last selected item.
  399. let name = selected[selected.length - 1];
  400. index = findIndex(items, value => value.name === name);
  401. }
  402. if (index !== -1) {
  403. this._selectItem(index, keepExisting);
  404. }
  405. }
  406. /**
  407. * Select previous item.
  408. *
  409. * @param keepExisting - Whether to keep the current selection and add to it.
  410. */
  411. selectPrevious(keepExisting = false): void {
  412. let index = -1;
  413. let selected = Object.keys(this._selection);
  414. let items = this.sortedItems;
  415. if (selected.length === 1 || keepExisting) {
  416. // Select the previous item.
  417. let name = selected[0];
  418. index = findIndex(items, value => value.name === name);
  419. index -= 1;
  420. if (index === -1) {
  421. index = this._items.length - 1;
  422. }
  423. } else if (selected.length === 0) {
  424. // Select the last item.
  425. index = this._items.length - 1;
  426. } else {
  427. // Select the first selected item.
  428. let name = selected[0];
  429. index = findIndex(items, value => value.name === name);
  430. }
  431. if (index !== -1) {
  432. this._selectItem(index, keepExisting);
  433. }
  434. }
  435. /**
  436. * Get whether an item is selected by name.
  437. */
  438. isSelected(name: string): boolean {
  439. return this._selection[name] === true;
  440. }
  441. /**
  442. * Find a path given a click.
  443. */
  444. pathForClick(event: MouseEvent): string {
  445. let items = this.sortedItems;
  446. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  447. if (index !== -1) {
  448. return items[index].path;
  449. }
  450. }
  451. /**
  452. * Handle the DOM events for the directory listing.
  453. *
  454. * @param event - The DOM event sent to the widget.
  455. *
  456. * #### Notes
  457. * This method implements the DOM `EventListener` interface and is
  458. * called in response to events on the panel's DOM node. It should
  459. * not be called directly by user code.
  460. */
  461. handleEvent(event: Event): void {
  462. switch (event.type) {
  463. case 'mousedown':
  464. this._evtMousedown(event as MouseEvent);
  465. break;
  466. case 'mouseup':
  467. this._evtMouseup(event as MouseEvent);
  468. break;
  469. case 'mousemove':
  470. this._evtMousemove(event as MouseEvent);
  471. break;
  472. case 'keydown':
  473. this._evtKeydown(event as KeyboardEvent);
  474. break;
  475. case 'click':
  476. this._evtClick(event as MouseEvent);
  477. break;
  478. case 'dblclick':
  479. this._evtDblClick(event as MouseEvent);
  480. break;
  481. case 'contextmenu':
  482. this._evtContextMenu(event as MouseEvent);
  483. break;
  484. case 'scroll':
  485. this._evtScroll(event as MouseEvent);
  486. break;
  487. case 'p-dragenter':
  488. this._evtDragEnter(event as IDragEvent);
  489. break;
  490. case 'p-dragleave':
  491. this._evtDragLeave(event as IDragEvent);
  492. break;
  493. case 'p-dragover':
  494. this._evtDragOver(event as IDragEvent);
  495. break;
  496. case 'p-drop':
  497. this._evtDrop(event as IDragEvent);
  498. break;
  499. default:
  500. break;
  501. }
  502. }
  503. /**
  504. * A message handler invoked on an `'after-attach'` message.
  505. */
  506. protected onAfterAttach(msg: Message): void {
  507. super.onAfterAttach(msg);
  508. let node = this.node;
  509. let content = utils.findElement(node, CONTENT_CLASS);
  510. node.addEventListener('mousedown', this);
  511. node.addEventListener('keydown', this);
  512. node.addEventListener('click', this);
  513. node.addEventListener('dblclick', this);
  514. node.addEventListener('contextmenu', this);
  515. content.addEventListener('scroll', this);
  516. content.addEventListener('p-dragenter', this);
  517. content.addEventListener('p-dragleave', this);
  518. content.addEventListener('p-dragover', this);
  519. content.addEventListener('p-drop', this);
  520. }
  521. /**
  522. * A message handler invoked on a `'before-detach'` message.
  523. */
  524. protected onBeforeDetach(msg: Message): void {
  525. super.onBeforeDetach(msg);
  526. let node = this.node;
  527. let content = utils.findElement(node, CONTENT_CLASS);
  528. node.removeEventListener('mousedown', this);
  529. node.removeEventListener('keydown', this);
  530. node.removeEventListener('click', this);
  531. node.removeEventListener('dblclick', this);
  532. node.removeEventListener('contextmenu', this);
  533. content.removeEventListener('scroll', this);
  534. content.removeEventListener('p-dragenter', this);
  535. content.removeEventListener('p-dragleave', this);
  536. content.removeEventListener('p-dragover', this);
  537. content.removeEventListener('p-drop', this);
  538. document.removeEventListener('mousemove', this, true);
  539. document.removeEventListener('mouseup', this, true);
  540. }
  541. /**
  542. * A handler invoked on an `'update-request'` message.
  543. */
  544. protected onUpdateRequest(msg: Message): void {
  545. // Fetch common variables.
  546. let items = this.sortedItems;
  547. let nodes = this._items;
  548. let content = utils.findElement(this.node, CONTENT_CLASS);
  549. let renderer = this._renderer;
  550. this.removeClass(MULTI_SELECTED_CLASS);
  551. this.removeClass(SELECTED_CLASS);
  552. // Remove any excess item nodes.
  553. while (nodes.length > items.length) {
  554. let node = nodes.pop();
  555. content.removeChild(node);
  556. }
  557. // Add any missing item nodes.
  558. while (nodes.length < items.length) {
  559. let node = renderer.createItemNode();
  560. node.classList.add(ITEM_CLASS);
  561. nodes.push(node);
  562. content.appendChild(node);
  563. }
  564. // Remove extra classes from the nodes.
  565. for (let i = 0, n = items.length; i < n; ++i) {
  566. nodes[i].classList.remove(SELECTED_CLASS);
  567. nodes[i].classList.remove(RUNNING_CLASS);
  568. nodes[i].classList.remove(CUT_CLASS);
  569. }
  570. // Add extra classes to item nodes based on widget state.
  571. for (let i = 0, n = items.length; i < n; ++i) {
  572. renderer.updateItemNode(nodes[i], items[i]);
  573. if (this._selection[items[i].name]) {
  574. nodes[i].classList.add(SELECTED_CLASS);
  575. if (this._isCut && this._model.path === this._prevPath) {
  576. nodes[i].classList.add(CUT_CLASS);
  577. }
  578. }
  579. }
  580. // Handle the selectors on the widget node.
  581. let selectedNames = Object.keys(this._selection);
  582. if (selectedNames.length > 1) {
  583. this.addClass(MULTI_SELECTED_CLASS);
  584. }
  585. if (selectedNames.length) {
  586. this.addClass(SELECTED_CLASS);
  587. }
  588. // Handle notebook session statuses.
  589. let paths = items.map(item => item.path);
  590. let specs = this._model.kernelspecs;
  591. each(this._model.sessions, session => {
  592. let index = paths.indexOf(session.notebook.path);
  593. let node = this._items[index];
  594. node.classList.add(RUNNING_CLASS);
  595. node.title = specs.kernelspecs[session.kernel.name].spec.display_name;
  596. });
  597. this._prevPath = this._model.path;
  598. }
  599. /**
  600. * Handle the `'click'` event for the widget.
  601. */
  602. private _evtClick(event: MouseEvent) {
  603. let target = event.target as HTMLElement;
  604. let header = this.headerNode;
  605. if (header.contains(target)) {
  606. let state = this.renderer.handleHeaderClick(header, event);
  607. if (state) {
  608. this.sort(state);
  609. }
  610. return;
  611. }
  612. }
  613. /**
  614. * Handle the `'scroll'` event for the widget.
  615. */
  616. private _evtScroll(event: MouseEvent): void {
  617. this.headerNode.scrollLeft = this.contentNode.scrollLeft;
  618. }
  619. /**
  620. * Handle the `'contextmenu'` event for the widget.
  621. */
  622. private _evtContextMenu(event: MouseEvent): void {
  623. this._inContext = true;
  624. }
  625. /**
  626. * Handle the `'mousedown'` event for the widget.
  627. */
  628. private _evtMousedown(event: MouseEvent): void {
  629. // Bail if clicking within the edit node
  630. if (event.target === this._editNode) {
  631. return;
  632. }
  633. // Blur the edit node if necessary.
  634. if (this._editNode.parentNode) {
  635. if (this._editNode !== event.target as HTMLElement) {
  636. this._editNode.focus();
  637. this._editNode.blur();
  638. clearTimeout(this._selectTimer);
  639. } else {
  640. return;
  641. }
  642. }
  643. // Check for clearing a context menu.
  644. let newContext = (IS_MAC && event.ctrlKey) || (event.button === 2);
  645. if (this._inContext && !newContext) {
  646. this._inContext = false;
  647. return;
  648. }
  649. this._inContext = false;
  650. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  651. if (index === -1) {
  652. return;
  653. }
  654. this._handleFileSelect(event);
  655. // Left mouse press for drag start.
  656. if (event.button === 0) {
  657. this._dragData = { pressX: event.clientX, pressY: event.clientY,
  658. index: index };
  659. document.addEventListener('mouseup', this, true);
  660. document.addEventListener('mousemove', this, true);
  661. }
  662. if (event.button !== 0) {
  663. clearTimeout(this._selectTimer);
  664. }
  665. }
  666. /**
  667. * Handle the `'mouseup'` event for the widget.
  668. */
  669. private _evtMouseup(event: MouseEvent): void {
  670. // Handle any soft selection from the previous mouse down.
  671. if (this._softSelection) {
  672. let altered = event.metaKey || event.shiftKey || event.ctrlKey;
  673. // See if we need to clear the other selection.
  674. if (!altered && event.button === 0) {
  675. this._selection = Object.create(null);
  676. this._selection[this._softSelection] = true;
  677. this.update();
  678. }
  679. this._softSelection = '';
  680. }
  681. // Remove the drag listeners if necessary.
  682. if (event.button !== 0 || !this._drag) {
  683. document.removeEventListener('mousemove', this, true);
  684. document.removeEventListener('mouseup', this, true);
  685. return;
  686. }
  687. event.preventDefault();
  688. event.stopPropagation();
  689. }
  690. /**
  691. * Handle the `'mousemove'` event for the widget.
  692. */
  693. private _evtMousemove(event: MouseEvent): void {
  694. event.preventDefault();
  695. event.stopPropagation();
  696. // Bail if we are the one dragging.
  697. if (this._drag) {
  698. return;
  699. }
  700. // Check for a drag initialization.
  701. let data = this._dragData;
  702. let dx = Math.abs(event.clientX - data.pressX);
  703. let dy = Math.abs(event.clientY - data.pressY);
  704. if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
  705. return;
  706. }
  707. this._startDrag(data.index, event.clientX, event.clientY);
  708. }
  709. /**
  710. * Handle the `'keydown'` event for the widget.
  711. */
  712. private _evtKeydown(event: KeyboardEvent): void {
  713. switch (event.keyCode) {
  714. case 38: // Up arrow
  715. this.selectPrevious(event.shiftKey);
  716. event.stopPropagation();
  717. event.preventDefault();
  718. break;
  719. case 40: // Down arrow
  720. this.selectNext(event.shiftKey);
  721. event.stopPropagation();
  722. event.preventDefault();
  723. break;
  724. default:
  725. break;
  726. }
  727. }
  728. /**
  729. * Handle the `'dblclick'` event for the widget.
  730. */
  731. private _evtDblClick(event: MouseEvent): void {
  732. // Do nothing if it's not a left mouse press.
  733. if (event.button !== 0) {
  734. return;
  735. }
  736. // Do nothing if any modifier keys are pressed.
  737. if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
  738. return;
  739. }
  740. // Stop the event propagation.
  741. event.preventDefault();
  742. event.stopPropagation();
  743. clearTimeout(this._selectTimer);
  744. this._noSelectTimer = setTimeout(() => {
  745. this._noSelectTimer = -1;
  746. }, RENAME_DURATION);
  747. this._editNode.blur();
  748. // Find a valid double click target.
  749. let target = event.target as HTMLElement;
  750. let i = findIndex(this._items, node => node.contains(target));
  751. if (i === -1) {
  752. return;
  753. }
  754. let model = this._model;
  755. let item = this.sortedItems[i];
  756. if (item.type === 'directory') {
  757. model.cd(item.name).catch(error =>
  758. showErrorMessage(this, 'Open directory', error)
  759. );
  760. } else {
  761. let path = item.path;
  762. let widget = this._manager.findWidget(path);
  763. if (!widget) {
  764. widget = this._manager.open(item.path);
  765. let context = this._manager.contextForWidget(widget);
  766. context.populated.connect(() => model.refresh() );
  767. context.kernelChanged.connect(() => model.refresh() );
  768. }
  769. this._opener.open(widget);
  770. }
  771. }
  772. /**
  773. * Handle the `'p-dragenter'` event for the widget.
  774. */
  775. private _evtDragEnter(event: IDragEvent): void {
  776. if (event.mimeData.hasData(utils.CONTENTS_MIME)) {
  777. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  778. if (index === -1) {
  779. return;
  780. }
  781. let item = this.sortedItems[index];
  782. if (item.type !== 'directory' || this._selection[item.name]) {
  783. return;
  784. }
  785. let target = event.target as HTMLElement;
  786. target.classList.add(utils.DROP_TARGET_CLASS);
  787. event.preventDefault();
  788. event.stopPropagation();
  789. }
  790. }
  791. /**
  792. * Handle the `'p-dragleave'` event for the widget.
  793. */
  794. private _evtDragLeave(event: IDragEvent): void {
  795. event.preventDefault();
  796. event.stopPropagation();
  797. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  798. if (dropTarget) {
  799. dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  800. }
  801. }
  802. /**
  803. * Handle the `'p-dragover'` event for the widget.
  804. */
  805. private _evtDragOver(event: IDragEvent): void {
  806. event.preventDefault();
  807. event.stopPropagation();
  808. event.dropAction = event.proposedAction;
  809. let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
  810. if (dropTarget) {
  811. dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
  812. }
  813. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  814. this._items[index].classList.add(utils.DROP_TARGET_CLASS);
  815. }
  816. /**
  817. * Handle the `'p-drop'` event for the widget.
  818. */
  819. private _evtDrop(event: IDragEvent): void {
  820. event.preventDefault();
  821. event.stopPropagation();
  822. clearTimeout(this._selectTimer);
  823. if (event.proposedAction === 'none') {
  824. event.dropAction = 'none';
  825. return;
  826. }
  827. if (!event.mimeData.hasData(utils.CONTENTS_MIME)) {
  828. return;
  829. }
  830. event.dropAction = event.proposedAction;
  831. let target = event.target as HTMLElement;
  832. while (target && target.parentElement) {
  833. if (target.classList.contains(utils.DROP_TARGET_CLASS)) {
  834. target.classList.remove(utils.DROP_TARGET_CLASS);
  835. break;
  836. }
  837. target = target.parentElement;
  838. }
  839. // Get the path based on the target node.
  840. let index = this._items.indexOf(target);
  841. let items = this.sortedItems;
  842. let path = items[index].name + '/';
  843. // Move all of the items.
  844. let promises: Promise<Contents.IModel>[] = [];
  845. let names = event.mimeData.getData(utils.CONTENTS_MIME) as string[];
  846. for (let name of names) {
  847. let newPath = path + name;
  848. promises.push(renameFile(this._model, name, newPath));
  849. }
  850. Promise.all(promises).then(
  851. () => this._model.refresh(),
  852. error => utils.showErrorMessage(this, 'Move Error', error)
  853. );
  854. }
  855. /**
  856. * Start a drag event.
  857. */
  858. private _startDrag(index: number, clientX: number, clientY: number): void {
  859. let selectedNames = Object.keys(this._selection);
  860. let source = this._items[index];
  861. let model = this._model;
  862. let items = this.sortedItems;
  863. let item: Contents.IModel = null;
  864. // If the source node is not selected, use just that node.
  865. if (!source.classList.contains(SELECTED_CLASS)) {
  866. item = items[index];
  867. selectedNames = [item.name];
  868. } else if (selectedNames.length === 1) {
  869. let name = selectedNames[0];
  870. item = find(items, value => value.name === name);
  871. }
  872. // Create the drag image.
  873. let dragImage = this.renderer.createDragImage(source, selectedNames.length);
  874. // Set up the drag event.
  875. this._drag = new Drag({
  876. dragImage,
  877. mimeData: new MimeData(),
  878. supportedActions: 'move',
  879. proposedAction: 'move'
  880. });
  881. this._drag.mimeData.setData(utils.CONTENTS_MIME, selectedNames);
  882. if (item && item.type !== 'directory') {
  883. this._drag.mimeData.setData(FACTORY_MIME, () => {
  884. let path = item.path;
  885. let widget = this._manager.findWidget(path);
  886. if (!widget) {
  887. widget = this._manager.open(item.path);
  888. let context = this._manager.contextForWidget(widget);
  889. context.populated.connect(() => model.refresh() );
  890. context.kernelChanged.connect(() => model.refresh() );
  891. }
  892. this._opener.open(widget);
  893. return widget;
  894. });
  895. }
  896. // Start the drag and remove the mousemove and mouseup listeners.
  897. document.removeEventListener('mousemove', this, true);
  898. document.removeEventListener('mouseup', this, true);
  899. clearTimeout(this._selectTimer);
  900. this._drag.start(clientX, clientY).then(action => {
  901. this._drag = null;
  902. clearTimeout(this._selectTimer);
  903. });
  904. }
  905. /**
  906. * Handle selection on a file node.
  907. */
  908. private _handleFileSelect(event: MouseEvent): void {
  909. // Fetch common variables.
  910. let items = this.sortedItems;
  911. let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
  912. clearTimeout(this._selectTimer);
  913. if (index === -1) {
  914. return;
  915. }
  916. // Clear any existing soft selection.
  917. this._softSelection = '';
  918. let name = items[index].name;
  919. let selected = Object.keys(this._selection);
  920. // Handle toggling.
  921. if ((IS_MAC && event.metaKey) || (!IS_MAC && event.ctrlKey)) {
  922. if (this._selection[name]) {
  923. delete this._selection[name];
  924. } else {
  925. this._selection[name] = true;
  926. }
  927. // Handle multiple select.
  928. } else if (event.shiftKey) {
  929. this._handleMultiSelect(selected, index);
  930. // Handle a 'soft' selection
  931. } else if (name in this._selection && selected.length > 1) {
  932. this._softSelection = name;
  933. // Default to selecting the only the item.
  934. } else {
  935. // Handle a rename.
  936. if (selected.length === 1 && selected[0] === name) {
  937. this._selectTimer = setTimeout(() => {
  938. if (this._noSelectTimer === -1) {
  939. this._doRename();
  940. }
  941. }, RENAME_DURATION);
  942. }
  943. // Select only the given item.
  944. this._selection = Object.create(null);
  945. this._selection[name] = true;
  946. }
  947. this._isCut = false;
  948. this.update();
  949. }
  950. /**
  951. * Handle a multiple select on a file item node.
  952. */
  953. private _handleMultiSelect(selected: string[], index: number): void {
  954. // Find the "nearest selected".
  955. let items = this.sortedItems;
  956. let nearestIndex = -1;
  957. for (let i = 0; i < this._items.length; i++) {
  958. if (i === index) {
  959. continue;
  960. }
  961. let name = items[i].name;
  962. if (selected.indexOf(name) !== -1) {
  963. if (nearestIndex === -1) {
  964. nearestIndex = i;
  965. } else {
  966. if (Math.abs(index - i) < Math.abs(nearestIndex - i)) {
  967. nearestIndex = i;
  968. }
  969. }
  970. }
  971. }
  972. // Default to the first element (and fill down).
  973. if (nearestIndex === -1) {
  974. nearestIndex = 0;
  975. }
  976. // Select the rows between the current and the nearest selected.
  977. for (let i = 0; i < this._items.length; i++) {
  978. if (nearestIndex >= i && index <= i ||
  979. nearestIndex <= i && index >= i) {
  980. this._selection[items[i].name] = true;
  981. }
  982. }
  983. }
  984. /**
  985. * Get the currently selected items.
  986. */
  987. private _getSelectedItems(): Contents.IModel[] {
  988. let items = this.sortedItems;
  989. return items.filter(item => this._selection[item.name]);
  990. }
  991. /**
  992. * Copy the selected items, and optionally cut as well.
  993. */
  994. private _copy(): void {
  995. this._clipboard = [];
  996. for (let item of this._getSelectedItems()) {
  997. if (item.type !== 'directory') {
  998. // Store the absolute path of the item.
  999. this._clipboard.push('/' + item.path);
  1000. }
  1001. }
  1002. this.update();
  1003. }
  1004. /**
  1005. * Delete the files with the given names.
  1006. */
  1007. private _delete(names: string[]): Promise<void> {
  1008. let promises: Promise<void>[] = [];
  1009. for (let name of names) {
  1010. promises.push(this._model.deleteFile(name));
  1011. }
  1012. return Promise.all(promises).then(
  1013. () => this._model.refresh(),
  1014. error => utils.showErrorMessage(this, 'Delete file', error)
  1015. );
  1016. }
  1017. /**
  1018. * Allow the user to rename item on a given row.
  1019. */
  1020. private _doRename(): Promise<string> {
  1021. let items = this.sortedItems;
  1022. let name = Object.keys(this._selection)[0];
  1023. let index = findIndex(items, value => value.name === name);
  1024. let row = this._items[index];
  1025. let item = items[index];
  1026. let nameNode = this.renderer.getNameNode(row);
  1027. let original = item.name;
  1028. this._editNode.value = original;
  1029. return Private.doRename(nameNode, this._editNode).then(newName => {
  1030. if (newName === original) {
  1031. return;
  1032. }
  1033. return renameFile(this._model, original, newName).catch(error => {
  1034. utils.showErrorMessage(this, 'Rename Error', error);
  1035. return original;
  1036. }).then(() => {
  1037. this._model.refresh();
  1038. return newName;
  1039. });
  1040. });
  1041. }
  1042. /**
  1043. * Select a given item.
  1044. */
  1045. private _selectItem(index: number, keepExisting: boolean) {
  1046. // Selected the given row(s)
  1047. let items = this.sortedItems;
  1048. if (!keepExisting) {
  1049. this._selection = Object.create(null);
  1050. }
  1051. let name = items[index].name;
  1052. this._selection[name] = true;
  1053. scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
  1054. this._isCut = false;
  1055. }
  1056. /**
  1057. * Handle the `refreshed` signal from the model.
  1058. */
  1059. private _onModelRefreshed(): void {
  1060. // Update the selection.
  1061. let existing = Object.keys(this._selection);
  1062. this._selection = Object.create(null);
  1063. each(this._model.items, item => {
  1064. let name = item.name;
  1065. if (existing.indexOf(name) !== -1) {
  1066. this._selection[name] = true;
  1067. }
  1068. });
  1069. // Update the sorted items.
  1070. this.sort(this.sortState);
  1071. }
  1072. /**
  1073. * Handle a `pathChanged` signal from the model.
  1074. */
  1075. private _onPathChanged(): void {
  1076. // Reset the selection.
  1077. this._selection = Object.create(null);
  1078. // Update the sorted items.
  1079. this.sort(this.sortState);
  1080. }
  1081. private _model: FileBrowserModel = null;
  1082. private _editNode: HTMLInputElement = null;
  1083. private _items: HTMLElement[] = [];
  1084. private _sortedModels: Contents.IModel[] = null;
  1085. private _sortState: DirListing.ISortState = { direction: 'ascending', key: 'name' };
  1086. private _drag: Drag = null;
  1087. private _dragData: { pressX: number, pressY: number, index: number } = null;
  1088. private _selectTimer = -1;
  1089. private _noSelectTimer = -1;
  1090. private _isCut = false;
  1091. private _prevPath = '';
  1092. private _clipboard: string[] = [];
  1093. private _manager: DocumentManager = null;
  1094. private _opener: IWidgetOpener = null;
  1095. private _softSelection = '';
  1096. private _inContext = false;
  1097. private _selection: { [key: string]: boolean; } = Object.create(null);
  1098. private _renderer: DirListing.IRenderer = null;
  1099. }
  1100. /**
  1101. * The namespace for the `DirListing` class statics.
  1102. */
  1103. export
  1104. namespace DirListing {
  1105. /**
  1106. * An options object for initializing a file browser directory listing.
  1107. */
  1108. export
  1109. interface IOptions {
  1110. /**
  1111. * A file browser model instance.
  1112. */
  1113. model: FileBrowserModel;
  1114. /**
  1115. * A document manager instance.
  1116. */
  1117. manager: DocumentManager;
  1118. /**
  1119. * A widget opener function.
  1120. */
  1121. opener: IWidgetOpener;
  1122. /**
  1123. * A renderer for file items.
  1124. *
  1125. * The default is a shared `Renderer` instance.
  1126. */
  1127. renderer?: IRenderer;
  1128. }
  1129. /**
  1130. * A sort state.
  1131. */
  1132. export
  1133. interface ISortState {
  1134. /**
  1135. * The direction of sort.
  1136. */
  1137. direction: 'ascending' | 'descending';
  1138. /**
  1139. * The sort key.
  1140. */
  1141. key: 'name' | 'last_modified';
  1142. }
  1143. /**
  1144. * The render interface for file browser listing options.
  1145. */
  1146. export
  1147. interface IRenderer {
  1148. /**
  1149. * Create the DOM node for a dir listing.
  1150. */
  1151. createNode(): HTMLElement;
  1152. /**
  1153. * Populate and empty header node for a dir listing.
  1154. *
  1155. * @param node - The header node to populate.
  1156. */
  1157. populateHeaderNode(node: HTMLElement): void;
  1158. /**
  1159. * Handle a header click.
  1160. *
  1161. * @param node - A node populated by [[populateHeaderNode]].
  1162. *
  1163. * @param event - A click event on the node.
  1164. *
  1165. * @returns The sort state of the header after the click event.
  1166. */
  1167. handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState;
  1168. /**
  1169. * Create a new item node for a dir listing.
  1170. *
  1171. * @returns A new DOM node to use as a content item.
  1172. */
  1173. createItemNode(): HTMLElement;
  1174. /**
  1175. * Update an item node to reflect the current state of a model.
  1176. *
  1177. * @param node - A node created by [[createItemNode]].
  1178. *
  1179. * @param model - The model object to use for the item state.
  1180. */
  1181. updateItemNode(node: HTMLElement, model: Contents.IModel): void;
  1182. /**
  1183. * Get the node containing the file name.
  1184. *
  1185. * @param node - A node created by [[createItemNode]].
  1186. *
  1187. * @returns The node containing the file name.
  1188. */
  1189. getNameNode(node: HTMLElement): HTMLElement;
  1190. /**
  1191. * Create an appropriate drag image for an item.
  1192. *
  1193. * @param node - A node created by [[createItemNode]].
  1194. *
  1195. * @param count - The number of items being dragged.
  1196. *
  1197. * @returns An element to use as the drag image.
  1198. */
  1199. createDragImage(node: HTMLElement, count: number): HTMLElement;
  1200. }
  1201. /**
  1202. * The default implementation of an `IRenderer`.
  1203. */
  1204. export
  1205. class Renderer implements IRenderer {
  1206. /**
  1207. * Create the DOM node for a dir listing.
  1208. */
  1209. createNode(): HTMLElement {
  1210. let node = document.createElement('div');
  1211. let header = document.createElement('div');
  1212. let content = document.createElement('ul');
  1213. content.className = CONTENT_CLASS;
  1214. header.className = HEADER_CLASS;
  1215. node.appendChild(header);
  1216. node.appendChild(content);
  1217. node.tabIndex = 1;
  1218. return node;
  1219. }
  1220. /**
  1221. * Populate and empty header node for a dir listing.
  1222. *
  1223. * @param node - The header node to populate.
  1224. */
  1225. populateHeaderNode(node: HTMLElement): void {
  1226. let name = this._createHeaderItemNode('Name');
  1227. let modified = this._createHeaderItemNode('Last Modified');
  1228. name.classList.add(NAME_ID_CLASS);
  1229. name.classList.add(SELECTED_CLASS);
  1230. modified.classList.add(MODIFIED_ID_CLASS);
  1231. node.appendChild(name);
  1232. node.appendChild(modified);
  1233. }
  1234. /**
  1235. * Handle a header click.
  1236. *
  1237. * @param node - A node populated by [[populateHeaderNode]].
  1238. *
  1239. * @param event - A click event on the node.
  1240. *
  1241. * @returns The sort state of the header after the click event.
  1242. */
  1243. handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState {
  1244. let name = utils.findElement(node, NAME_ID_CLASS);
  1245. let modified = utils.findElement(node, MODIFIED_ID_CLASS);
  1246. let state: ISortState = { direction: 'ascending', key: 'name' };
  1247. let target = event.target as HTMLElement;
  1248. if (name.contains(target)) {
  1249. if (name.classList.contains(SELECTED_CLASS)) {
  1250. if (!name.classList.contains(DESCENDING_CLASS)) {
  1251. state.direction = 'descending';
  1252. name.classList.add(DESCENDING_CLASS);
  1253. } else {
  1254. name.classList.remove(DESCENDING_CLASS);
  1255. }
  1256. } else {
  1257. name.classList.remove(DESCENDING_CLASS);
  1258. }
  1259. name.classList.add(SELECTED_CLASS);
  1260. modified.classList.remove(SELECTED_CLASS);
  1261. modified.classList.remove(DESCENDING_CLASS);
  1262. return state;
  1263. }
  1264. if (modified.contains(target)) {
  1265. state.key = 'last_modified';
  1266. if (modified.classList.contains(SELECTED_CLASS)) {
  1267. if (!modified.classList.contains(DESCENDING_CLASS)) {
  1268. state.direction = 'descending';
  1269. modified.classList.add(DESCENDING_CLASS);
  1270. } else {
  1271. modified.classList.remove(DESCENDING_CLASS);
  1272. }
  1273. } else {
  1274. modified.classList.remove(DESCENDING_CLASS);
  1275. }
  1276. modified.classList.add(SELECTED_CLASS);
  1277. name.classList.remove(SELECTED_CLASS);
  1278. name.classList.remove(DESCENDING_CLASS);
  1279. return state;
  1280. }
  1281. return void 0;
  1282. }
  1283. /**
  1284. * Create a new item node for a dir listing.
  1285. *
  1286. * @returns A new DOM node to use as a content item.
  1287. */
  1288. createItemNode(): HTMLElement {
  1289. let node = document.createElement('li');
  1290. let icon = document.createElement('span');
  1291. let text = document.createElement('span');
  1292. let modified = document.createElement('span');
  1293. icon.className = ITEM_ICON_CLASS;
  1294. text.className = ITEM_TEXT_CLASS;
  1295. modified.className = ITEM_MODIFIED_CLASS;
  1296. node.appendChild(icon);
  1297. node.appendChild(text);
  1298. node.appendChild(modified);
  1299. return node;
  1300. }
  1301. /**
  1302. * Update an item node to reflect the current state of a model.
  1303. *
  1304. * @param node - A node created by [[createItemNode]].
  1305. *
  1306. * @param model - The model object to use for the item state.
  1307. */
  1308. updateItemNode(node: HTMLElement, model: Contents.IModel): void {
  1309. let icon = utils.findElement(node, ITEM_ICON_CLASS);
  1310. let text = utils.findElement(node, ITEM_TEXT_CLASS);
  1311. let modified = utils.findElement(node, ITEM_MODIFIED_CLASS);
  1312. icon.className = ITEM_ICON_CLASS;
  1313. switch (model.type) {
  1314. case 'directory':
  1315. icon.classList.add(FOLDER_TYPE_CLASS);
  1316. break;
  1317. case 'notebook':
  1318. icon.classList.add(NOTEBOOK_TYPE_CLASS);
  1319. break;
  1320. default:
  1321. icon.classList.add(FILE_TYPE_CLASS);
  1322. break;
  1323. }
  1324. let modText = '';
  1325. let modTitle = '';
  1326. if (model.last_modified) {
  1327. modText = humanTime(model.last_modified);
  1328. modTitle = dateTime(model.last_modified);
  1329. }
  1330. // If an item is being edited currently, its text node is unavailable.
  1331. if (text) {
  1332. text.textContent = model.name;
  1333. }
  1334. modified.textContent = modText;
  1335. modified.title = modTitle;
  1336. }
  1337. /**
  1338. * Get the node containing the file name.
  1339. *
  1340. * @param node - A node created by [[createItemNode]].
  1341. *
  1342. * @returns The node containing the file name.
  1343. */
  1344. getNameNode(node: HTMLElement): HTMLElement {
  1345. return utils.findElement(node, ITEM_TEXT_CLASS);
  1346. }
  1347. /**
  1348. * Create a drag image for an item.
  1349. *
  1350. * @param node - A node created by [[createItemNode]].
  1351. *
  1352. * @param count - The number of items being dragged.
  1353. *
  1354. * @returns An element to use as the drag image.
  1355. */
  1356. createDragImage(node: HTMLElement, count: number): HTMLElement {
  1357. let dragImage = node.cloneNode(true) as HTMLElement;
  1358. let modified = utils.findElement(dragImage, ITEM_MODIFIED_CLASS);
  1359. dragImage.removeChild(modified as HTMLElement);
  1360. if (count > 1) {
  1361. let nameNode = utils.findElement(dragImage, ITEM_TEXT_CLASS);
  1362. nameNode.textContent = '(' + count + ')';
  1363. let iconNode = utils.findElement(dragImage, ITEM_ICON_CLASS);
  1364. iconNode.className = `${ITEM_ICON_CLASS} ${FILE_TYPE_CLASS}`;
  1365. }
  1366. return dragImage;
  1367. }
  1368. /**
  1369. * Create a node for a header item.
  1370. */
  1371. private _createHeaderItemNode(label: string): HTMLElement {
  1372. let node = document.createElement('div');
  1373. let text = document.createElement('span');
  1374. let icon = document.createElement('span');
  1375. node.className = HEADER_ITEM_CLASS;
  1376. text.className = HEADER_ITEM_TEXT_CLASS;
  1377. icon.className = HEADER_ITEM_ICON_CLASS;
  1378. text.textContent = label;
  1379. node.appendChild(text);
  1380. node.appendChild(icon);
  1381. return node;
  1382. }
  1383. }
  1384. /**
  1385. * The default `IRenderer` instance.
  1386. */
  1387. export
  1388. const defaultRenderer = new Renderer();
  1389. }
  1390. /**
  1391. * The namespace for the listing private data.
  1392. */
  1393. namespace Private {
  1394. /**
  1395. * Handle editing text on a node.
  1396. *
  1397. * @returns Boolean indicating whether the name changed.
  1398. */
  1399. export
  1400. function doRename(text: HTMLElement, edit: HTMLInputElement): Promise<string> {
  1401. let changed = true;
  1402. let parent = text.parentElement as HTMLElement;
  1403. parent.replaceChild(edit, text);
  1404. edit.focus();
  1405. let index = edit.value.lastIndexOf('.');
  1406. if (index === -1) {
  1407. edit.setSelectionRange(0, edit.value.length);
  1408. } else {
  1409. edit.setSelectionRange(0, index);
  1410. }
  1411. return new Promise<string>((resolve, reject) => {
  1412. edit.onblur = () => {
  1413. parent.replaceChild(text, edit);
  1414. resolve(edit.value);
  1415. };
  1416. edit.onkeydown = (event: KeyboardEvent) => {
  1417. switch (event.keyCode) {
  1418. case 13: // Enter
  1419. event.stopPropagation();
  1420. event.preventDefault();
  1421. edit.blur();
  1422. break;
  1423. case 27: // Escape
  1424. event.stopPropagation();
  1425. event.preventDefault();
  1426. changed = false;
  1427. edit.blur();
  1428. break;
  1429. default:
  1430. break;
  1431. }
  1432. };
  1433. });
  1434. }
  1435. /**
  1436. * Sort a list of items by sort state as a new array.
  1437. */
  1438. export
  1439. function sort(items: ISequence<Contents.IModel>, state: DirListing.ISortState) : Contents.IModel[] {
  1440. let copy: Contents.IModel[] = [];
  1441. each(items, item => {
  1442. copy.push(item);
  1443. });
  1444. if (state.key === 'last_modified') {
  1445. copy.sort((a, b) => {
  1446. let valA = new Date(a.last_modified).getTime();
  1447. let valB = new Date(b.last_modified).getTime();
  1448. return valB - valA;
  1449. });
  1450. }
  1451. // Reverse the order if descending.
  1452. if (state.direction === 'descending') {
  1453. copy.reverse();
  1454. }
  1455. return copy;
  1456. }
  1457. }