buttons.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Kernel
  5. } from '@jupyterlab/services';
  6. import {
  7. each
  8. } from 'phosphor/lib/algorithm/iteration';
  9. import {
  10. DisposableSet
  11. } from 'phosphor/lib/core/disposable';
  12. import {
  13. CommandRegistry
  14. } from 'phosphor/lib/ui/commandregistry';
  15. import {
  16. Keymap
  17. } from 'phosphor/lib/ui/keymap';
  18. import {
  19. Menu
  20. } from 'phosphor/lib/ui/menu';
  21. import {
  22. Widget
  23. } from 'phosphor/lib/ui/widget';
  24. import {
  25. showDialog
  26. } from '../dialog';
  27. import {
  28. DocumentManager
  29. } from '../docmanager';
  30. import {
  31. createFromDialog
  32. } from './dialogs';
  33. import {
  34. FileBrowserModel
  35. } from './model';
  36. import * as utils
  37. from './utils';
  38. /**
  39. * The class name added to a file buttons widget.
  40. */
  41. const FILE_BUTTONS_CLASS = 'jp-FileButtons';
  42. /**
  43. * The class name added to a button node.
  44. */
  45. const BUTTON_CLASS = 'jp-FileButtons-button';
  46. /**
  47. * The class name added to a button content node.
  48. */
  49. const CONTENT_CLASS = 'jp-FileButtons-buttonContent';
  50. /**
  51. * The class name added to a button icon node.
  52. */
  53. const ICON_CLASS = 'jp-FileButtons-buttonIcon';
  54. /**
  55. * The class name added to the create button.
  56. */
  57. const CREATE_CLASS = 'jp-id-create';
  58. /**
  59. * The class name added to the add button.
  60. */
  61. const MATERIAL_CREATE = 'jp-AddIcon';
  62. /**
  63. * The class name added to the upload button.
  64. */
  65. const MATERIAL_UPLOAD = 'jp-UploadIcon';
  66. /**
  67. * The class name added to the refresh button.
  68. */
  69. const MATERIAL_REFRESH = 'jp-RefreshIcon';
  70. /**
  71. * The class name added to the down caret.
  72. */
  73. const MATERIAL_DOWNCARET = 'jp-DownCaretIcon';
  74. /**
  75. * The class name added to a material icon button.
  76. */
  77. const MATERIAL_CLASS = 'jp-MaterialIcon';
  78. /**
  79. * The class name added to the upload button.
  80. */
  81. const UPLOAD_CLASS = 'jp-id-upload';
  82. /**
  83. * The class name added to the refresh button.
  84. */
  85. const REFRESH_CLASS = 'jp-id-refresh';
  86. /**
  87. * The class name added to an active create button.
  88. */
  89. const ACTIVE_CLASS = 'jp-mod-active';
  90. /**
  91. * The class name added to a dropdown icon.
  92. */
  93. const DROPDOWN_CLASS = 'jp-FileButtons-dropdownIcon';
  94. /**
  95. * A widget which hosts the file browser buttons.
  96. */
  97. export
  98. class FileButtons extends Widget {
  99. /**
  100. * Construct a new file browser buttons widget.
  101. */
  102. constructor(options: FileButtons.IOptions) {
  103. super();
  104. this.addClass(FILE_BUTTONS_CLASS);
  105. this._model = options.model;
  106. this._buttons.create.onmousedown = this._onCreateButtonPressed;
  107. this._buttons.upload.onclick = this._onUploadButtonClicked;
  108. this._buttons.refresh.onclick = this._onRefreshButtonClicked;
  109. this._input.onchange = this._onInputChanged;
  110. let node = this.node;
  111. node.appendChild(this._buttons.create);
  112. node.appendChild(this._buttons.upload);
  113. node.appendChild(this._buttons.refresh);
  114. this._commands = options.commands;
  115. this._keymap = options.keymap;
  116. this._manager = options.manager;
  117. }
  118. /**
  119. * Dispose of the resources held by the widget.
  120. */
  121. dispose(): void {
  122. this._buttons = null;
  123. this._commands = null;
  124. this._input = null;
  125. this._keymap = null;
  126. this._manager = null;
  127. this._model = null;
  128. super.dispose();
  129. }
  130. /**
  131. * Get the model used by the widget.
  132. */
  133. get model(): FileBrowserModel {
  134. return this._model;
  135. }
  136. /**
  137. * Get the document manager used by the widget.
  138. */
  139. get manager(): DocumentManager {
  140. return this._manager;
  141. }
  142. /**
  143. * Get the create button node.
  144. */
  145. get createNode(): HTMLButtonElement {
  146. return this._buttons.create;
  147. }
  148. /**
  149. * Get the upload button node.
  150. */
  151. get uploadNode(): HTMLButtonElement {
  152. return this._buttons.upload;
  153. }
  154. /**
  155. * Get the refresh button node.
  156. */
  157. get refreshNode(): HTMLButtonElement {
  158. return this._buttons.refresh;
  159. }
  160. /**
  161. * Create a file from a creator.
  162. *
  163. * @param creatorName - The name of the file creator.
  164. *
  165. * @returns A promise that resolves with the created widget.
  166. */
  167. createFrom(creatorName: string): Promise<Widget> {
  168. return createFromDialog(this.model, this.manager, creatorName);
  169. }
  170. /**
  171. * Open a file by path.
  172. *
  173. * @param path - The path of the file.
  174. *
  175. * @param widgetName - The name of the widget factory to use.
  176. *
  177. * @param kernel - The kernel model to use.
  178. *
  179. * @return The widget for the path.
  180. */
  181. open(path: string, widgetName='default', kernel?: Kernel.IModel): Widget {
  182. let widget = this._manager.openOrReveal(path, widgetName, kernel);
  183. return widget;
  184. }
  185. /**
  186. * Create a new file by path.
  187. *
  188. * @param path - The path of the file.
  189. *
  190. * @param widgetName - The name of the widget factory to use.
  191. *
  192. * @param kernel - The kernel model to use.
  193. *
  194. * @return The widget for the path.
  195. */
  196. createNew(path: string, widgetName='default', kernel?: Kernel.IModel): Widget {
  197. return this._manager.createNew(path, widgetName, kernel);
  198. }
  199. /**
  200. * The 'mousedown' handler for the create button.
  201. */
  202. private _onCreateButtonPressed = (event: MouseEvent) => {
  203. // Do nothing if nothing if it's not a left press.
  204. if (event.button !== 0) {
  205. return;
  206. }
  207. // Do nothing if the create button is already active.
  208. let button = this._buttons.create;
  209. if (button.classList.contains(ACTIVE_CLASS)) {
  210. return;
  211. }
  212. // Create a new dropdown menu and snap the button geometry.
  213. let commands = this._commands;
  214. let keymap = this._keymap;
  215. let dropdown = Private.createDropdownMenu(this, commands, keymap);
  216. let rect = button.getBoundingClientRect();
  217. // Mark the button as active.
  218. button.classList.add(ACTIVE_CLASS);
  219. // Setup the `aboutToClose` signal handler. The menu is disposed on an
  220. // animation frame to allow a mouse press event which closed the
  221. // menu to run its course. This keeps the button from re-opening.
  222. dropdown.aboutToClose.connect(() => {
  223. requestAnimationFrame(() => { dropdown.dispose(); });
  224. });
  225. // Setup the `disposed` signal handler. This restores the button
  226. // to the non-active state and allows a new menu to be opened.
  227. dropdown.disposed.connect(() => {
  228. button.classList.remove(ACTIVE_CLASS);
  229. });
  230. // Popup the menu aligned with the bottom of the create button.
  231. dropdown.open(rect.left, rect.bottom, { forceX: false, forceY: false });
  232. };
  233. /**
  234. * The 'click' handler for the upload button.
  235. */
  236. private _onUploadButtonClicked = (event: MouseEvent) => {
  237. if (event.button !== 0) {
  238. return;
  239. }
  240. this._input.click();
  241. };
  242. /**
  243. * The 'click' handler for the refresh button.
  244. */
  245. private _onRefreshButtonClicked = (event: MouseEvent) => {
  246. if (event.button !== 0) {
  247. return;
  248. }
  249. // Force a refresh of the current directory.
  250. this._model.cd('.');
  251. };
  252. /**
  253. * The 'change' handler for the input field.
  254. */
  255. private _onInputChanged = () => {
  256. let files = Array.prototype.slice.call(this._input.files);
  257. Private.uploadFiles(this, files as File[]);
  258. };
  259. private _buttons = Private.createButtons();
  260. private _commands: CommandRegistry = null;
  261. private _input = Private.createUploadInput();
  262. private _keymap: Keymap = null;
  263. private _manager: DocumentManager = null;
  264. private _model: FileBrowserModel;
  265. }
  266. /**
  267. * The namespace for the `FileButtons` class statics.
  268. */
  269. export
  270. namespace FileButtons {
  271. /**
  272. * An options object for initializing a file buttons widget.
  273. */
  274. export
  275. interface IOptions {
  276. /**
  277. * The command registry for use with the file buttons.
  278. */
  279. commands: CommandRegistry;
  280. /**
  281. * The keymap for use with the file buttons.
  282. */
  283. keymap: Keymap;
  284. /**
  285. * A file browser model instance.
  286. */
  287. model: FileBrowserModel;
  288. /**
  289. * A document manager instance.
  290. */
  291. manager: DocumentManager;
  292. }
  293. }
  294. /**
  295. * The namespace for the `FileButtons` private data.
  296. */
  297. namespace Private {
  298. /**
  299. * The ID counter prefix for new commands.
  300. *
  301. * #### Notes
  302. * Even though the commands are disposed when the dropdown menu is disposed,
  303. * in order to guarantee there are no race conditions with other `FileButtons`
  304. * instances, each set of commands is prefixed.
  305. */
  306. let id = 0;
  307. /**
  308. * An object which holds the button nodes for a file buttons widget.
  309. */
  310. export
  311. interface IButtonGroup {
  312. create: HTMLButtonElement;
  313. upload: HTMLButtonElement;
  314. refresh: HTMLButtonElement;
  315. }
  316. /**
  317. * Create the button group for a file buttons widget.
  318. */
  319. export
  320. function createButtons(): IButtonGroup {
  321. let create = document.createElement('button');
  322. let upload = document.createElement('button');
  323. let refresh = document.createElement('button');
  324. let createContent = document.createElement('span');
  325. let uploadContent = document.createElement('span');
  326. let refreshContent = document.createElement('span');
  327. let createIcon = document.createElement('span');
  328. let uploadIcon = document.createElement('span');
  329. let refreshIcon = document.createElement('span');
  330. let dropdownIcon = document.createElement('span');
  331. create.type = 'button';
  332. upload.type = 'button';
  333. refresh.type = 'button';
  334. create.title = 'Create New...';
  335. upload.title = 'Upload File(s)';
  336. refresh.title = 'Refresh File List';
  337. create.className = `${BUTTON_CLASS} ${CREATE_CLASS}`;
  338. upload.className = `${BUTTON_CLASS} ${UPLOAD_CLASS}`;
  339. refresh.className = `${BUTTON_CLASS} ${REFRESH_CLASS}`;
  340. createContent.className = CONTENT_CLASS;
  341. uploadContent.className = CONTENT_CLASS;
  342. refreshContent.className = CONTENT_CLASS;
  343. // TODO make these icons configurable.
  344. createIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_CREATE;
  345. uploadIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_UPLOAD;
  346. refreshIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_REFRESH;
  347. dropdownIcon.className = DROPDOWN_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_DOWNCARET;
  348. createContent.appendChild(createIcon);
  349. createContent.appendChild(dropdownIcon);
  350. uploadContent.appendChild(uploadIcon);
  351. refreshContent.appendChild(refreshIcon);
  352. create.appendChild(createContent);
  353. upload.appendChild(uploadContent);
  354. refresh.appendChild(refreshContent);
  355. return { create, upload, refresh };
  356. }
  357. /**
  358. * Create the upload input node for a file buttons widget.
  359. */
  360. export
  361. function createUploadInput(): HTMLInputElement {
  362. let input = document.createElement('input');
  363. input.type = 'file';
  364. input.multiple = true;
  365. return input;
  366. }
  367. /**
  368. * Create a new folder.
  369. */
  370. export
  371. function createNewFolder(widget: FileButtons): void {
  372. widget.model.newUntitled({ type: 'directory' }).catch(error => {
  373. utils.showErrorMessage('New Folder Error', error);
  374. });
  375. }
  376. /**
  377. * Create a new dropdown menu for the create new button.
  378. */
  379. export
  380. function createDropdownMenu(widget: FileButtons, commands: CommandRegistry, keymap: Keymap): Menu {
  381. let menu = new Menu({ commands, keymap });
  382. let prefix = `file-buttons-${++id}`;
  383. let disposables = new DisposableSet();
  384. let registry = widget.manager.registry;
  385. let command: string;
  386. // Remove all the commands associated with this menu upon disposal.
  387. menu.disposed.connect(() => disposables.dispose());
  388. command = `${prefix}:new-text-folder`;
  389. disposables.add(commands.addCommand(command, {
  390. execute: () => { createNewFolder(widget); },
  391. label: 'Folder'
  392. }));
  393. menu.addItem({ command });
  394. each(registry.creators(), creator => {
  395. command = `${prefix}:new-${creator.name}`;
  396. disposables.add(commands.addCommand(command, {
  397. execute: () => {
  398. widget.createFrom(creator.name);
  399. },
  400. label: creator.name
  401. }));
  402. menu.addItem({ command });
  403. });
  404. return menu;
  405. }
  406. /**
  407. * Upload an array of files to the server.
  408. */
  409. export
  410. function uploadFiles(widget: FileButtons, files: File[]): void {
  411. let pending = files.map(file => uploadFile(widget, file));
  412. Promise.all(pending).catch(error => {
  413. utils.showErrorMessage('Upload Error', error);
  414. });
  415. }
  416. /**
  417. * Upload a file to the server.
  418. */
  419. function uploadFile(widget: FileButtons, file: File): Promise<any> {
  420. return widget.model.upload(file).catch(error => {
  421. let exists = error.message.indexOf('already exists') !== -1;
  422. if (exists) {
  423. return uploadFileOverride(widget, file);
  424. }
  425. throw error;
  426. });
  427. }
  428. /**
  429. * Upload a file to the server checking for override.
  430. */
  431. function uploadFileOverride(widget: FileButtons, file: File): Promise<any> {
  432. let options = {
  433. title: 'Overwrite File?',
  434. body: `"${file.name}" already exists, overwrite?`
  435. };
  436. return showDialog(options).then(button => {
  437. if (widget.isDisposed || button.text !== 'Ok') {
  438. return;
  439. }
  440. return widget.model.upload(file, true);
  441. });
  442. }
  443. }