Browse Source

wip filebrowser refactor

Steven Silvester 8 years ago
parent
commit
b0a7bc2536
4 changed files with 184 additions and 189 deletions
  1. 1 1
      src/filebrowser/browser.ts
  2. 1 1
      src/filebrowser/dialogs.ts
  3. 179 103
      src/filebrowser/listing.ts
  4. 3 84
      src/filebrowser/model.ts

+ 1 - 1
src/filebrowser/browser.ts

@@ -152,7 +152,7 @@ class FileBrowserWidget extends Widget {
    */
   open(): void {
     let foundDir = false;
-    let items = this._model.sortedItems;
+    let items = this._model.items;
     for (let item of items) {
       if (!this._listing.isSelected(item.name)) {
         continue;

+ 1 - 1
src/filebrowser/dialogs.ts

@@ -303,7 +303,7 @@ class CreateNewHandler extends Widget {
    */
   protected inputChanged(): void {
     let path = this.input.value;
-    for (let item of this._model.sortedItems) {
+    for (let item of this._model.items) {
       if (item.path === path) {
         this.addClass(FILE_CONFLICT_CLASS);
         return;

+ 179 - 103
src/filebrowser/listing.ts

@@ -260,6 +260,40 @@ class DirListing extends Widget {
     return utils.findElement(this.node, CONTENT_CLASS);
   }
 
+  /**
+   * The renderer instance used by the directory listing.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get renderer(): DirListing.IRenderer {
+    return this._renderer;
+  }
+
+  /**
+   * The the sorted content items.
+   */
+  get sortedItems(): IContentsModel[] {
+    return this._sortedModels;
+  }
+
+  /**
+   * The current sort state.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get sortState(): DirListing.ISortState {
+    return this._sortState;
+  }
+
+  /**
+   * Sort the items using a sort condition.
+   */
+  sort(state: DirListing.ISortState): void {
+
+  }
+
   /**
    * Rename the first currently selected item.
    */
@@ -321,7 +355,7 @@ class DirListing extends Widget {
     if (this._softSelection) {
       names.push(this._softSelection);
     } else {
-      let items = this._model.sortedItems;
+      let items = this._model.items;
       for (let item of items) {
         if (this._selection[item.name]) {
           names.push(item.name);
@@ -381,7 +415,7 @@ class DirListing extends Widget {
    */
   shutdownKernels(): Promise<void> {
     let promises: Promise<void>[] = [];
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let paths = items.map(item => item.path);
     for (let sessionId of this._model.sessionIds) {
       let index = paths.indexOf(sessionId.notebook.path);
@@ -405,22 +439,26 @@ class DirListing extends Widget {
   selectNext(keepExisting = false): void {
     let index = -1;
     let selected = Object.keys(this._selection);
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     if (selected.length === 1 || keepExisting) {
       // Select the next item.
       let name = selected[selected.length - 1];
-      index = arrays.findIndex(items, (value, index) => value.name === name);
+      index = arrays.findIndex(items, (value) => value.name === name);
       index += 1;
-      if (index === this._items.length) index = 0;
+      if (index === this._items.length) {
+        index = 0;
+      }
     } else if (selected.length === 0) {
       // Select the first item.
       index = 0;
     } else {
       // Select the last selected item.
       let name = selected[selected.length - 1];
-      index = arrays.findIndex(items, (value, index) => value.name === name);
+      index = arrays.findIndex(items, (value) => value.name === name);
+    }
+    if (index !== -1) {
+      this._selectItem(index, keepExisting);
     }
-    if (index !== -1) this._selectItem(index, keepExisting);
   }
 
   /**
@@ -431,22 +469,26 @@ class DirListing extends Widget {
   selectPrevious(keepExisting = false): void {
     let index = -1;
     let selected = Object.keys(this._selection);
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     if (selected.length === 1 || keepExisting) {
       // Select the previous item.
       let name = selected[0];
-      index = arrays.findIndex(items, (value, index) => value.name === name);
+      index = arrays.findIndex(items, (value) => value.name === name);
       index -= 1;
-      if (index === -1) index = this._items.length - 1;
+      if (index === -1) {
+        index = this._items.length - 1;
+      }
     } else if (selected.length === 0) {
       // Select the last item.
       index = this._items.length - 1;
     } else {
       // Select the first selected item.
       let name = selected[0];
-      index = arrays.findIndex(items, (value, index) => value.name === name);
+      index = arrays.findIndex(items, (value) => value.name === name);
+    }
+    if (index !== -1) {
+      this._selectItem(index, keepExisting);
     }
-    if (index !== -1) this._selectItem(index, keepExisting);
   }
 
   /**
@@ -547,7 +589,7 @@ class DirListing extends Widget {
    */
   protected onUpdateRequest(msg: Message): void {
     // Fetch common variables.
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let nodes = this._items;
     let content = utils.findElement(this.node, CONTENT_CLASS);
     let renderer = this._renderer;
@@ -564,6 +606,7 @@ class DirListing extends Widget {
     // Add any missing item nodes.
     while (nodes.length < items.length) {
       let node = renderer.createItemNode();
+      node.classList.add(ITEM_CLASS);
       nodes.push(node);
       content.appendChild(node);
     }
@@ -610,40 +653,8 @@ class DirListing extends Widget {
 
     let header = this.headerNode;
     if (header.contains(target)) {
-
-      let children = header.getElementsByClassName(HEADER_ITEM_CLASS);
-      let name = children[0] as HTMLElement;
-      let modified = children[1] as HTMLElement;
-
-      if (name.contains(target)) {
-        if (this._model.sortKey === 'name') {
-          let flag = !this._model.sortAscending;
-          this._model.sortAscending = flag;
-          if (flag) name.classList.remove(DESCENDING_CLASS);
-          else name.classList.add(DESCENDING_CLASS);
-        } else {
-          this._model.sortKey = 'name';
-          this._model.sortAscending = true;
-          name.classList.remove(DESCENDING_CLASS);
-        }
-        name.classList.add(SELECTED_CLASS);
-        modified.classList.remove(SELECTED_CLASS);
-        modified.classList.remove(DESCENDING_CLASS);
-      } else if (modified.contains(target)) {
-        if (this._model.sortKey === 'last_modified') {
-          let flag = !this._model.sortAscending;
-          this._model.sortAscending = flag;
-          if (flag) modified.classList.remove(DESCENDING_CLASS);
-          else modified.classList.add(DESCENDING_CLASS);
-        } else {
-          this._model.sortKey = 'last_modified';
-          this._model.sortAscending = true;
-          modified.classList.remove(DESCENDING_CLASS);
-        }
-        modified.classList.add(SELECTED_CLASS);
-        name.classList.remove(SELECTED_CLASS);
-        name.classList.remove(DESCENDING_CLASS);
-      }
+      let state = this.renderer.handleHeaderClick(header, event);
+      this.sort(state);
       this.update();
       return;
     }
@@ -692,7 +703,7 @@ class DirListing extends Widget {
       return;
     }
     this._softSelection = '';
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let selected = Object.keys(this._selection);
     if (selected.indexOf(items[index].name) === -1) {
       this._softSelection = items[index].name;
@@ -798,7 +809,7 @@ class DirListing extends Widget {
     }
 
     let model = this._model;
-    let item = model.sortedItems[i];
+    let item = this.sortedItems[i];
     if (item.type === 'directory') {
       model.cd(item.name).catch(error =>
         showErrorMessage(this, 'Open directory', error)
@@ -826,7 +837,7 @@ class DirListing extends Widget {
       if (index === -1) {
         return;
       }
-      let item = this._model.sortedItems[index];
+      let item = this.sortedItems[index];
       let target = this._items[index];
       if (!target.classList.contains(FOLDER_TYPE_CLASS)) {
         return;
@@ -847,7 +858,9 @@ class DirListing extends Widget {
     event.preventDefault();
     event.stopPropagation();
     let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
-    if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
+    if (dropTarget) {
+      dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
+    }
   }
 
   /**
@@ -858,7 +871,9 @@ class DirListing extends Widget {
     event.stopPropagation();
     event.dropAction = event.proposedAction;
     let dropTarget = utils.findElement(this.node, utils.DROP_TARGET_CLASS);
-    if (dropTarget) dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
+    if (dropTarget) {
+      dropTarget.classList.remove(utils.DROP_TARGET_CLASS);
+    }
     let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
     this._items[index].classList.add(utils.DROP_TARGET_CLASS);
   }
@@ -890,7 +905,7 @@ class DirListing extends Widget {
 
     // Get the path based on the target node.
     let index = this._items.indexOf(target);
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let path = items[index].name + '/';
 
     // Move all of the items.
@@ -932,7 +947,7 @@ class DirListing extends Widget {
     let selectedNames = Object.keys(this._selection);
     let source = this._items[index];
     let model = this._model;
-    let items = model.sortedItems;
+    let items = this.sortedItems;
     let item: IContentsModel = null;
 
     // If the source node is not selected, use just that node.
@@ -941,20 +956,15 @@ class DirListing extends Widget {
       selectedNames = [item.name];
     } else if (selectedNames.length === 1) {
       let name = selectedNames[0];
-      item = arrays.find(items, (value, index) => value.name === name);
+      item = arrays.find(items, (value) => value.name === name);
     }
 
     // Create the drag image.
-    let dragImage = source.cloneNode(true) as HTMLElement;
-    dragImage.removeChild(dragImage.lastChild);
-    if (selectedNames.length > 1) {
-      let text = utils.findElement(dragImage, ITEM_TEXT_CLASS);
-      text.textContent = '(' + selectedNames.length + ')';
-    }
+    let dragImage = this.renderer.createDragImage(source, selectedNames.length);
 
     // Set up the drag event.
     this._drag = new Drag({
-      dragImage: dragImage,
+      dragImage,
       mimeData: new MimeData(),
       supportedActions: DropActions.Move,
       proposedAction: DropAction.Move
@@ -987,12 +997,12 @@ class DirListing extends Widget {
    */
   private _handleFileSelect(event: MouseEvent): void {
     // Fetch common variables.
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let index = utils.hitTestNodes(this._items, event.clientX, event.clientY);
 
     clearTimeout(this._selectTimer);
 
-    if (index == -1) {
+    if (index === -1) {
       return;
     }
 
@@ -1033,7 +1043,7 @@ class DirListing extends Widget {
    */
   private _handleMultiSelect(selected: string[], index: number): void {
     // Find the "nearest selected".
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let nearestIndex = -1;
     for (let i = 0; i < this._items.length; i++) {
       if (i === index) {
@@ -1069,7 +1079,7 @@ class DirListing extends Widget {
    * Get the currently selected items.
    */
   private _getSelectedItems(): IContentsModel[] {
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     if (!this._softSelection) {
       return items.filter(item => this._selection[item.name]);
     }
@@ -1081,7 +1091,7 @@ class DirListing extends Widget {
    */
   private _copy(): void {
     this._clipboard = [];
-    for (var item of this._getSelectedItems()) {
+    for (let item of this._getSelectedItems()) {
       if (item.type !== 'directory') {
         // Store the absolute path of the item.
         this._clipboard.push('/' + item.path);
@@ -1108,11 +1118,11 @@ class DirListing extends Widget {
    * Allow the user to rename item on a given row.
    */
   private _doRename(): Promise<string> {
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     let name = this._softSelection || Object.keys(this._selection)[0];
-    let index = arrays.findIndex(items, (value, index) => value.name === name);
+    let index = arrays.findIndex(items, (value) => value.name === name);
     let row = this._items[index];
-    let text = utils.findElement(row, ITEM_TEXT_CLASS);
+    let text = this.renderer.getItemText(row);
     let original = text.textContent;
 
     return Private.doRename(row, text, this._editNode).then(changed => {
@@ -1159,7 +1169,7 @@ class DirListing extends Widget {
    */
   private _selectItem(index: number, keepExisting: boolean) {
     // Selected the given row(s)
-    let items = this._model.sortedItems;
+    let items = this.sortedItems;
     if (!keepExisting) {
       this._selection = Object.create(null);
     }
@@ -1173,13 +1183,17 @@ class DirListing extends Widget {
    * Handle the `refreshed` signal from the model.
    */
   private _onModelRefreshed(): void {
+    // Update the selection.
     let existing = Object.keys(this._selection);
     this._selection = Object.create(null);
-    for (let name of this._model.names) {
+    for (let item of this._model.items) {
+      let name = item.name;
       if (existing.indexOf(name) !== -1) {
         this._selection[name] = true;
       }
     }
+    // Update the sorted items.
+    this.sort(this.sortState);
     this.update();
   }
 
@@ -1189,11 +1203,15 @@ class DirListing extends Widget {
   private _onPathChanged(): void {
     // Reset the selection.
     this._selection = Object.create(null);
+    // Update the sorted items.
+    this.sort(this.sortState);
   }
 
   private _model: FileBrowserModel = null;
   private _editNode: HTMLInputElement = null;
   private _items: HTMLElement[] = [];
+  private _sortedModels: IContentsModel[] = null;
+  private _sortState: DirListing.ISortState = { direction: 'ascending', key: 'name' };
   private _drag: Drag = null;
   private _dragData: { pressX: number, pressY: number, index: number } = null;
   private _selectTimer = -1;
@@ -1242,6 +1260,21 @@ namespace DirListing {
     renderer?: IRenderer;
   }
 
+  /**
+   * A sort state.
+   */
+  export
+  interface ISortState {
+    /**
+     * The direction of sort.
+     */
+    direction: 'ascending' | 'descending';
+
+    /**
+     * The sort key.
+     */
+    key: 'name' | 'last_modified';
+  }
 
   /**
    * The render interface for file browser listing options.
@@ -1251,38 +1284,56 @@ namespace DirListing {
     /**
      * Populate and empty header node for a dir listing.
      *
-     * @returns A new DOM node to use as the dir listing header.
-     *
-     * #### Notes
-     * This method may be reimplemented to create custom headers.
+     * @param node - The header node to populate.
      */
     populateHeaderNode(node: HTMLElement): void;
 
+    /**
+     * Handle a header click.
+     *
+     * @param node - A node populated by [[populateHeaderNode]].
+     *
+     * @param event - A click event on the node.
+     *
+     * @returns The sort state of the header after the click event.
+     */
+    handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState;
+
     /**
      * Create a new item node for a dir listing.
      *
      * @returns A new DOM node to use as a content item.
-     *
-     * #### Notes
-     * This method may be reimplemented to create custom items.
      */
     createItemNode(): HTMLElement;
 
     /**
      * Update an item node to reflect the current state of a model.
      *
-     * @param node - A node created by a call to [[createItemNode]].
+     * @param node - A node created by [[createItemNode]].
      *
      * @param model - The model object to use for the item state.
+     */
+    updateItemNode(node: HTMLElement, model: IContentsModel): void;
+
+    /**
+     * Get the node containing the file name.
      *
-     * #### Notes
-     * This is called automatically when the item should be updated.
+     * @param node - A node created by [[createItemNode]].
      *
-     * If the [[createItemNode]] method is reimplemented, this method
-     * should also be reimplemented so that the item state is properly
-     * updated.
+     * @returns The node containing the file name.
      */
-    updateItemNode(node: HTMLElement, model: IContentsModel): void;
+    getNameNode(node: HTMLElement): HTMLElement;
+
+    /**
+     * Create an appropriate drag image for an item.
+     *
+     * @param node - A node created by [[createItemNode]].
+     *
+     * @param count - The number of items being dragged.
+     *
+     * @returns An element to use as the drag image.
+     */
+    createDragImage(node: HTMLElement, count: number): HTMLElement;
   }
 
   /**
@@ -1293,10 +1344,7 @@ namespace DirListing {
     /**
      * Populate and empty header node for a dir listing.
      *
-     * @returns A new DOM node to use as the dir listing header.
-     *
-     * #### Notes
-     * This method may be reimplemented to create custom headers.
+     * @param node - The header node to populate.
      */
     populateHeaderNode(node: HTMLElement): void {
       let name = this._createHeaderItemNode('Name');
@@ -1308,20 +1356,29 @@ namespace DirListing {
       node.appendChild(modified);
     }
 
+    /**
+     * Handle a header click.
+     *
+     * @param node - A node populated by [[populateHeaderNode]].
+     *
+     * @param event - A click event on the node.
+     *
+     * @returns The sort state of the header after the click event.
+     */
+    handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState {
+      return void 0;
+    }
+
     /**
      * Create a new item node for a dir listing.
      *
      * @returns A new DOM node to use as a content item.
-     *
-     * #### Notes
-     * This method may be reimplemented to create custom items.
      */
     createItemNode(): HTMLElement {
       let node = document.createElement('li');
       let icon = document.createElement('span');
       let text = document.createElement('span');
       let modified = document.createElement('span');
-      node.className = ITEM_CLASS;
       icon.className = ITEM_ICON_CLASS;
       text.className = ITEM_TEXT_CLASS;
       modified.className = ITEM_MODIFIED_CLASS;
@@ -1334,16 +1391,9 @@ namespace DirListing {
     /**
      * Update an item node to reflect the current state of a model.
      *
-     * @param node - A node created by a call to [[createItemNode]].
+     * @param node - A node created by [[createItemNode]].
      *
      * @param model - The model object to use for the item state.
-     *
-     * #### Notes
-     * This is called automatically when the item should be updated.
-     *
-     * If the [[createItemNode]] method is reimplemented, this method
-     * should also be reimplemented so that the item state is properly
-     * updated.
      */
     updateItemNode(node: HTMLElement, model: IContentsModel): void {
       let icon = node.firstChild as HTMLElement;
@@ -1377,6 +1427,30 @@ namespace DirListing {
       modified.title = modTitle;
     }
 
+    /**
+     * Get the node containing the file name.
+     *
+     * @param node - A node created by [[createItemNode]].
+     *
+     * @returns The node containing the file name.
+     */
+    getNameNode(node: HTMLElement): HTMLElement {
+      return void 0;
+    }
+
+    /**
+     * Create an appropriate drag image for an item.
+     *
+     * @param node - A node created by [[createItemNode]].
+     *
+     * @param count - The number of items being dragged.
+     *
+     * @returns An element to use as the drag image.
+     */
+    createDragImage(node: HTMLElement, count: number): HTMLElement {
+      return void 0;
+    }
+
     /**
      * Create a node for a header item.
      */
@@ -1431,7 +1505,9 @@ namespace Private {
         if (text.textContent === edit.value) {
           changed = false;
         }
-        if (changed) text.textContent = edit.value;
+        if (changed) {
+          text.textContent = edit.value;
+        }
         resolve(changed);
       };
       edit.onkeydown = (event: KeyboardEvent) => {

+ 3 - 84
src/filebrowser/model.ts

@@ -70,14 +70,10 @@ class FileBrowserModel implements IDisposable {
   }
 
   /**
-   * Get a read-only, unsorted list of file names in the current path.
-   *
-   * #### Notes
-   * This is a read-only property.
-   *
+   * Get a read-only list of the items in the current path.
    */
-  get names(): string[] {
-    return this._unsortedNames.slice();
+  get items(): IContentsModel[] {
+    return this._model.content ? this._model.content.slice() : [];
   }
 
   /**
@@ -104,46 +100,6 @@ class FileBrowserModel implements IDisposable {
     return this._specs;
   }
 
-  /**
-   * Get whether the items are sorted in ascending order.
-   */
-  get sortAscending(): boolean {
-    return this._ascending;
-  }
-
-  /**
-   * Set whether the items are sorted in ascending order.
-   */
-  set sortAscending(value: boolean) {
-    this._ascending = value;
-    this._sort();
-  }
-
-  /**
-   * Get which key the items are sorted on.
-   */
-  get sortKey(): string {
-    return this._sortKey;
-  }
-
-  /**
-   * Set which key the items are sorted on.
-   */
-  set sortKey(value: string) {
-    this._sortKey = value;
-    this._sort();
-  }
-
-  /**
-   * Get the sorted list of items.
-   *
-   * #### Notes
-   * This is a read-only property and should be treated as immutable.
-   */
-  get sortedItems(): IContentsModel[] {
-    return this._model.content;
-  }
-
   /**
    * Dispose of the resources held by the view model.
    */
@@ -167,11 +123,6 @@ class FileBrowserModel implements IDisposable {
     let oldValue = this.path;
     return this._contentsManager.get(newValue, {}).then(contents => {
       this._model = contents;
-      let content = contents.content as IContentsModel[];
-      this._unsortedNames = content.map((value, index) => value.name);
-      if (this._sortKey !== 'name' || !this._ascending) {
-        this._sort();
-      }
       return this._findSessions();
     }).then(() => {
       if (oldValue !== newValue) {
@@ -363,35 +314,6 @@ class FileBrowserModel implements IDisposable {
     });
   }
 
-  /**
-   * Sort the model items.
-   */
-  private _sort(): void {
-    if (!this._unsortedNames) {
-      return;
-    }
-    let items = this._model.content.slice() as IContentsModel[];
-    if (this._sortKey === 'name') {
-      items.sort((a, b) => {
-        let indexA = this._unsortedNames.indexOf(a.name);
-        let indexB = this._unsortedNames.indexOf(b.name);
-        return indexA - indexB;
-      });
-    } else if (this._sortKey === 'last_modified') {
-      items.sort((a, b) => {
-        let valA = new Date(a.last_modified).getTime();
-        let valB = new Date(b.last_modified).getTime();
-        return valB - valA;
-      });
-    }
-
-    // Reverse the order if descending.
-    if (!this._ascending) {
-      items.reverse();
-    }
-    this._model.content = items;
-  }
-
   /**
    * Perform the actual upload.
    */
@@ -464,9 +386,6 @@ class FileBrowserModel implements IDisposable {
   private _sessions: ISession.IModel[] = [];
   private _sessionManager: ISession.IManager = null;
   private _model: IContentsModel;
-  private _sortKey = 'name';
-  private _ascending = true;
-  private _unsortedNames: string[] = [];
   private _specs: IKernel.ISpecModels = null;
 }