listing.ts 42 KB

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