listing.ts 49 KB

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