| Index: resources/bookmark_manager/js/cr/ui/listselectionmodel.js
 | 
| ===================================================================
 | 
| --- resources/bookmark_manager/js/cr/ui/listselectionmodel.js	(revision 0)
 | 
| +++ resources/bookmark_manager/js/cr/ui/listselectionmodel.js	(revision 0)
 | 
| @@ -0,0 +1,442 @@
 | 
| +// Copyright (c) 2010 The Chromium Authors. All rights reserved.
 | 
| +// Use of this source code is governed by a BSD-style license that can be
 | 
| +// found in the LICENSE file.
 | 
| +
 | 
| +cr.define('cr.ui', function() {
 | 
| +  const Event = cr.Event;
 | 
| +  const EventTarget = cr.EventTarget;
 | 
| +
 | 
| +  /**
 | 
| +   * Creates a new selection model that is to be used with lists. This is
 | 
| +   * implemented for vertical lists but changing the behavior for horizontal
 | 
| +   * lists or icon views is a matter of overriding {@code getItemBefore},
 | 
| +   * {@code getItemAfter}, {@code getItemAbove} as well as {@code getItemBelow}.
 | 
| +   *
 | 
| +   * @constructor
 | 
| +   * @extends {!cr.EventTarget}
 | 
| +   */
 | 
| +  function ListSelectionModel(list) {
 | 
| +    this.list = list;
 | 
| +    this.selectedItems_ = {};
 | 
| +  }
 | 
| +
 | 
| +  ListSelectionModel.prototype = {
 | 
| +    __proto__: EventTarget.prototype,
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the item below (y axis) the given element.
 | 
| +     * @param {*} item The item to get the item below.
 | 
| +     * @return {*} The item below or null if not found.
 | 
| +     */
 | 
| +    getItemBelow: function(item) {
 | 
| +      return item.nextElementSibling;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the item above (y axis) the given element.
 | 
| +     * @param {*} item The item to get the item above.
 | 
| +     * @return {*} The item below or null if not found.
 | 
| +     */
 | 
| +    getItemAbove: function(item) {
 | 
| +      return item.previousElementSibling;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the item before (x axis) the given element. This returns null
 | 
| +     * by default but override this for icon view and horizontal selection
 | 
| +     * models.
 | 
| +     *
 | 
| +     * @param {*} item The item to get the item before.
 | 
| +     * @return {*} The item before or null if not found.
 | 
| +     */
 | 
| +    getItemBefore: function(item) {
 | 
| +      return null;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the item after (x axis) the given element. This returns null
 | 
| +     * by default but override this for icon view and horizontal selection
 | 
| +     * models.
 | 
| +     *
 | 
| +     * @param {*} item The item to get the item after.
 | 
| +     * @return {*} The item after or null if not found.
 | 
| +     */
 | 
| +    getItemAfter: function(item) {
 | 
| +      return null;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the next list item. This is the next logical and should not
 | 
| +     * depend on any kind of layout of the list.
 | 
| +     * @param {*} item The item to get the next item for.
 | 
| +     * @return {*} The next item or null if not found.
 | 
| +     */
 | 
| +    getNextItem: function(item) {
 | 
| +      return item.nextElementSibling;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Returns the prevous list item. This is the previous logical and should
 | 
| +     * not depend on any kind of layout of the list.
 | 
| +     * @param {*} item The item to get the previous item for.
 | 
| +     * @return {*} The previous item or null if not found.
 | 
| +     */
 | 
| +    getPreviousItem: function(item) {
 | 
| +      return item.previousElementSibling;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * @return {*} The first item.
 | 
| +     */
 | 
| +    getFirstItem: function() {
 | 
| +      return this.list.firstElementChild;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * @return {*} The last item.
 | 
| +     */
 | 
| +    getLastItem: function() {
 | 
| +      return this.list.lastElementChild;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Called by the view when the user does a mousedown or mouseup on the list.
 | 
| +     * @param {!Event} e The browser mousedown event.
 | 
| +     * @param {*} item The item that was under the mouse pointer, null if none.
 | 
| +     */
 | 
| +    handleMouseDownUp: function(e, item) {
 | 
| +      var anchorItem = this.anchorItem;
 | 
| +
 | 
| +      this.beginChange_();
 | 
| +
 | 
| +      if (!item && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
 | 
| +        this.clear();
 | 
| +      } else {
 | 
| +        var isDown = e.type == 'mousedown';
 | 
| +        if (!cr.isMac && e.ctrlKey) {
 | 
| +          // Handle ctrlKey on mouseup
 | 
| +          if (!isDown) {
 | 
| +            // toggle the current one and make it anchor item
 | 
| +            this.setItemSelected(item, !this.getItemSelected(item));
 | 
| +            this.leadItem = item;
 | 
| +            this.anchorItem = item;
 | 
| +          }
 | 
| +        } else if (e.shiftKey && anchorItem && anchorItem != item) {
 | 
| +          // Shift is done in mousedown
 | 
| +          if (isDown) {
 | 
| +            this.clearAllSelected_();
 | 
| +            this.leadItem = item;
 | 
| +            this.selectRange(anchorItem, item);
 | 
| +          }
 | 
| +        } else {
 | 
| +          // Right click for a context menu need to not clear the selection.
 | 
| +          var isRightClick = e.button == 2;
 | 
| +
 | 
| +          // If the item is selected this is handled in mouseup.
 | 
| +          var itemSelected = this.getItemSelected(item);
 | 
| +          if ((itemSelected && !isDown || !itemSelected && isDown) &&
 | 
| +              !(itemSelected && isRightClick)) {
 | 
| +            this.clearAllSelected_();
 | 
| +            this.setItemSelected(item, true);
 | 
| +            this.leadItem = item;
 | 
| +            this.anchorItem = item;
 | 
| +          }
 | 
| +        }
 | 
| +      }
 | 
| +
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Called by the view when it recieves a keydown event.
 | 
| +     * @param {Event} e The keydown event.
 | 
| +     */
 | 
| +    handleKeyDown: function(e) {
 | 
| +      var newItem = null;
 | 
| +      var leadItem = this.leadItem;
 | 
| +      var prevent = true;
 | 
| +
 | 
| +      // Ctrl/Meta+A
 | 
| +      if (e.keyCode == 65 &&
 | 
| +          (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
 | 
| +        this.selectAll();
 | 
| +        e.preventDefault();
 | 
| +        return;
 | 
| +      }
 | 
| +
 | 
| +      // Space
 | 
| +      if (e.keyCode == 32) {
 | 
| +        if (leadItem != null) {
 | 
| +          var selected = this.getItemSelected(leadItem);
 | 
| +          if (e.ctrlKey || !selected) {
 | 
| +            this.beginChange_();
 | 
| +            this.setItemSelected(leadItem, !selected);
 | 
| +            this.endChange_();
 | 
| +            return;
 | 
| +          }
 | 
| +        }
 | 
| +      }
 | 
| +
 | 
| +      switch (e.keyIdentifier) {
 | 
| +        case 'Home':
 | 
| +          newItem = this.getFirstItem();
 | 
| +          break;
 | 
| +        case 'End':
 | 
| +          newItem = this.getLastItem();
 | 
| +          break;
 | 
| +        case 'Up':
 | 
| +          newItem = !leadItem  ?
 | 
| +              this.getLastItem() : this.getItemAbove(leadItem);
 | 
| +          break;
 | 
| +        case 'Down':
 | 
| +          newItem = !leadItem ?
 | 
| +              this.getFirstItem() : this.getItemBelow(leadItem);
 | 
| +          break;
 | 
| +        case 'Left':
 | 
| +          newItem = !leadItem ?
 | 
| +              this.getLastItem() : this.getItemBefore(leadItem);
 | 
| +          break;
 | 
| +        case 'Right':
 | 
| +          newItem = !leadItem ?
 | 
| +              this.getFirstItem() : this.getItemAfter(leadItem);
 | 
| +          break;
 | 
| +        default:
 | 
| +          prevent = false;
 | 
| +      }
 | 
| +
 | 
| +      if (newItem) {
 | 
| +        this.beginChange_();
 | 
| +
 | 
| +        this.leadItem = newItem;
 | 
| +        if (e.shiftKey) {
 | 
| +          var anchorItem = this.anchorItem;
 | 
| +          this.clearAllSelected_();
 | 
| +          if (!anchorItem) {
 | 
| +            this.setItemSelected(newItem, true);
 | 
| +            this.anchorItem = newItem;
 | 
| +          } else {
 | 
| +            this.selectRange(anchorItem, newItem);
 | 
| +          }
 | 
| +        } else if (e.ctrlKey && !cr.isMac) {
 | 
| +          // Setting the lead item is done above
 | 
| +          // Mac does not allow you to change the lead.
 | 
| +        } else {
 | 
| +          this.clearAllSelected_();
 | 
| +          this.setItemSelected(newItem, true);
 | 
| +          this.anchorItem = newItem;
 | 
| +        }
 | 
| +
 | 
| +        this.endChange_();
 | 
| +
 | 
| +        if (prevent)
 | 
| +          e.preventDefault();
 | 
| +      }
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * @type {!Array} The selected items.
 | 
| +     */
 | 
| +    get selectedItems() {
 | 
| +      return Object.keys(this.selectedItems_).map(function(uid) {
 | 
| +        return this.selectedItems_[uid];
 | 
| +      }, this);
 | 
| +    },
 | 
| +    set selectedItems(selectedItems) {
 | 
| +      this.beginChange_();
 | 
| +      this.clearAllSelected_();
 | 
| +      for (var i = 0; i < selectedItems.length; i++) {
 | 
| +        this.setItemSelected(selectedItems[i], true);
 | 
| +      }
 | 
| +      this.leadItem = this.anchorItem = selectedItems[0] || null;
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Convenience getter which returns the first selected item.
 | 
| +     * @type {*}
 | 
| +     */
 | 
| +    get selectedItem() {
 | 
| +      for (var uid in this.selectedItems_) {
 | 
| +        return this.selectedItems_[uid];
 | 
| +      }
 | 
| +      return null;
 | 
| +    },
 | 
| +    set selectedItem(selectedItem) {
 | 
| +      this.beginChange_();
 | 
| +      this.clearAllSelected_();
 | 
| +      if (selectedItem) {
 | 
| +        this.selectedItems = [selectedItem];
 | 
| +      } else {
 | 
| +        this.leadItem = this.anchorItem = null;
 | 
| +      }
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Selects a range of items, starting with {@code start} and ends with
 | 
| +     * {@code end}.
 | 
| +     * @param {*} start The first item to select.
 | 
| +     * @param {*} end The last item to select.
 | 
| +     */
 | 
| +    selectRange: function(start, end) {
 | 
| +      // Swap if starts comes after end.
 | 
| +      if (start.compareDocumentPosition(end) & Node.DOCUMENT_POSITION_PRECEDING) {
 | 
| +        var tmp = start;
 | 
| +        start = end;
 | 
| +        end = tmp;
 | 
| +      }
 | 
| +
 | 
| +      this.beginChange_();
 | 
| +
 | 
| +      for (var item = start; item != end; item = this.getNextItem(item)) {
 | 
| +        this.setItemSelected(item, true);
 | 
| +      }
 | 
| +      this.setItemSelected(end, true);
 | 
| +
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Selects all items.
 | 
| +     */
 | 
| +    selectAll: function() {
 | 
| +      this.selectRange(this.getFirstItem(), this.getLastItem());
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Clears the selection
 | 
| +     */
 | 
| +    clear: function() {
 | 
| +      this.beginChange_();
 | 
| +      this.clearAllSelected_();
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Clears the selection and updates the view.
 | 
| +     * @private
 | 
| +     */
 | 
| +    clearAllSelected_: function() {
 | 
| +      for (var uid in this.selectedItems_) {
 | 
| +        this.setItemSelected(this.selectedItems_[uid], false);
 | 
| +      }
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Sets the selecte state for an item.
 | 
| +     * @param {*} item The item to set the selected state for.
 | 
| +     * @param {boolean} b Whether to select the item or not.
 | 
| +     */
 | 
| +    setItemSelected: function(item, b) {
 | 
| +      var uid = this.list.itemToUid(item);
 | 
| +      var oldSelected = uid in this.selectedItems_;
 | 
| +      if (oldSelected == b)
 | 
| +        return;
 | 
| +
 | 
| +      if (b)
 | 
| +        this.selectedItems_[uid] = item;
 | 
| +      else
 | 
| +        delete this.selectedItems_[uid];
 | 
| +
 | 
| +      this.beginChange_();
 | 
| +
 | 
| +      // Changing back?
 | 
| +      if (uid in this.changedUids_ && this.changedUids_[uid] == !b) {
 | 
| +        delete this.changedUids_[uid];
 | 
| +      } else {
 | 
| +        this.changedUids_[uid] = b;
 | 
| +      }
 | 
| +
 | 
| +      // End change dispatches an event which in turn may update the view.
 | 
| +      this.endChange_();
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Whether a given item is selected or not.
 | 
| +     * @param {*} item The item to check.
 | 
| +     * @return {boolean} Whether an item is selected.
 | 
| +     */
 | 
| +    getItemSelected: function(item) {
 | 
| +      var uid = this.list.itemToUid(item);
 | 
| +      return uid in this.selectedItems_;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * This is used to begin batching changes. Call {@code endChange_} when you
 | 
| +     * are done making changes.
 | 
| +     * @private
 | 
| +     */
 | 
| +    beginChange_: function() {
 | 
| +      if (!this.changeCount_) {
 | 
| +        this.changeCount_ = 0;
 | 
| +        this.changedUids_ = {};
 | 
| +      }
 | 
| +      this.changeCount_++;
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Call this after changes are done and it will dispatch a change event if
 | 
| +     * any changes were actually done.
 | 
| +     * @private
 | 
| +     */
 | 
| +    endChange_: function() {
 | 
| +      this.changeCount_--;
 | 
| +      if (!this.changeCount_) {
 | 
| +        var uids = Object.keys(this.changedUids_);
 | 
| +        if (uids.length) {
 | 
| +          var e = new Event('change');
 | 
| +          e.changes = uids.map(function(uid) {
 | 
| +            return {
 | 
| +              uid: uid,
 | 
| +              selected: this.changedUids_[uid]
 | 
| +            };
 | 
| +          }, this);
 | 
| +          this.dispatchEvent(e);
 | 
| +        }
 | 
| +        delete this.changedUids_;
 | 
| +        delete this.changeCount_;
 | 
| +      }
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Called when an item is removed from the lisst.
 | 
| +     * @param {cr.ui.ListItem} item The list item that was removed.
 | 
| +     */
 | 
| +    remove: function(item) {
 | 
| +      if (item == this.leadItem)
 | 
| +        this.leadItem = this.getNextItem(item) || this.getPreviousItem(item);
 | 
| +      if (item == this.anchorItem)
 | 
| +        this.anchorItem = this.getNextItem(item) || this.getPreviousItem(item);
 | 
| +
 | 
| +      // Deselect when removing items.
 | 
| +      if (this.getItemSelected(item))
 | 
| +        this.setItemSelected(item, false);
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * Called when an item was added to the list.
 | 
| +     * @param {cr.ui.ListItem} item The list item to add.
 | 
| +     */
 | 
| +    add: function(item) {
 | 
| +      // We could (should?) check if the item is selected here and update the
 | 
| +      // selection model.
 | 
| +    }
 | 
| +  };
 | 
| +
 | 
| +  /**
 | 
| +   * The anchorItem is used with multiple selection.
 | 
| +   * @type {*}
 | 
| +   */
 | 
| +  cr.defineProperty(ListSelectionModel, 'anchorItem', cr.PropertyKind.JS, null);
 | 
| +
 | 
| +  /**
 | 
| +   * The leadItem is used with multiple selection and it is the item that the
 | 
| +   * user is moving uysing the arrow keys.
 | 
| +   * @type {*}
 | 
| +   */
 | 
| +  cr.defineProperty(ListSelectionModel, 'leadItem', cr.PropertyKind.JS, null);
 | 
| +
 | 
| +  return {
 | 
| +    ListSelectionModel: ListSelectionModel
 | 
| +  };
 | 
| +});
 | 
| 
 |