crumbs.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ArrayExt } from '@phosphor/algorithm';
  4. import { Message } from '@phosphor/messaging';
  5. import { IDragEvent } from '@phosphor/dragdrop';
  6. import { ElementExt } from '@phosphor/domutils';
  7. import { Widget } from '@phosphor/widgets';
  8. import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
  9. import { PathExt, PageConfig } from '@jupyterlab/coreutils';
  10. import { renameFile } from '@jupyterlab/docmanager';
  11. import { FileBrowserModel } from './model';
  12. /**
  13. * The class name added to material icons
  14. */
  15. const MATERIAL_CLASS = 'jp-MaterialIcon';
  16. /**
  17. * The class name added to the breadcrumb node.
  18. */
  19. const BREADCRUMB_CLASS = 'jp-BreadCrumbs';
  20. /**
  21. * The class name added to add the folder icon for the breadcrumbs
  22. */
  23. const BREADCRUMB_HOME = 'jp-FolderIcon';
  24. /**
  25. * The class named associated to the ellipses icon
  26. */
  27. const BREADCRUMB_ELLIPSES = 'jp-EllipsesIcon';
  28. /**
  29. * The class name added to the breadcrumb node.
  30. */
  31. const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item';
  32. /**
  33. * Bread crumb paths.
  34. */
  35. const BREAD_CRUMB_PATHS = ['/', '../../', '../', ''];
  36. /**
  37. * The mime type for a contents drag object.
  38. */
  39. const CONTENTS_MIME = 'application/x-jupyter-icontents';
  40. /**
  41. * The class name added to drop targets.
  42. */
  43. const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
  44. /**
  45. * A class which hosts folder breadcrumbs.
  46. */
  47. export class BreadCrumbs extends Widget {
  48. /**
  49. * Construct a new file browser crumb widget.
  50. *
  51. * @param model - The file browser view model.
  52. */
  53. constructor(options: BreadCrumbs.IOptions) {
  54. super();
  55. this._model = options.model;
  56. this.addClass(BREADCRUMB_CLASS);
  57. this._crumbs = Private.createCrumbs();
  58. this._crumbSeps = Private.createCrumbSeparators();
  59. this.node.appendChild(this._crumbs[Private.Crumb.Home]);
  60. this._model.refreshed.connect(this.update, this);
  61. }
  62. /**
  63. * Handle the DOM events for the bread crumbs.
  64. *
  65. * @param event - The DOM event sent to the widget.
  66. *
  67. * #### Notes
  68. * This method implements the DOM `EventListener` interface and is
  69. * called in response to events on the panel's DOM node. It should
  70. * not be called directly by user code.
  71. */
  72. handleEvent(event: Event): void {
  73. switch (event.type) {
  74. case 'click':
  75. this._evtClick(event as MouseEvent);
  76. break;
  77. case 'p-dragenter':
  78. this._evtDragEnter(event as IDragEvent);
  79. break;
  80. case 'p-dragleave':
  81. this._evtDragLeave(event as IDragEvent);
  82. break;
  83. case 'p-dragover':
  84. this._evtDragOver(event as IDragEvent);
  85. break;
  86. case 'p-drop':
  87. this._evtDrop(event as IDragEvent);
  88. break;
  89. default:
  90. return;
  91. }
  92. }
  93. /**
  94. * A message handler invoked on an `'after-attach'` message.
  95. */
  96. protected onAfterAttach(msg: Message): void {
  97. super.onAfterAttach(msg);
  98. this.update();
  99. let node = this.node;
  100. node.addEventListener('click', this);
  101. node.addEventListener('p-dragenter', this);
  102. node.addEventListener('p-dragleave', this);
  103. node.addEventListener('p-dragover', this);
  104. node.addEventListener('p-drop', this);
  105. }
  106. /**
  107. * A message handler invoked on a `'before-detach'` message.
  108. */
  109. protected onBeforeDetach(msg: Message): void {
  110. super.onBeforeDetach(msg);
  111. let node = this.node;
  112. node.removeEventListener('click', this);
  113. node.removeEventListener('p-dragenter', this);
  114. node.removeEventListener('p-dragleave', this);
  115. node.removeEventListener('p-dragover', this);
  116. node.removeEventListener('p-drop', this);
  117. }
  118. /**
  119. * A handler invoked on an `'update-request'` message.
  120. */
  121. protected onUpdateRequest(msg: Message): void {
  122. // Update the breadcrumb list.
  123. const contents = this._model.manager.services.contents;
  124. const localPath = contents.localPath(this._model.path);
  125. Private.updateCrumbs(this._crumbs, this._crumbSeps, localPath);
  126. }
  127. /**
  128. * Handle the `'click'` event for the widget.
  129. */
  130. private _evtClick(event: MouseEvent): void {
  131. // Do nothing if it's not a left mouse press.
  132. if (event.button !== 0) {
  133. return;
  134. }
  135. // Find a valid click target.
  136. let node = event.target as HTMLElement;
  137. while (node && node !== this.node) {
  138. if (node.classList.contains(BREADCRUMB_ITEM_CLASS)) {
  139. let index = ArrayExt.findFirstIndex(
  140. this._crumbs,
  141. value => value === node
  142. );
  143. this._model
  144. .cd(BREAD_CRUMB_PATHS[index])
  145. .catch(error => showErrorMessage('Open Error', error));
  146. // Stop the event propagation.
  147. event.preventDefault();
  148. event.stopPropagation();
  149. return;
  150. }
  151. node = node.parentElement as HTMLElement;
  152. }
  153. }
  154. /**
  155. * Handle the `'p-dragenter'` event for the widget.
  156. */
  157. private _evtDragEnter(event: IDragEvent): void {
  158. if (event.mimeData.hasData(CONTENTS_MIME)) {
  159. let index = ArrayExt.findFirstIndex(this._crumbs, node =>
  160. ElementExt.hitTest(node, event.clientX, event.clientY)
  161. );
  162. if (index !== -1) {
  163. if (index !== Private.Crumb.Current) {
  164. this._crumbs[index].classList.add(DROP_TARGET_CLASS);
  165. event.preventDefault();
  166. event.stopPropagation();
  167. }
  168. }
  169. }
  170. }
  171. /**
  172. * Handle the `'p-dragleave'` event for the widget.
  173. */
  174. private _evtDragLeave(event: IDragEvent): void {
  175. event.preventDefault();
  176. event.stopPropagation();
  177. let dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
  178. if (dropTarget) {
  179. dropTarget.classList.remove(DROP_TARGET_CLASS);
  180. }
  181. }
  182. /**
  183. * Handle the `'p-dragover'` event for the widget.
  184. */
  185. private _evtDragOver(event: IDragEvent): void {
  186. event.preventDefault();
  187. event.stopPropagation();
  188. event.dropAction = event.proposedAction;
  189. let dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
  190. if (dropTarget) {
  191. dropTarget.classList.remove(DROP_TARGET_CLASS);
  192. }
  193. let index = ArrayExt.findFirstIndex(this._crumbs, node =>
  194. ElementExt.hitTest(node, event.clientX, event.clientY)
  195. );
  196. if (index !== -1) {
  197. this._crumbs[index].classList.add(DROP_TARGET_CLASS);
  198. }
  199. }
  200. /**
  201. * Handle the `'p-drop'` event for the widget.
  202. */
  203. private _evtDrop(event: IDragEvent): void {
  204. event.preventDefault();
  205. event.stopPropagation();
  206. if (event.proposedAction === 'none') {
  207. event.dropAction = 'none';
  208. return;
  209. }
  210. if (!event.mimeData.hasData(CONTENTS_MIME)) {
  211. return;
  212. }
  213. event.dropAction = event.proposedAction;
  214. let target = event.target as HTMLElement;
  215. while (target && target.parentElement) {
  216. if (target.classList.contains(DROP_TARGET_CLASS)) {
  217. target.classList.remove(DROP_TARGET_CLASS);
  218. break;
  219. }
  220. target = target.parentElement;
  221. }
  222. // Get the path based on the target node.
  223. let index = ArrayExt.findFirstIndex(this._crumbs, node => node === target);
  224. if (index === -1) {
  225. return;
  226. }
  227. const model = this._model;
  228. const path = PathExt.resolve(model.path, BREAD_CRUMB_PATHS[index]);
  229. const manager = model.manager;
  230. // Move all of the items.
  231. let promises: Promise<any>[] = [];
  232. let oldPaths = event.mimeData.getData(CONTENTS_MIME) as string[];
  233. for (let oldPath of oldPaths) {
  234. let localOldPath = manager.services.contents.localPath(oldPath);
  235. let name = PathExt.basename(localOldPath);
  236. let newPath = PathExt.join(path, name);
  237. promises.push(renameFile(manager, oldPath, newPath));
  238. }
  239. void Promise.all(promises).catch(err => {
  240. return showErrorMessage('Move Error', err);
  241. });
  242. }
  243. private _model: FileBrowserModel;
  244. private _crumbs: ReadonlyArray<HTMLElement>;
  245. private _crumbSeps: ReadonlyArray<HTMLElement>;
  246. }
  247. /**
  248. * The namespace for the `BreadCrumbs` class statics.
  249. */
  250. export namespace BreadCrumbs {
  251. /**
  252. * An options object for initializing a bread crumb widget.
  253. */
  254. export interface IOptions {
  255. /**
  256. * A file browser model instance.
  257. */
  258. model: FileBrowserModel;
  259. }
  260. }
  261. /**
  262. * The namespace for the crumbs private data.
  263. */
  264. namespace Private {
  265. /**
  266. * Breadcrumb item list enum.
  267. */
  268. export enum Crumb {
  269. Home,
  270. Ellipsis,
  271. Parent,
  272. Current
  273. }
  274. /**
  275. * Populate the breadcrumb node.
  276. */
  277. export function updateCrumbs(
  278. breadcrumbs: ReadonlyArray<HTMLElement>,
  279. separators: ReadonlyArray<HTMLElement>,
  280. path: string
  281. ) {
  282. let node = breadcrumbs[0].parentNode as HTMLElement;
  283. // Remove all but the home node.
  284. let firstChild = node.firstChild as HTMLElement;
  285. while (firstChild && firstChild.nextSibling) {
  286. node.removeChild(firstChild.nextSibling);
  287. }
  288. node.appendChild(separators[0]);
  289. let parts = path.split('/');
  290. if (parts.length > 2) {
  291. node.appendChild(breadcrumbs[Crumb.Ellipsis]);
  292. let grandParent = parts.slice(0, parts.length - 2).join('/');
  293. breadcrumbs[Crumb.Ellipsis].title = grandParent;
  294. node.appendChild(separators[1]);
  295. }
  296. if (path) {
  297. if (parts.length >= 2) {
  298. breadcrumbs[Crumb.Parent].textContent = parts[parts.length - 2];
  299. node.appendChild(breadcrumbs[Crumb.Parent]);
  300. let parent = parts.slice(0, parts.length - 1).join('/');
  301. breadcrumbs[Crumb.Parent].title = parent;
  302. node.appendChild(separators[2]);
  303. }
  304. breadcrumbs[Crumb.Current].textContent = parts[parts.length - 1];
  305. node.appendChild(breadcrumbs[Crumb.Current]);
  306. breadcrumbs[Crumb.Current].title = path;
  307. node.appendChild(separators[3]);
  308. }
  309. }
  310. /**
  311. * Create the breadcrumb nodes.
  312. */
  313. export function createCrumbs(): ReadonlyArray<HTMLElement> {
  314. let home = document.createElement('span');
  315. home.className = `${MATERIAL_CLASS} ${BREADCRUMB_HOME} ${BREADCRUMB_ITEM_CLASS}`;
  316. home.title = PageConfig.getOption('serverRoot') || 'Jupyter Server Root';
  317. let ellipsis = document.createElement('span');
  318. ellipsis.className =
  319. MATERIAL_CLASS + ' ' + BREADCRUMB_ELLIPSES + ' ' + BREADCRUMB_ITEM_CLASS;
  320. let parent = document.createElement('span');
  321. parent.className = BREADCRUMB_ITEM_CLASS;
  322. let current = document.createElement('span');
  323. current.className = BREADCRUMB_ITEM_CLASS;
  324. return [home, ellipsis, parent, current];
  325. }
  326. /**
  327. * Create the breadcrumb separator nodes.
  328. */
  329. export function createCrumbSeparators(): ReadonlyArray<HTMLElement> {
  330. let items: HTMLElement[] = [];
  331. // The maximum number of directories that will be shown in the crumbs
  332. const MAX_DIRECTORIES = 2;
  333. // Make separators for after each directory, one at the beginning, and one
  334. // after a possible ellipsis.
  335. for (let i = 0; i < MAX_DIRECTORIES + 2; i++) {
  336. let item = document.createElement('span');
  337. item.textContent = '/';
  338. items.push(item);
  339. }
  340. return items;
  341. }
  342. }