listing.ts 44 KB

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