buttons.ts 12 KB

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