diff options
Diffstat (limited to 'chrome/browser')
35 files changed, 5783 insertions, 0 deletions
diff --git a/chrome/browser/resources/bookmark_manager/css/list.css b/chrome/browser/resources/bookmark_manager/css/list.css new file mode 100644 index 0000000..0170566 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/css/list.css @@ -0,0 +1,61 @@ + +list { + overflow: auto; + outline: none; +} + +list > * { + font: 11px verdana; + -webkit-user-select: none; + border: 1px solid rgba(255,255,255,0); /* transparent white */ + background-color: rgba(255,255,255,0); + -webkit-border-radius: 2px; + padding: 0px 3px; + line-height: 20px; + white-space: nowrap; + cursor: default; + -webkit-transition: all .12s; + position: relative; /* to allow overlap */ + display: block; +} + +list > [lead] { + border-color: transparent; +} + +list:focus > [lead] { + border-color: hsl(214, 91%, 65%); + z-index: 2; +} + +list > [anchor] { + +} + +list > :hover { + border-color: hsl(214, 91%, 85%); + z-index: 1; + background-color: hsl(214,91%,97%); +} + +list > :hover, +list > [selected] { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.8)), to(rgba(255,255,255,0))); +} + +list > [selected] { + border-color: hsl(0,0%,85%); + background-color: hsl(0,0%,90%); + z-index: 2; +} + +list:focus > [selected] { + background-color: hsl(214,91%,89%); + border-color: hsl(214, 91%, 65%); +} + +list:focus > [lead][selected], +list > [selected]:hover { + background-color: hsl(214,91%,87%); + border-color: hsl(214, 91%, 65%); +} diff --git a/chrome/browser/resources/bookmark_manager/css/menu.css b/chrome/browser/resources/bookmark_manager/css/menu.css new file mode 100644 index 0000000..3fe0261 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/css/menu.css @@ -0,0 +1,48 @@ + +menu { + display: none; + position: absolute; + border: 1px solid #999; + -webkit-box-shadow: 2px 2px 3px hsla(0, 0%, 0%, .3); + color: black; + background-color: hsla(213, 0%, 100%, .95); + left: 0; + white-space: nowrap; + z-index: 2; + padding: 1px; + margin: 0; + cursor: default; +} + +menu > * { + display: block; + margin: 0; + width: 100%; + text-align: start; +} + +menu > :not(hr) { + -webkit-appearance: none; + background: transparent; + font: inherit; + border: 0; + + padding: 3px 8px; + overflow: hidden; + text-overflow: ellipsis; +} + +menu > hr { + border: 0; + border-top: 1px solid rgb(153, 153, 153); + margin: 2px 0; +} + +menu > [hidden] { + display: none; +} + +menu > [selected] { + background-color: hsl(213, 66%, 57%); + color: white; +} diff --git a/chrome/browser/resources/bookmark_manager/css/tree.css b/chrome/browser/resources/bookmark_manager/css/tree.css new file mode 100644 index 0000000..66bdaf3 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/css/tree.css @@ -0,0 +1,143 @@ +tree { + outline: none; + overflow: auto; + display: block; +} + +.tree-item > .tree-row { + color: black; + font: 11px verdana; + -webkit-user-select: none; + border: 1px solid rgba(255,255,255,0); /* transparent white */ + background-color: rgba(255,255,255,0); + -webkit-border-radius: 2px; + padding: 0px 3px; + /*margin-bottom:-1px;*/ + line-height: 20px; + white-space: nowrap; + cursor: default; + /*-webkit-transition: all .12s;*/ + position: relative; +} + +.expand-icon { + width: 11px; + height: 16px; + display: inline-block; + vertical-align: top; + position: relative; + top: 2px; + background-image: -webkit-canvas(triangle-empty); + background-position: 50% 50%; + background-repeat: no-repeat; + -webkit-transition: all .15s, opacity 1.5s; + opacity: 0; +} + +html[dir=rtl] .expand-icon { + -webkit-transform: scale(-1, 1); +} + +tree:hover .expand-icon, +tree:focus .expand-icon { + opacity: 1; + -webkit-transition: all .15s, opacity .5s; +} + +.tree-item[expanded] > .tree-row > .expand-icon { + background-image: -webkit-canvas(triangle-filled); + -webkit-transform: translate(0, 3px) rotate(45deg); +} + +html[dir=rtl] .tree-item[expanded] > .tree-row > .expand-icon { + -webkit-transform: scale(-1, 1) translate(0, 3px) rotate(45deg); /* flip */ +} + +.tree-item > .tree-row > .expand-icon:hover { + background-image: -webkit-canvas(triangle-hover); +} + +.tree-row .expand-icon { + visibility: hidden; +} + +.tree-row[may-have-children] .expand-icon { + visibility: visible; +} + +.tree-row[has-children=false] .expand-icon { + visibility: hidden; +} + +.tree-item > .tree-row:hover { + border-color: hsl(214, 91%, 85%); + z-index: 1; + background-color: hsl(214, 91%, 97%); +} + +/* + WebKit has a bug with attribute selectors so we apply selected to the tree row + as well. + + https://bugs.webkit.org/show_bug.cgi?id=12519 + +*/ +.tree-row[selected]:hover, +.tree-row[selected] { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.8)), to(rgba(255,255,255,0))); +} + +.tree-row[selected] { + border-color: hsl(0,0%,85%); + background-color: hsl(0, 0%, 90%); + z-index: 2; +} + +:focus .tree-row[selected] { + background-color: hsl(214, 91%, 89%); + border-color: #7da2ce; +} + +.tree-children[expanded] { + display: block; +} + +.tree-children { + display: none; +} + +.tree-item > .tree-row > * { + display: inline-block; + -webkit-box-sizing: border-box; +} + +.tree-label { + -webkit-padding-start: 20px; + background-repeat: no-repeat; + background-position: 0 50%; + background-image: url("../images/folder_closed.png"); +} + +.tree-rename > .tree-row > .tree-label { + -webkit-user-select: auto; + -webkit-user-modify: read-write-plaintext-only; + background: white; + color: black; + outline: 1px solid black; +} + +html[dir=rtl] .tree-label { + background-position: 100% 50%; +} + +.tree-row[selected] > .tree-label { + background-image: url("../images/folder_open.png"); +} + +html[dir='rtl'] .tree-label { + background-image: url("../images/folder_closed_rtl.png"); +} + +html[dir='rtl'] .tree-row[selected] > .tree-label { + background-image: url("../images/folder_open_rtl.png"); +} diff --git a/chrome/browser/resources/bookmark_manager/css/tree.css.js b/chrome/browser/resources/bookmark_manager/css/tree.css.js new file mode 100644 index 0000000..34cdf8c --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/css/tree.css.js @@ -0,0 +1,77 @@ + + +(function() { + var a = 7; + var a2 = a / 2; + var ctx = document.getCSSCanvasContext('2d', 'triangle-filled', a2 + 2, a + 1); + + ctx.fillStyle = '#000'; + ctx.translate(.5, .5); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, a); + ctx.lineTo(a2, a2); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + var ctx = document.getCSSCanvasContext('2d', 'triangle-empty', a2 + 2, a + 1); + + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1.2; + ctx.translate(.5, .5); + ctx.fillStyle = '#000'; + ctx.beginPath(); + + + ctx.moveTo(0, 0); + ctx.lineTo(0, a); + ctx.lineTo(a2, a2); + ctx.closePath(); + ctx.stroke(); + + var ctx = document.getCSSCanvasContext('2d', 'triangle-hover', a2 + 2 + 4, a + 1 + 4); + + ctx.shadowColor = 'hsl(214,91%,89%)' + ctx.shadowBlur = 3; + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 0; + + ctx.strokeStyle = 'hsl(214,91%,79%)'; + ctx.lineWidth = 1.2; + ctx.translate(.5 + 2, .5 + 2); + ctx.fillStyle = '#000'; + ctx.beginPath(); + + ctx.moveTo(0, 0); + ctx.lineTo(0, a); + ctx.lineTo(a2, a2); + ctx.closePath(); + ctx.stroke(); +})(); + +// We need to generate CSS for the indentation. +(function() { + // We need to generat the following + //.tree-item > * > .tree-item > .tree-row { + // -webkit-padding-start: 20px; + //} + + //.tree-item > * .tree-item > * .tree-item > * > .tree-item > .tree-row { + // -webkit-padding-start: 60px; + //} + var style = document.createElement('style'); + + function repeat(s, n) { + return Array(n + 1).join(s); + } + + var s = ''; + for (var i = 1; i < 10; i++) { + s += repeat('.tree-item > * ', i) + '.tree-item > .tree-row {\n' + + '-webkit-padding-start:' + i * 20 + 'px\n' + + '}\n'; + } + style.textContent = s; + document.documentElement.firstElementChild.appendChild(style); +})(); diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png b/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png Binary files differnew file mode 100644 index 0000000..ec5d21f --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png Binary files differnew file mode 100644 index 0000000..9740e90 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png Binary files differnew file mode 100644 index 0000000..76abc27 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png diff --git a/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png b/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png Binary files differnew file mode 100644 index 0000000..08682cf --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png diff --git a/chrome/browser/resources/bookmark_manager/images/folder_closed.png b/chrome/browser/resources/bookmark_manager/images/folder_closed.png Binary files differnew file mode 100644 index 0000000..746fab9 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/folder_closed.png diff --git a/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png b/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png Binary files differnew file mode 100644 index 0000000..dbd0b0a --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png diff --git a/chrome/browser/resources/bookmark_manager/images/folder_open.png b/chrome/browser/resources/bookmark_manager/images/folder_open.png Binary files differnew file mode 100644 index 0000000..3276810 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/folder_open.png diff --git a/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png b/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png Binary files differnew file mode 100644 index 0000000..9ba7069 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png diff --git a/chrome/browser/resources/bookmark_manager/js/bmm.js b/chrome/browser/resources/bookmark_manager/js/bmm.js new file mode 100644 index 0000000..24c4ad0 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/bmm.js @@ -0,0 +1,24 @@ +// 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('bmm', function() { + function isFolder(bookmarkNode) { + return !bookmarkNode.url; + } + + function contains(parent, descendant) { + if (descendant.parentId == parent.id) + return true; + // the bmm.treeLookup contains all folders + var parentTreeItem = bmm.treeLookup[descendant.parentId]; + if (!parentTreeItem || !parentTreeItem.bookmarkNode) + return false; + return this.contains(parent, parentTreeItem.bookmarkNode); + } + + return { + isFolder: isFolder, + contains: contains + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js new file mode 100644 index 0000000..a87e91bf --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js @@ -0,0 +1,235 @@ +// 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('bmm', function() { + const List = cr.ui.List; + + var listLookup = {}; + + /** + * Creates a new bookmark list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLButtonElement} + */ + var BookmarkList = cr.ui.define('list'); + + BookmarkList.prototype = { + __proto__: List.prototype, + + decorate: function() { + List.prototype.decorate.call(this); + this.addEventListener('click', this.handleClick_); + }, + + parentId_: '', + get parentId() { + return this.parentId_; + }, + set parentId(parentId) { + if (this.parentId_ == parentId) + return; + + var oldParentId = this.parentId_; + this.parentId_ = parentId; + + var callback = cr.bind(this.handleBookmarkCallback, this); + + if (!parentId) { + callback([]); + } else if (/^q=/.test(parentId)) { + chrome.bookmarks.search(parentId.slice(2), callback); + } else if (parentId == 'recent') { + chrome.bookmarks.getRecent(50, callback); + } else { + chrome.bookmarks.getChildren(parentId, callback); + } + + cr.dispatchPropertyChange(this, 'parentId', parentId, oldParentId); + }, + + handleBookmarkCallback: function(items) { + if (!items) { + // Failed to load bookmarks. Most likely due to the bookmark beeing + // removed. + cr.dispatchSimpleEvent(this, 'invalidId'); + return; + } + listLookup = {}; + this.clear(); + var showFolder = this.showFolder(); + items.forEach(function(item) { + var li = createListItem(item, showFolder); + this.add(li); + }, this); + cr.dispatchSimpleEvent(this, 'load'); + }, + + /** + * The bookmark node that the list is currently displaying. If we are currently + * displaying recent or search this returns null. + * @type {BookmarkTreeNode} + */ + get bookmarkNode() { + if (this.isSearch() || this.isRecent()) + return null; + var treeItem = bmm.treeLookup[this.parentId]; + return treeItem && treeItem.bookmarkNode; + }, + + showFolder: function() { + return this.isSearch() || this.isRecent(); + }, + + isSearch: function() { + return this.parentId_[0] == 'q'; + }, + + isRecent: function() { + return this.parentId_ == 'recent'; + }, + + /** + * Handles the clicks on the list so that we can check if the user clicked + * on a link or an folder. + * @private + * @param {Event} e The click event object. + */ + handleClick_: function(e) { + + var el = e.target; + if (el.href) { + var event = this.ownerDocument.createEvent('Event'); + event.initEvent('urlClicked', true, false); + event.url = el.href; + event.kind = e.shiftKey ? 'window' : e.button == 1 ? 'tab' : 'self'; + this.dispatchEvent(event); + } + }, + + // Bookmark model update callbacks + handleBookmarkChanged: function(id, changeInfo) { + var listItem = listLookup[id]; + if (listItem) { + listItem.bookmarkNode.title = changeInfo.title; + updateListItem(listItem, listItem.bookmarkNode, list.showFolder()); + } + }, + + handleChildrenReordered: function(id, reorderInfo) { + if (this.parentId == id) { + var self = this; + reorderInfo.childIds.forEach(function(id, i) { + var li = listLookup[id] + self.addAt(li, i); + // At this point we do not read the index from the bookmark node so we + // do not need to update it. + li.bookmarkNode.index = i; + }); + } + }, + + handleCreated: function(id, bookmarkNode) { + if (this.parentId == bookmarkNode.parentId) { + var li = createListItem(bookmarkNode, false); + this.addAt(li, bookmarkNode.index); + } + }, + + handleMoved: function(id, moveInfo) { + if (moveInfo.parentId == this.parentId || + moveInfo.oldParentId == this.parentId) { + + if (moveInfo.oldParentId == moveInfo.parentId) { + var listItem = listLookup[id]; + if (listItem) { + this.remove(listItem); + this.addAt(listItem, moveInfo.index); + } + } else { + if (moveInfo.oldParentId == this.parentId) { + var listItem = listLookup[id]; + if (listItem) { + this.remove(listItem); + delete listLookup[id]; + } + } + + if (moveInfo.parentId == list.parentId) { + var self = this; + chrome.bookmarks.get(id, function(bookmarkNodes) { + var bookmarkNode = bookmarkNodes[0]; + var li = createListItem(bookmarkNode, false); + self.addAt(li, bookmarkNode.index); + }); + } + } + } + }, + + handleRemoved: function(id, removeInfo) { + var listItem = listLookup[id]; + if (listItem) { + this.remove(listItem); + delete listLookup[id]; + } + } + }; + + /** + * The contextMenu property. + * @type {cr.ui.Menu} + */ + cr.ui.contextMenuHandler.addContextMenuProperty(BookmarkList); + + function createListItem(bookmarkNode, showFolder) { + var li = listItemPromo.cloneNode(true); + ListItem.decorate(li); + updateListItem(li, bookmarkNode, showFolder); + li.bookmarkId = bookmarkNode.id; + li.bookmarkNode = bookmarkNode; + li.draggable = true; + listLookup[bookmarkNode.id] = li; + return li; + } + + function updateListItem(el, bookmarkNode, showFolder) { + var labelEl = el.firstChild; + const NBSP = '\u00a0'; + labelEl.textContent = bookmarkNode.title || NBSP; + if (bookmarkNode.url) { + labelEl.style.backgroundImage = url('chrome://favicon/' + + bookmarkNode.url); + var urlEl = el.childNodes[1].firstChild; + urlEl.textContent = urlEl.href = bookmarkNode.url; + } else { + el.className = 'folder'; + } + + var folderEl = el.lastChild.firstChild; + if (showFolder) { + folderEl.style.display = ''; + folderEl.textContent = getFolder(bookmarkNode.parentId); + folderEl.href = '#' + bookmarkNode.parentId; + } else { + folderEl.style.display = 'none'; + } + } + + var listItemPromo = (function() { + var div = cr.doc.createElement('div'); + div.innerHTML = '<div>' + + '<div class=label></div>' + + '<div><span class=url></span></div>' + + '<div><span class=folder></span></div>' + + '</div>'; + return div.firstChild; + })(); + + return { + BookmarkList: BookmarkList, + listLookup: listLookup + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js new file mode 100644 index 0000000..df83b77 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js @@ -0,0 +1,191 @@ +// 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('bmm', function() { + const Tree = cr.ui.Tree; + const TreeItem = cr.ui.TreeItem; + + var treeLookup = {}; + + /** + * Creates a new tree item for a bookmark node. + * @param {!Object} bookmarkNode The bookmark node. + * @return {!cr.ui.TreeItem} The newly created tree item. + */ + function createTreeItem(bookmarkNode) { + var id = bookmarkNode.id; + var ti = new TreeItem({ + bookmarkId: id, + bookmarkNode: bookmarkNode, + // Bookmark toolbar and Other bookmarks are not draggable. + draggable: bookmarkNode.parentId != ROOT_ID + }); + treeLookup[id] = ti; + updateTreeItem(ti, bookmarkNode); + return ti; + } + + /** + * Updates an existing tree item to match a bookmark node. + * @param {!cr.ui.TreeItem} el The tree item to update. + * @param {!Object} bookmarkNode The bookmark node describing the tree item. + */ + function updateTreeItem(el, bookmarkNode) { + el.label = bookmarkNode.title; + } + + /** + * Asynchronousy adds a tree item at the correct index based on the bookmark + * backend. + * + * Since the bookmark tree only contains folders the index we get from certain + * callbacks is not very useful so we therefore have this async call which gets + * the children of the parent and adds the tree item at the desired index. + * + * @param {!cr.ui.TreeItem} parent The parent tree item. + * @param {!cr.ui.TreeItem} treeItem The tree item to add. + * @param {Function=} f A function which gets called after the item has been + * added at the right index. + */ + function addTreeItem(parent, treeItem, opt_f) { + chrome.bookmarks.getChildren(parent.bookmarkNode.id, function(children) { + var index = children.filter(bmm.isFolder).map(function(item) { + return item.id; + }).indexOf(treeItem.bookmarkNode.id); + parent.addAt(treeItem, index); + if (opt_f) + opt_f(); + }); + } + + + /** + * Creates a new bookmark list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLButtonElement} + */ + var BookmarkTree = cr.ui.define('tree'); + + BookmarkTree.prototype = { + __proto__: Tree.prototype, + + handleBookmarkChanged: function(id, changeInfo) { + var treeItem = treeLookup[id]; + if (treeItem) { + treeItem.bookmarkNode.title = changeInfo.title; + updateTreeItem(treeItem, listItem.bookmarkNode); + } + }, + + handleChildrenReordered: function(id, reorderInfo) { + var parentItem = treeLookup[id]; + // The tree only contains folders. + var dirIds = reorderInfo.childIds.filter(function(id) { + return id in treeLookup; + }).forEach(function(id, i) { + parentItem.addAt(treeLookup[id], i); + }); + }, + + handleCreated: function(id, bookmarkNode) { + if (bmm.isFolder(bookmarkNode)) { + var parentItem = treeLookup[bookmarkNode.parentId]; + var newItem = createTreeItem(bookmarkNode); + addTreeItem(parentItem, newItem); + } + }, + + handleMoved: function(id, moveInfo) { + var treeItem = treeLookup[id]; + if (treeItem) { + var oldParentItem = treeLookup[moveInfo.oldParentId]; + oldParentItem.remove(treeItem); + var newParentItem = treeLookup[moveInfo.parentId]; + // If the new parent did not have any children before we expand it after + // adding the new item because the default state is to expand the folders. + var hadChildren = newParentItem.hasChildren; + // The tree only shows folders so the index is not the index we want. We + // therefore get the children need to adjust the index. + addTreeItem(newParentItem, treeItem, function() { + if (!hadChildren) + newParentItem.expanded = true; + }); + } + }, + + handleRemoved: function(id, removeInfo) { + var parentItem = treeLookup[removeInfo.parentId]; + var itemToRemove = treeLookup[id]; + if (parentItem && itemToRemove) { + parentItem.remove(itemToRemove); + } + }, + + insertSubtree:function(folder) { + if (!bmm.isFolder(folder)) + return; + var children = folder.children; + this.handleCreated(folder.id, folder); + for(var i = 0; i < children.length; i++) { + var child = children[i]; + this.insertSubtree(child); + } + }, + + /** + * Returns the bookmark node with the given ID. The tree only maintains + * folder nodes. + * @param {string} id The ID of the node to find. + * @return {BookmarkTreeNode} The bookmark tree node or null if not found. + */ + getBookmarkNodeById: function(id) { + var treeItem = treeLookup[id]; + if (treeItem) + return treeItem.bookmarkNode; + return null; + }, + + /** + * Fetches the bookmark items and builds the tree control. + */ + buildTree: function() { + + /** + * Recursive helper function that adds all the directories to the + * parentTreeItem. + * @param {!cr.ui.Tree|!cr.ui.TreeItem} parentTreeItem The parent tree element + * to append to. + * @param {!Array.<BookmarkTreeNode>} bookmarkNodes + * @return {boolean} Whether any directories where added. + */ + function buildTreeItems(parentTreeItem, bookmarkNodes) { + var hasDirectories = false; + for (var i = 0, bookmarkNode; bookmarkNode = bookmarkNodes[i]; i++) { + if (bmm.isFolder(bookmarkNode)) { + hasDirectories = true; + var item = bmm.createTreeItem(bookmarkNode); + parentTreeItem.add(item); + var anyChildren = buildTreeItems(item, bookmarkNode.children); + item.expanded = anyChildren; + } + } + return hasDirectories; + } + + var self = this; + chrome.bookmarks.getTree(function(root) { + buildTreeItems(self, root[0].children); + cr.dispatchSimpleEvent(self, 'load'); + }); + } + }; + + return { + BookmarkTree: BookmarkTree, + createTreeItem: createTreeItem, + treeLookup: treeLookup + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr.js b/chrome/browser/resources/bookmark_manager/js/cr.js new file mode 100644 index 0000000..9de94da --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr.js @@ -0,0 +1,314 @@ +// 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. + +const cr = (function() { + + /** + * Whether we are using a Mac or not. + * @type {boolean} + */ + const isMac = /Mac/.test(navigator.platform); + + /** + * Builds an object structure for the provided namespace path, + * ensuring that names that already exist are not overwritten. For + * example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * @param {string} name Name of the object that this file defines. + * @param {*=} opt_object The object to expose at the end of the path. + * @param {Object=} opt_objectToExportTo The object to add the path to; + * default is {@code window}. + * @private + */ + function exportPath(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || window /* global */; + + for (var part; parts.length && (part = parts.shift());) { + if (!parts.length && opt_object !== undefined) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (part in cur) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } + return cur; + }; + + /** + * Fires a property change event on the target. + * @param {EventTarget} target The target to dispatch the event on. + * @param {string} propertyName The name of the property that changed. + * @param {*} newValue The new value for the property. + * @param {*} oldValue The old value for the property. + */ + function dispatchPropertyChange(target, propertyName, newValue, oldValue) { + // TODO(arv): Depending on cr.Event here is a bit ugly. + var e = new cr.Event(propertyName + 'Change'); + e.propertyName = propertyName; + e.newValue = newValue; + e.oldValue = oldValue; + target.dispatchEvent(e); + } + + /** + * The kind of property to define in {@code defineProperty}. + * @enum {number} + */ + const PropertyKind = { + /** + * Plain old JS property where the backing data is stored as a "private" + * field on the object. + */ + JS: 'js', + + /** + * The property backing data is stored as an attribute on an element. + */ + ATTR: 'attr', + + /** + * The property backing data is stored as an attribute on an element. If the + * element has the attribute then the value is true. + */ + BOOL_ATTR: 'boolAttr' + }; + + /** + * Helper function for defineProperty that returns the getter to use for the + * property. + * @param {string} name + * @param {cr.PropertyKind} kind + * @param {*} defaultValue The default value. This is only used for the ATTR + * kind. + * @return {function():*} The getter for the property. + */ + function getGetter(name, kind, defaultValue) { + switch (kind) { + case PropertyKind.JS: + var privateName = name + '_'; + return function() { + return this[privateName]; + }; + case PropertyKind.ATTR: + // For attr with default value we return the default value if the + // element is missing the attribute. + if (defaultValue == undefined) { + return function() { + return this.getAttribute(name); + }; + } else { + return function() { + // WebKit uses null for non existant attributes. + var value = this.getAttribute(name); + return value !== null ? value : defaultValue; + }; + } + case PropertyKind.BOOL_ATTR: + // Boolean attributes don't support default values. + return function() { + return this.hasAttribute(name); + }; + } + } + + /** + * Helper function for defineProperty that returns the setter of the right + * kind. + * @param {string} name The name of the property we are defining the setter + * for. + * @param {cr.PropertyKind} kind The kind of property we are getting the + * setter for. + * @return {function(*):void} The function to use as a setter. + */ + function getSetter(name, kind) { + switch (kind) { + case PropertyKind.JS: + var privateName = name + '_'; + return function(value) { + var oldValue = this[privateName]; + if (value !== oldValue) { + this[privateName] = value; + dispatchPropertyChange(this, name, value, oldValue); + } + }; + + case PropertyKind.ATTR: + return function(value) { + var oldValue = this[name]; + if (value !== oldValue) { + this.setAttribute(name, value); + dispatchPropertyChange(this, name, value, oldValue); + } + }; + + case PropertyKind.BOOL_ATTR: + return function(value) { + var oldValue = this[name]; + if (value !== oldValue) { + if (value) + this.setAttribute(name, name); + else + this.removeAttribute(name); + dispatchPropertyChange(this, name, value, oldValue); + } + }; + } + } + + /** + * Defines a property on an object. When the setter changes the value a + * property change event with the type {@code name + 'Change'} is fired. + * @param {!Object} The object to define the property for. + * @param {string} The name of the property. + * @param {cr.PropertyKind=} opt_kind What kind of underlying storage to use. + * @param {*} opt_defaultValue The default value. + */ + function defineProperty(obj, name, opt_kind, opt_default) { + if (typeof obj == 'function') + obj = obj.prototype; + + var kind = opt_kind || PropertyKind.JS; + + if (!obj.__lookupGetter__(name)) { + // For js properties we set the default value on the prototype. + if (kind == PropertyKind.JS && arguments.length > 3) { + var privateName = name + '_'; + obj[privateName] = opt_default; + } + obj.__defineGetter__(name, getGetter(name, kind, opt_default)); + } + + if (!obj.__lookupSetter__(name)) { + obj.__defineSetter__(name, getSetter(name, kind)); + } + } + + /** + * Counter for use with createUid + */ + var uidCounter = 1; + + /** + * @return {number} A new unique ID. + */ + function createUid() { + return uidCounter++; + } + + /** + * Returns a unique ID for the item. This mutates the item so it needs to be + * an object + * @param {!Object} item The item to get the unique ID for. + * @return {number} The unique ID for the item. + */ + function getUid(item) { + if (item.hasOwnProperty('uid')) + return item.uid; + return item.uid = createUid(); + } + + /** + * Partially applies this function to a particular 'this object' and zero or + * more arguments. The result is a new function with some arguments of the + * first function pre-filled and the value of |this| 'pre-specified'. + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones. + * + * Usage: + * <pre>var barMethBound = bind(myFunction, myObj, 'arg1', 'arg2'); + * barMethBound('arg3', 'arg4');</pre> + * + * @param {Function} fn A function to partially apply. + * @param {Object|undefined} selfObj Specifies the object which |this| should + * point to when the function is run. If the value is null or undefined, + * it will default to the global object. + * @param {...*} var_args Additional arguments that are partially + * applied to the function. + * + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ + function bind(fn, selfObj, var_args) { + var boundArgs = Array.prototype.slice.call(arguments, 2); + return function() { + var args = Array.prototype.slice.call(arguments); + args.unshift.apply(args, boundArgs); + return fn.apply(selfObj, args); + } + } + + /** + * Dispatches a simple event on an event target. + * @param {!EventTarget} target The event target to dispatch the event on. + * @param {string} type The type of the event. + * @param {boolean=} opt_bubbles Whether the event bubbles or not. + * @param {boolean=} opt_cancelable Whether the default action of the event + * can be prevented. + * @return {boolean} If any of the listeners called {@code preventDefault} + * during the dispatch this will return false. + */ + function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { + var e = new cr.Event(type, opt_bubbles, opt_cancelable); + return target.dispatchEvent(e); + } + + /** + * @param {string} name + * @param {!Function} fun + */ + function define(name, fun) { + var obj = exportPath(name); + var exports = fun(); + for (var key in exports) { + obj[key] = exports[key]; + } + } + + /** + * Document used for various document related operations. + * @type {!Document} + */ + var doc = document; + + + /** + * Allows you to run func in the context of a different document. + * @param {!Document} document The document to use. + * @param {function():*} func The function to call. + */ + function withDoc(document, func) { + var oldDoc = doc; + doc = document; + try { + func(); + } finally { + doc = oldDoc; + } + } + + return { + isMac: isMac, + define: define, + defineProperty: defineProperty, + PropertyKind: PropertyKind, + createUid: createUid, + getUid: getUid, + bind: bind, + dispatchSimpleEvent: dispatchSimpleEvent, + dispatchPropertyChange: dispatchPropertyChange, + + /** + * The document that we are currently using. + * @type {!Document} + */ + get doc() { + return doc; + }, + withDoc: withDoc + }; +})(); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/event.js b/chrome/browser/resources/bookmark_manager/js/cr/event.js new file mode 100644 index 0000000..19fee7e --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/event.js @@ -0,0 +1,40 @@ +// 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. + +/** + * @fileoverview This is a simple pure JS event class that can be used with + * {@code cr.ui.EventTarget}. It should not be used with DOM EventTargets. + */ + +cr.define('cr', function() { + + // cr.Event is called CustomEvent in here to prevent naming conflicts. We + // alse store the original Event in case someone does a global alias of + // cr.Event. + + const DomEvent = Event; + + /** + * Creates a new event to be used with cr.EventTarget or DOM EventTarget + * objects. + * @param {string} type The name of the event. + * @param {boolean=} + * @constructor + */ + function CustomEvent(type, opt_bubbles, opt_capture) { + var e = cr.doc.createEvent('Event'); + e.initEvent(type, !!opt_bubbles, !!opt_capture); + e.__proto__ = CustomEvent.prototype; + return e; + } + + CustomEvent.prototype = { + __proto__: DomEvent.prototype + }; + + // Export + return { + Event: CustomEvent + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js new file mode 100644 index 0000000..f2bc733 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js @@ -0,0 +1,96 @@ +// 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', function() { + + // TODO(arv): object.handleEvent + + function EventTarget() { + } + + EventTarget.prototype = { + + /** + * Adds an event listener to the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. This is called when the event is dispatched. + */ + addEventListener: function(type, handler) { + if (!this.listeners_) + this.listeners_ = {__proto__: null}; + if (!(type in this.listeners_)) { + this.listeners_[type] = [handler]; + } else { + var handlers = this.listeners_[type]; + if (handlers.indexOf(handler) < 0) + handlers.push(handler); + } + }, + + /** + * Removes an event listener from the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. + */ + removeEventListener: function(type, handler) { + if (!this.listeners_) + return; + if (type in this.listeners_) { + var handlers = this.listeners_[type]; + var index = handlers.indexOf(handler); + if (index >= 0) { + // Clean up if this was the last listener. + if (handlers.length == 1) + delete this.listeners_[type]; + else + handlers.splice(index, 1); + } + } + }, + + /** + * Dispatches an event and calls all the listeners that are listening to + * the type of the event. + * @param {!cr.event.Event} event The event to dispatch. + * @return {boolean} Whether the default action was prevented. If someone + * calls preventDefault on the event object then this returns false. + */ + dispatchEvent: function(event) { + if (!this.listeners_) + return true; + + // Since we are using DOM Event objects we need to override some of the + // properties and methods so that we can emulate this correctly. + var self = this; + event.__defineGetter__('target', function() { + return self; + }); + event.preventDefault = function() { + this.returnValue = false; + }; + + var type = event.type; + var prevented = 0; + if (type in this.listeners_) { + // Clone to prevent removal during dispatch + var handlers = this.listeners_[type].concat(); + for (var i = 0, handler; handler = handlers[i]; i++) { + if (handler.handleEvent) + prevented |= handler.handleEvent.call(handler, event) === false; + else + prevented |= handler.call(this, event) === false; + } + } + + return !prevented && event.returnValue; + } + }; + + // Export + return { + EventTarget: EventTarget + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html new file mode 100644 index 0000000..4f782d2 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html @@ -0,0 +1,144 @@ +<!DOCTYPE html> +<html> +<head> +<title></title> +<style> + +</style> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="../cr.js"></script> +<script src="event.js"></script> +<script src="eventtarget.js"></script> +<script> + +goog.require('goog.testing.jsunit'); + +</script> + +</head> +<body> + +<script> + +const EventTarget = cr.EventTarget; +const Event = cr.Event; + +function testFunctionListener() { + debugger; + var fi = 0; + function f(e) { + fi++; + } + + var gi = 0; + function g(e) { + gi++; + } + + var et = new EventTarget; + et.addEventListener('f', f); + et.addEventListener('g', g); + + // Adding again should not cause it to be called twice + et.addEventListener('f', f); + et.dispatchEvent(new Event('f')); + assertEquals('Should have been called once', 1, fi); + assertEquals(0, gi); + + et.removeEventListener('f', f); + et.dispatchEvent(new Event('f')); + assertEquals('Should not have been called again', 1, fi); + + et.dispatchEvent(new Event('g')); + assertEquals('Should have been called once', 1, gi); +} + +function testHandleEvent() { + var fi = 0; + var f = { + handleEvent: function(e) { + fi++; + } + }; + + var gi = 0; + var g = { + handleEvent: function(e) { + gi++; + } + }; + + var et = new EventTarget; + et.addEventListener('f', f); + et.addEventListener('g', g); + + // Adding again should not cause it to be called twice + et.addEventListener('f', f); + et.dispatchEvent(new Event('f')); + assertEquals('Should have been called once', 1, fi); + assertEquals(0, gi); + + et.removeEventListener('f', f); + et.dispatchEvent(new Event('f')); + assertEquals('Should not have been called again', 1, fi); + + et.dispatchEvent(new Event('g')); + assertEquals('Should have been called once', 1, gi); +} + +function testPreventDefault() { + var i = 0; + function prevent(e) { + i++; + e.preventDefault(); + } + + var j = 0; + function pass(e) { + j++; + } + + var et = new EventTarget; + et.addEventListener('test', pass); + + assertTrue(et.dispatchEvent(new Event('test'))); + assertEquals(1, j); + + et.addEventListener('test', prevent); + + console.log('NOW'); + assertFalse(et.dispatchEvent(new Event('test'))); + assertEquals(2, j); + assertEquals(1, i); +} + + +function testReturnFalse() { + var i = 0; + function prevent(e) { + i++; + return false; + } + + var j = 0; + function pass(e) { + j++; + } + + var et = new EventTarget; + et.addEventListener('test', pass); + + assertTrue(et.dispatchEvent(new Event('test'))); + assertEquals(1, j); + + et.addEventListener('test', prevent); + + assertFalse(et.dispatchEvent(new Event('test'))); + assertEquals(2, j); + assertEquals(1, i); +} + +</script> + +</body> +</html> diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui.js b/chrome/browser/resources/bookmark_manager/js/cr/ui.js new file mode 100644 index 0000000..ae90c65 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui.js @@ -0,0 +1,106 @@ +// 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() { + + /** + * Decorates elements as an instance of a class. + * @param {string|!Element} source The way to find the element(s) to decorate. + * If this is a string then {@code querySeletorAll} is used to find the + * elements to decorate. + * @param {!Function} constr The constructor to decorate with. The constr + * needs to have a {@code decorate} function. + */ + function decorate(source, constr) { + var elements; + if (typeof source == 'string') + elements = cr.doc.querySelectorAll(source); + else + elements = [source]; + + for (var i = 0, el; el = elements[i]; i++) { + if (!(el instanceof constr)) + constr.decorate(el); + } + } + + /** + * Helper function for creating new element for define. + */ + function createElementHelper(tagName, opt_bag) { + // Allow passing in ownerDocument to create in a different document. + var doc; + if (opt_bag && opt_bag.ownerDocument) + doc = opt_bag.ownerDocument; + else + doc = cr.doc; + return doc.createElement(tagName); + } + + /** + * Creates the constructor for a UI element class. + * + * Usage: + * <pre> + * var List = cr.ui.define('list'); + * List.prototype = { + * __proto__: HTMLUListElement.prototype, + * decorate: function() { + * ... + * }, + * ... + * }; + * </pre> + * + * @param {string|Function} tagNameOrFunction The tagName or + * function to use for newly created elements. If this is a function it + * needs to return a new element when called. + * @return {function(Object=):Element} The constructor function which takes + * an optional property bag. The function also has a static + * {@code decorate} method added to it. + */ + function define(tagNameOrFunction) { + var createFunction, tagName; + if (typeof tagNameOrFunction == 'function') { + createFunction = tagNameOrFunction; + tagName = ''; + } else { + createFunction = createElementHelper; + tagName = tagNameOrFunction; + } + + /** + * Creates a new UI element constructor. + * @param {Object=} opt_propertyBag Optional bag of properties to set on the + * object after created. The property {@code ownerDocument} is special + * cased and it allows you to create the element in a different + * document than the default. + * @constructor + */ + function f(opt_propertyBag) { + var el = createFunction(tagName, opt_propertyBag); + f.decorate(el); + for (var propertyName in opt_propertyBag) { + el[propertyName] = opt_propertyBag[propertyName]; + } + return el; + } + + /** + * Decorates an element as a UI element class. + * @param {!Element} el The element to decorate. + */ + f.decorate = function(el) { + el.__proto__ = f.prototype; + el.decorate(); + }; + + return f; + } + + return { + decorate: decorate, + define: define + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js new file mode 100644 index 0000000..d1a0677 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js @@ -0,0 +1,265 @@ +// 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. + +/** + * @fileoverview A command is an abstraction of an action a user can do in the + * UI. + * + * When the focus changes in the document for each command a canExecute event + * is dispatched on the active element. By listening to this event you can + * enable and disable the command by setting the event.canExecute property. + * + * When a command is executed a command event is dispatched on the active + * element. Note that you should stop the propagation after you have handled the + * command if there might be other command listeners higher up in the DOM tree. + */ + +cr.define('cr.ui', function() { + + /** + * Creates a new command element. + * @constructor + * @extends {HTMLElement} + */ + var Command = cr.ui.define('command'); + + Command.prototype = { + __proto__: HTMLElement.prototype, + + /** + * Initializes the command. + */ + decorate: function() { + CommandManager.init(this.ownerDocument); + }, + + /** + * Executes the command. This dispatches a command event on the active + * element. If the command is {@code disabled} this does nothing. + */ + execute: function() { + if (this.disabled) + return; + var doc = this.ownerDocument; + if (doc.activeElement) { + var e = new cr.Event('command', true, false); + e.command = this; + doc.activeElement.dispatchEvent(e); + } + }, + + /** + * Call this when there have been changes that might change whether the + * command can be executed or not. + */ + canExecuteChange: function() { + dispatchCanExecuteEvent(this, this.ownerDocument.activeElement); + }, + + /** + * The keyboard shortcut that triggers the command. This is a string + * consisting of a keyIdentifier (as reported by WebKit in keydown) as + * well as optional key modifiers joinded with a '-'. + * For example: + * "F1" + * "U+0008-Meta" for Apple command backspace. + * "U+0041-Ctrl" for Control A + * + * @type {string} + */ + shortcut_: null, + get shortcut() { + return this.shortcut_; + }, + set shortcut(shortcut) { + var oldShortcut = this.shortcut_; + if (shortcut !== oldShortcut) { + this.shortcut_ = shortcut; + + // TODO(arv): Multiple shortcuts? + + var mods = {}; + var ident = ''; + shortcut.split('-').forEach(function(part) { + var partLc = part.toLowerCase(); + switch (partLc) { + case 'alt': + case 'ctrl': + case 'meta': + case 'shift': + mods[partLc + 'Key'] = true; + break; + default: + if (ident) + throw Error('Multiple keyboard shortcuts are not supported'); + ident = part; + } + + this.keyIdentifier_ = ident; + this.keyModifiers_ = mods; + }, this); + + cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, + oldShortcut); + } + }, + + /** + * Whether the event object matches the shortcut for this command. + * @param {!Event} e The key event object. + * @return {boolean} Whether it matched or not. + */ + matchesEvent: function(e) { + if (!this.keyIdentifier_) + return false; + + if (e.keyIdentifier == this.keyIdentifier_) { + // All keyboard modifiers needs to match. + var mods = this.keyModifiers_; + return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { + return e[k] == !!mods[k]; + }); + } + return false; + } + }; + + /** + * The label of the command. + * @type {string} + */ + cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); + + /** + * Whether the command is disabled or not. + * @type {boolean} + */ + cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the command is hidden or not. + * @type {boolean} + */ + cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); + + /** + * Dispatches a canExecute event on the target. + * @param {cr.ui.Command} command The command that we are testing for. + * @param {Element} target The target element to dispatch the event on. + */ + function dispatchCanExecuteEvent(command, target) { + var e = new CanExecuteEvent(command, true) + target.dispatchEvent(e); + command.disabled = !e.canExecute; + } + + /** + * The command managers for different documents. + */ + var commandManagers = {}; + + /** + * Keeps track of the focused element and updates the commands when the focus + * changes. + * @param {!Document} doc The document that we are managing the commands for. + * @constructor + */ + function CommandManager(doc) { + doc.addEventListener('focus', cr.bind(this.handleFocus_, this), true); + doc.addEventListener('keydown', cr.bind(this.handleKeyDown_, this), true); + } + + /** + * Initializes a command manager for the document as needed. + * @param {!Document} doc The document to manage the commands for. + */ + CommandManager.init = function(doc) { + var uid = cr.getUid(doc); + if (!(uid in commandManagers)) { + commandManagers[uid] = new CommandManager(doc); + } + }, + + CommandManager.prototype = { + + /** + * Handles focus changes on the document. + * @param {Event} e The focus event object. + * @private + */ + handleFocus_: function(e) { + var target = e.target; + var commands = Array.prototype.slice.call( + target.ownerDocument.querySelectorAll('command')); + + commands.forEach(function(command) { + dispatchCanExecuteEvent(command, target); + }); + }, + + /** + * Handles the keydown event and routes it to the right command. + * @param {!Event} e The keydown event. + */ + handleKeyDown_: function(e) { + var target = e.target; + var commands = Array.prototype.slice.call( + target.ownerDocument.querySelectorAll('command')); + + for (var i = 0, command; command = commands[i]; i++) { + if (!command.disabled && command.matchesEvent(e)) { + e.preventDefault(); + // We do not want any other element to handle this. + e.stopPropagation(); + + command.execute(); + return; + } + } + } + }; + + /** + * The event type used for canExecute events. + * @param {!cr.ui.Command} command The command that we are evaluating. + * @extends {Event} + */ + function CanExecuteEvent(command) { + var e = command.ownerDocument.createEvent('Event'); + e.initEvent('canExecute', true, false); + e.__proto__ = CanExecuteEvent.prototype; + e.command = command; + return e; + } + + CanExecuteEvent.prototype = { + __proto__: Event.prototype, + + /** + * The current command + * @type {cr.ui.Command} + */ + command: null, + + /** + * Whether the target can execute the command. Setting this also stops the + * propagation. + * @type {boolean} + */ + canExecute_: false, + get canExecute() { + return this.canExecute_; + }, + set canExecute(canExecute) { + this.canExecute_ = canExecute; + this.stopPropagation(); + } + }; + + // Export + return { + Command: Command, + CanExecuteEvent: CanExecuteEvent + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js new file mode 100644 index 0000000..5ac0789 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js @@ -0,0 +1,211 @@ +// 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() { + + /** + * Handles context menus. + * @constructor + */ + function ContextMenuHandler() {} + + ContextMenuHandler.prototype = { + + /** + * The menu that we are currently showing. + * @type {cr.ui.Menu} + */ + menu_: null, + get menu() { + return this.menu_; + }, + + /** + * Shows a menu as a context menu. + * @param {!Event} e The event triggering the show (usally a contextmenu + * event). + * @param {!cr.ui.Menu} menu The menu to show. + */ + showMenu: function(e, menu) { + this.menu_ = menu; + + menu.style.display = 'block'; + // when the menu is shown we steal all keyboard events. + menu.ownerDocument.addEventListener('keydown', this, true); + menu.ownerDocument.addEventListener('mousedown', this, true); + menu.ownerDocument.addEventListener('blur', this, true); + menu.addEventListener('activate', this); + this.positionMenu_(e, menu); + }, + + /** + * Hide the currently shown menu. + */ + hideMenu: function() { + var menu = this.menu; + if (!menu) + return; + + menu.style.display = 'none'; + menu.ownerDocument.removeEventListener('keydown', this, true); + menu.ownerDocument.removeEventListener('mousedown', this, true); + menu.ownerDocument.removeEventListener('blur', this, true); + menu.removeEventListener('activate', this); + menu.selectedIndex = -1; + this.menu_ = null; + + // On windows we might hide the menu in a right mouse button up and if + // that is the case we wait some short period before we allow the menu + // to be shown again. + this.hideTimestamp_ = Date.now(); + }, + + /** + * Positions the menu + * @param {!Event} e The event object triggering the showing. + * @param {!cr.ui.Menu} menu The menu to position. + * @private + */ + positionMenu_: function(e, menu) { + // TODO(arv): Handle scrolled documents when needed. + + var x, y; + // When the user presses the context menu key (on the keyboard) we need + // to detect this. + if (e.screenX == 0 && e.screenY == 0) { + var rect = e.currentTarget.getBoundingClientRect(); + x = rect.left; + y = rect.top; + } else { + x = e.clientX; + y = e.clientY; + } + + var menuRect = menu.getBoundingClientRect(); + var bodyRect = menu.ownerDocument.body.getBoundingClientRect(); + + // Does menu fit below? + if (y + menuRect.height > bodyRect.height) { + // Does menu fit above? + if (y - menuRect.height >= 0) { + y -= menuRect.height; + } else { + // Menu did not fit above nor below. + y = 0; + // We could resize the menu here but lets not worry about that at this + // point. + } + } + + // Does menu fit to the right? + if (x + menuRect.width > bodyRect.width) { + // Does menu fit to the left? + if (x - menuRect.width >= 0) { + x -= menuRect.width; + } else { + // Menu did not fit to the right nor to the left. + x = 0; + // We could resize the menu here but lets not worry about that at this + // point. + } + } + + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + }, + + /** + * Handles event callbacks. + * @param {!Event} e The event object. + */ + handleEvent: function(e) { + // Context menu is handled even when we have no menu. + if (e.type != 'contextmenu' && !this.menu) + return; + + switch (e.type) { + case 'mousedown': + if (!this.menu.contains(e.target)) + this.hideMenu(); + else + e.preventDefault(); + break; + case 'keydown': + // keyIdentifier does not report 'Esc' correctly + if (e.keyCode == 27 /* Esc */) { + this.hideMenu(); + + // If the menu is visible we let it handle all the keyboard events. + } else if (this.menu) { + this.menu.handleKeyDown(e); + e.preventDefault(); + e.stopPropagation(); + } + break; + + case 'activate': + case 'blur': + this.hideMenu(); + break; + + case 'contextmenu': + if ((!this.menu || !this.menu.contains(e.target)) && + (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50)) + this.showMenu(e, e.currentTarget.contextMenu); + e.preventDefault(); + // Don't allow elements further up in the DOM to show their menus. + e.stopPropagation(); + break; + } + }, + + /** + * Adds a contextMenu property to an element or element class. + * @param {!Element|!Function} element The element or class to add the + * contextMenu property to. + */ + addContextMenuProperty: function(element) { + if (typeof element == 'function') + element = element.prototype; + + element.__defineGetter__('contextMenu', function() { + return this.contextMenu_; + }); + element.__defineSetter__('contextMenu', function(menu) { + var oldContextMenu = this.contextMenu; + + if (typeof menu == 'string' && menu[0] == '#') { + menu = this.ownerDocument.getElementById(menu.slice(1)); + cr.ui.decorate(menu, Menu); + } + + if (menu === oldContextMenu) + return; + + if (oldContextMenu && !menu) + this.removeEventListener('contextmenu', contextMenuHandler); + if (menu && !oldContextMenu) + this.addEventListener('contextmenu', contextMenuHandler); + + this.contextMenu_ = menu; + + if (menu && menu.id) + this.setAttribute('contextmenu', '#' + menu.id); + + cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu); + }); + } + }; + + /** + * The singleton context menu handler. + * @type {!ContextMenuHandler} + */ + var contextMenuHandler = new ContextMenuHandler; + + // Export + return { + contextMenuHandler: contextMenuHandler + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js new file mode 100644 index 0000000..c70165d --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js @@ -0,0 +1,229 @@ + +// require: listselectionmodel.js + +/** + * @fileoverview This implements a list control. + */ + +cr.define('cr.ui', function() { + const ListSelectionModel = cr.ui.ListSelectionModel; + + /** + * Creates a new list element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLUListElement} + */ + var List = cr.ui.define('list'); + + List.prototype = { + __proto__: HTMLUListElement.prototype, + + /** + * The selection model to use. + * @type {cr.ui.ListSelectionModel} + */ + get selectionModel() { + return this.selectionModel_; + }, + set selectionModel(sm) { + var oldSm = this.selectionModel_; + if (oldSm == sm) + return; + + if (!this.boundHandleOnChange_) { + this.boundHandleOnChange_ = cr.bind(this.handleOnChange_, this); + this.boundHandleLeadChange_ = cr.bind(this.handleLeadChange_, this); + } + + if (oldSm) { + oldSm.removeEventListener('change', this.boundHandleOnChange_); + oldSm.removeEventListener('leadItemChange', this.boundHandleLeadChange_); + } + + this.selectionModel_ = sm; + + if (sm) { + sm.addEventListener('change', this.boundHandleOnChange_); + sm.addEventListener('leadItemChange', this.boundHandleLeadChange_); + } + }, + + /** + * Convenience alias for selectionModel.selectedItem + * @type {cr.ui.ListItem} + */ + get selectedItem() { + return this.selectionModel.selectedItem; + }, + + /** + * Convenience alias for selectionModel.selectedItems + * @type {!Array<cr.ui.ListItem>} + */ + get selectedItems() { + return this.selectionModel.selectedItems; + }, + + /** + * The HTML elements representing the items. This is just all the element + * children but subclasses may override this to filter out certain elements. + * @type {HTMLCollection} + */ + get items() { + return this.children; + }, + + add: function(listItem) { + this.appendChild(listItem); + + var uid = cr.getUid(listItem); + this.uidToListItem_[uid] = listItem; + + this.selectionModel.add(listItem); + }, + + addAt: function(listItem, index) { + this.insertBefore(listItem, this.items[index]); + + var uid = cr.getUid(listItem); + this.uidToListItem_[uid] = listItem; + + this.selectionModel.add(listItem); + }, + + remove: function(listItem) { + this.selectionModel.remove(listItem); + + this.removeChild(listItem); + + var uid = cr.getUid(listItem); + delete this.uidToListItem_[uid]; + }, + + clear: function() { + this.innerHTML = ''; + this.selectionModel.clear(); + }, + + /** + * Initializes the element. + */ + decorate: function() { + this.uidToListItem_ = {}; + + this.selectionModel = new ListSelectionModel(this); + + this.addEventListener('mousedown', this.handleMouseDownUp_); + this.addEventListener('mouseup', this.handleMouseDownUp_); + this.addEventListener('keydown', this.handleKeyDown); + this.addEventListener('dblclick', this.handleDoubleClick_); + + // Make list focusable + if (!this.hasAttribute('tabindex')) + this.tabIndex = 0; + }, + + /** + * Callback for mousedown and mouseup events. + * @param {Event} e The mouse event object. + * @private + */ + handleMouseDownUp_: function(e) { + var target = e.target; + while (target && target.parentNode != this) { + target = target.parentNode; + } + this.selectionModel.handleMouseDownUp(e, target); + }, + + /** + * Callback for mousedown events. + * @param {Event} e The mouse event object. + * @private + */ + handleMouseUp_: function(e) { + var target = e.target; + while (target && target.parentNode != this) { + target = target.parentNode; + } + if (target) { + this.selectionModel.handleMouseDown(e, target); + } else { + this.selectionModel.clear(); + } + }, + + + /** + * Handle a keydown event. + * @param {Event} e The keydown event. + * @return {boolean} Whether the key event was handled. + */ + handleKeyDown: function(e) { + if (this.selectionModel.handleKeyDown(e)) + return true; + if (e.keyIdentifier == 'Enter' && this.selectionModel.selectedItem) { + cr.dispatchSimpleEvent(this, 'activate'); + return true; + } + return false; + }, + + /** + * Handler for double clicking. When the user double clicks on a selected + * item we dispatch an {@code activate} event. + * @param {Event} e The mouse event object. + * @private + */ + handleDoubleClick_: function(e) { + if (e.button == 0 && this.selectionModel.selectedItem) { + cr.dispatchSimpleEvent(this, 'activate'); + } + }, + + /** + * Callback from the selection model. We dispatch {@code change} events + * when the selection changes. + * @param {!cr.Event} e Event with change info. + * @private + */ + handleOnChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.uidToListItem_[change.uid]; + listItem.selected = change.selected; + }, this); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Handles a change of the lead item from the selection model. + * @property {Event} pe The property change event. + * @private + */ + handleLeadChange_: function(pe) { + if (pe.oldValue) { + pe.oldValue.lead = false; + } + if (pe.newValue) { + pe.newValue.lead = true; + } + }, + + /** + * Gets a unique ID for an item. This needs to be unique to the list but + * does not have to be gloabally unique. This uses {@code cr.getUid} by + * default. Override to provide a more efficient way to get the unique ID. + * @param {cr.ui.ListItem} item The item to get the unique ID for. + * @return + */ + itemToUid: function(item) { + return cr.getUid(item); + } + }; + + return { + List: List + } +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js new file mode 100644 index 0000000..0cd8826 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js @@ -0,0 +1,58 @@ +// 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() { + + /** + * Creates a new list item element. + * @param {string} opt_label The text label for the item. + * @constructor + * @extends {HTMLLIElement} + */ + var ListItem = cr.ui.define('li'); + + ListItem.prototype = { + __proto__: HTMLLIElement.prototype, + + /** + * Plain text label. + * @type {string} + */ + get label() { + return this.textContent; + }, + set label(label) { + this.textContent = label; + }, + + /** + * Whether the item is the lead in a selection. Setting this does not update + * the underlying selection model. This is only used for display purpose. + * @type {boolean} + */ + get lead() { + return this.hasAttribute('lead'); + }, + set lead(lead) { + if (lead) { + this.setAttribute('lead', ''); + this.scrollIntoViewIfNeeded(false); + } else { + this.removeAttribute('lead'); + } + }, + + /** + * Called when an element is decorated as a list item. + */ + decorate: function() { + } + }; + + cr.defineProperty(ListItem, 'selected', cr.PropertyKind.BOOL_ATTR); + + return { + ListItem: ListItem + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js new file mode 100644 index 0000000..2ef83ff --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js @@ -0,0 +1,423 @@ +// 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); + }, + + /** + * Convenience getter which returns the first selected item. + * @type {*} + */ + get selectedItem() { + for (var uid in this.selectedItems_) { + return this.selectedItems_[uid]; + } + return null; + }, + + /** + * 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 + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js new file mode 100644 index 0000000..1145d0f --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js @@ -0,0 +1,157 @@ +// 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 MenuItem = cr.ui.MenuItem; + + /** + * Creates a new menu element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLMenuElement} + */ + var Menu = cr.ui.define('menu'); + + Menu.prototype = { + __proto__: HTMLMenuElement.prototype, + + /** + * Initializes the menu element. + */ + decorate: function() { + this.addEventListener('mouseover', this.handleMouseOver_); + this.addEventListener('mouseout', this.handleMouseOut_); + + // Decorate the children as menu items. + var children = this.children; + for (var i = 0, child; child = children[i]; i++) { + cr.ui.decorate(child, MenuItem); + } + }, + + /** + * Walks up the ancestors until a menu item belonging to this menu is found. + * @param {Element} el + * @return {cr.ui.MenuItem} The found menu item or null. + * @private + */ + findMenuItem_: function(el) { + while (el && el.parentNode != this) { + el = el.parentNode; + } + return el; + }, + + /** + * Handles mouseover events and selects the hovered item. + * @param {Event} e The mouseover event. + * @private + */ + handleMouseOver_: function(e) { + var overItem = this.findMenuItem_(e.target); + this.selectedItem = overItem; + }, + + /** + * Handles mouseout events and deselects any selected item. + * @param {Event} e The mouseout event. + * @private + */ + handleMouseOut_: function(e) { + this.selectedItem = null; + }, + + /** + * The index of the selected item. + * @type {boolean} + */ + // getter and default value is defined using cr.defineProperty. + set selectedIndex(selectedIndex) { + if (this.selectedIndex_ != selectedIndex) { + var oldSelectedItem = this.selectedItem; + this.selectedIndex_ = selectedIndex; + if (oldSelectedItem) + oldSelectedItem.selected = false; + var item = this.selectedItem; + if (item) + item.selected = true; + + cr.dispatchSimpleEvent(this, 'change'); + } + }, + + /** + * The selected menu item or null if none. + * @type {cr.ui.MenuItem} + */ + get selectedItem() { + return this.children[this.selectedIndex]; + }, + set selectedItem(item) { + var index = Array.prototype.indexOf.call(this.children, item); + this.selectedIndex = index; + }, + + /** + * This is the function that handles keyboard navigation. This is usually + * called by the element responsible for managing the menu. + * @param {Event} e The keydown event object. + * @return {boolean} Whether the event was handled be the menu. + */ + handleKeyDown: function(e) { + var item = this.selectedItem; + + var self = this; + function selectNextVisible(m) { + var children = self.children; + var len = children.length; + var i = self.selectedIndex; + if (i == -1 && m == -1) { + // Edge case when we need to go the last item fisrt. + i = 0; + } + while (true) { + i = (i + m + len) % len; + item = children[i]; + if (item && !item.isSeparator() && !item.hidden) + break; + } + if (item) + self.selectedIndex = i; + } + + switch (e.keyIdentifier) { + case 'Down': + selectNextVisible(1); + return true; + case 'Up': + selectNextVisible(-1); + return true; + case 'Enter': + case 'U+0020': // Space + if (item) { + if (cr.dispatchSimpleEvent(item, 'activate', true, true)) { + if (item.command) + item.command.execute(); + } + } + return true; + } + + return false; + } + }; + + /** + * The selected menu item. + * @type {number} + */ + cr.defineProperty(Menu, 'selectedIndex', cr.PropertyKind.JS, -1); + + // Export + return { + Menu: Menu + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js new file mode 100644 index 0000000..469cfd8 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js @@ -0,0 +1,166 @@ +// 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 Menu = cr.ui.Menu; + + /** + * Creates a new menu button element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLButtonElement} + */ + var MenuButton = cr.ui.define('button'); + + MenuButton.prototype = { + __proto__: HTMLButtonElement.prototype, + + /** + * Initializes the menu button. + */ + decorate: function() { + this.addEventListener('mousedown', this); + this.addEventListener('keydown', this); + + var menu; + if ((menu = this.getAttribute('menu'))) + this.menu = menu; + }, + + /** + * The menu associated with the menu button. + * @type {cr.ui.Menu} + */ + get menu() { + return this.menu_; + }, + set menu(menu) { + if (typeof menu == 'string' && menu[0] == '#') { + menu = this.ownerDocument.getElementById(menu.slice(1)); + cr.ui.decorate(menu, Menu); + } + + this.menu_ = menu; + if (menu) { + if (menu.id) + this.setAttribute('menu', '#' + menu.id); + } + }, + + /** + * Handles event callbacks. + * @param {Event} e The event object. + */ + handleEvent: function(e) { + if (!this.menu) + return; + + switch (e.type) { + case 'mousedown': + if (e.currentTarget == this.ownerDocument) { + if (!this.contains(e.target) && !this.menu.contains(e.target)) + this.hideMenu(); + else + e.preventDefault(); + } else { + if (this.isMenuShown()) { + this.hideMenu(); + } else { + this.showMenu(); + // Prevent the button from stealing focus on mousedown. + e.preventDefault(); + } + } + break; + case 'keydown': + this.handleKeyDown(e); + // If the menu is visible we let it handle all the keyboard events. + if (this.isMenuShown() && e.currentTarget == this.ownerDocument) { + this.menu.handleKeyDown(e); + e.preventDefault(); + e.stopPropagation(); + } + break; + + case 'activate': + case 'blur': + this.hideMenu(); + break; + } + }, + + /** + * Shows the menu. + */ + showMenu: function() { + this.menu.style.display = 'block'; + // when the menu is shown we steal all keyboard events. + this.ownerDocument.addEventListener('keydown', this, true); + this.ownerDocument.addEventListener('mousedown', this, true); + this.ownerDocument.addEventListener('blur', this, true); + this.menu.addEventListener('activate', this); + this.positionMenu_(); + }, + + /** + * Hides the menu. + */ + hideMenu: function() { + this.menu.style.display = 'none'; + this.ownerDocument.removeEventListener('keydown', this, true); + this.ownerDocument.removeEventListener('mousedown', this, true); + this.ownerDocument.removeEventListener('blur', this, true); + this.menu.removeEventListener('activate', this); + this.menu.selectedIndex = -1; + }, + + /** + * Whether the menu is shown. + */ + isMenuShown: function() { + return window.getComputedStyle(this.menu).display != 'none'; + }, + + /** + * Positions the menu below the menu button. At this point we do not use any + * advanced positioning logic to ensure the menu fits in the viewport. + * @private + */ + positionMenu_: function() { + var buttonRect = this.getBoundingClientRect(); + this.menu.style.top = buttonRect.bottom + 'px'; + if (getComputedStyle(this).direction == 'rtl') { + var menuRect = this.menu.getBoundingClientRect(); + this.menu.style.left = buttonRect.right - menuRect.width + 'px'; + } else { + this.menu.style.left = buttonRect.left + 'px'; + } + }, + + /** + * Handles the keydown event for the menu button. + */ + handleKeyDown: function(e) { + switch (e.keyIdentifier) { + case 'Down': + case 'Up': + case 'Enter': + case 'U+0020': // Space + if (!this.isMenuShown()) + this.showMenu(); + e.preventDefault(); + break; + case 'Esc': + case 'U+001B': // Maybe this is remote desktop playing a prank? + this.hideMenu(); + break; + } + } + }; + + // Export + return { + MenuButton: MenuButton + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js new file mode 100644 index 0000000..5c66f17 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js @@ -0,0 +1,147 @@ +// 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 Command = cr.ui.Command; + + /** + * Creates a new menu item element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLButtonElement} + */ + var MenuItem = cr.ui.define('button'); + + /** + * Creates a new menu separator element. + * @return {cr.ui.MenuItem} + */ + MenuItem.createSeparator = function() { + var el = cr.doc.createElement('hr'); + MenuItem.decorate(el); + return el; + }; + + MenuItem.prototype = { + __proto__: HTMLButtonElement.prototype, + + /** + * Initializes the menu item. + */ + decorate: function() { + var commandId; + if ((commandId = this.getAttribute('command'))) + this.command = commandId; + + this.addEventListener('mouseup', this.handleMouseUp_); + }, + + /** + * The command associated with this menu item. If this is set to a string + * of the form "#element-id" then the element is looked up in the document + * of the command. + * @type {cr.ui.Command} + */ + command_: null, + get command() { + return this.command_; + }, + set command(command) { + if (this.command_) { + this.command_.removeEventListener('labelChange', this); + this.command_.removeEventListener('disabledChange', this); + this.command_.removeEventListener('hiddenChange', this); + } + + if (typeof command == 'string' && command[0] == '#') { + command = this.ownerDocument.getElementById(command.slice(1)); + cr.ui.decorate(command, Command); + } + + this.command_ = command; + if (command) { + if (command.id) + this.setAttribute('command', '#' + command.id); + + this.label = command.label; + this.disabled = command.disabled; + this.hidden = command.hidden; + + this.command_.addEventListener('labelChange', this); + this.command_.addEventListener('disabledChange', this); + this.command_.addEventListener('hiddenChange', this); + } + }, + + /** + * The text label. + * @type {string} + */ + get label() { + return this.textContent; + }, + set label(label) { + this.textContent = label; + }, + + /** + * @return {boolean} Whether the menu item is a separator. + */ + isSeparator: function() { + return this.tagName == 'HR'; + }, + + /** + * Handles mouseup events. This dispatches an active event and if there + * is an assiciated command then that is executed. + * @param {Event} The mouseup event object. + * @private + */ + handleMouseUp_: function(e) { + if (!this.disabled && !this.isSeparator()) { + // Dispatch command event followed by executing the command object. + if (cr.dispatchSimpleEvent(this, 'activate', true, true)) { + var command = this.command; + if (command) + command.execute(); + } + } + }, + + /** + * Handles changes to the associated command. + * @param {Event} e The event object. + */ + handleEvent: function(e) { + switch (e.type) { + case 'disabledChange': + this.disabled = this.command.disabled; + break; + case 'hiddenChange': + this.hidden = this.command.hidden; + break; + case 'labelChange': + this.label = this.command.label; + break; + } + } + }; + + /** + * Whether the menu item is hidden or not. + * @type {boolean} + */ + cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the menu item is selected or not. + * @type {boolean} + */ + cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR); + + // Export + return { + MenuItem: MenuItem + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js new file mode 100644 index 0000000..c06fad6 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js @@ -0,0 +1,507 @@ +// 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() { + + /** + * Helper function that finds the first ancestor tree item. + * @param {!Element} el The element to start searching from. + * @return {cr.ui.TreeItem} The found tree item or null if not found. + */ + function findTreeItem(el) { + while (el && !(el instanceof TreeItem)) { + el = el.parentNode; + } + return el; + } + + /** + * Creates a new tree element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLElement} + */ + var Tree = cr.ui.define('tree'); + + Tree.prototype = { + __proto__: HTMLElement.prototype, + + /** + * Initializes the element. + */ + decorate: function() { + // Make list focusable + if (!this.hasAttribute('tabindex')) + this.tabIndex = 0; + + this.addEventListener('click', this.handleClick); + this.addEventListener('mousedown', this.handleMouseDown); + this.addEventListener('dblclick', this.handleDblClick); + this.addEventListener('keydown', this.handleKeyDown); + }, + + /** + * Returns the tree item that are children of this tree. + */ + get items() { + return this.children; + }, + + /** + * Adds a tree item to the tree. + * @param {!cr.ui.TreeItem} treeItem The item to add. + */ + add: function(treeItem) { + this.appendChild(treeItem); + }, + + /** + * Adds a tree item at the given index. + * @param {!cr.ui.TreeItem} treeItem The item to add. + * @param {number} index The index where we want to add the item. + */ + addAt: function(treeItem, index) { + this.insertBefore(treeItem, this.children[index]); + }, + + /** + * Removes a tree item child. + * @param {!cr.ui.TreeItem} treeItem The tree item to remove. + */ + remove: function(treeItem) { + this.removeChild(treeItem); + }, + + /** + * Handles click events on the tree and forwards the event to the relevant + * tree items as necesary. + * @param {Event} e The click event object. + */ + handleClick: function(e) { + var treeItem = findTreeItem(e.target); + if (treeItem) + treeItem.handleClick(e); + }, + + handleMouseDown: function(e) { + if (e.button == 2) // right + this.handleClick(e); + }, + + /** + * Handles double click events on the tree. + * @param {Event} e The dblclick event object. + */ + handleDblClick: function(e) { + var treeItem = findTreeItem(e.target); + if (treeItem) + treeItem.expanded = !treeItem.expanded; + }, + + /** + * Handles keydown events on the tree and updates selection and exanding + * of tree items. + * @param {Event} e The click event object. + */ + handleKeyDown: function(e) { + var itemToSelect; + if (e.ctrlKey) + return; + + var item = this.selectedItem; + + var rtl = window.getComputedStyle(item).direction == 'rtl'; + + switch (e.keyIdentifier) { + case 'Up': + itemToSelect = item ? getPrevious(item) : + this.items[this.items.length - 1]; + break; + case 'Down': + itemToSelect = item ? getNext(item) : + this.items[0]; + break; + case 'Left': + case 'Right': + // Don't let back/forward keyboard shortcuts be used. + if (!cr.isMac && e.altKey || cr.isMac && e.metaKey) + break; + + if (e.keyIdentifier == 'Left' && !rtl || + e.keyIdentifier == 'Right' && rtl) { + if (item.expanded) + item.expanded = false; + else + itemToSelect = findTreeItem(item.parentNode); + } else { + if (!item.expanded) + item.expanded = true; + else + itemToSelect = item.items[0]; + } + break; + case 'Home': + itemToSelect = this.items[0]; + break; + case 'End': + itemToSelect = this.items[this.items.length - 1]; + break; + } + + if (itemToSelect) { + itemToSelect.selected = true; + e.preventDefault(); + } + }, + + /** + * The selected tree item or null if none. + * @type {cr.ui.TreeItem} + */ + get selectedItem() { + return this.selectedItem_ || null; + }, + set selectedItem(item) { + var oldSelectedItem = this.selectedItem_; + if (oldSelectedItem != item) { + // Set the selectedItem_ before deselecting the old item since we only + // want one change when moving between items. + this.selectedItem_ = item; + + if (oldSelectedItem) + oldSelectedItem.selected = false; + + if (item) + item.selected = true; + + cr.dispatchSimpleEvent(this, 'change'); + } + } + }; + + /** + * This is used as a blueprint for new tree item elements. + * @type {!HTMLElement} + */ + var treeItemProto = (function() { + var treeItem = cr.doc.createElement('div'); + treeItem.className = 'tree-item'; + treeItem.innerHTML = '<div class=tree-row>' + + '<span class=expand-icon></span>' + + '<span class=tree-label></span>' + + '</div>' + + '<div class=tree-children></div>'; + return treeItem; + })(); + + /** + * Creates a new tree item. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLElement} + */ + var TreeItem = cr.ui.define(function() { + return treeItemProto.cloneNode(true); + }); + + TreeItem.prototype = { + __proto__: HTMLElement.prototype, + + /** + * Initializes the element. + */ + decorate: function() { + + }, + + /** + * The tree items children. + */ + get items() { + return this.lastElementChild.children; + }, + + /** + * Adds a tree item as a child. + * @param {!cr.ui.TreeItem} child The child to add. + */ + add: function(child) { + this.addAt(child, 0xffffffff); + }, + + /** + * Adds a tree item as a child at a given index. + * @param {!cr.ui.TreeItem} child The child to add. + * @param {number} index The index where to add the child. + */ + addAt: function(child, index) { + this.lastElementChild.insertBefore(child, this.items[index]); + if (this.items.length == 1) + this.hasChildren_ = true; + }, + + /** + * Removes a child. + * @param {!cr.ui.TreeItem} child The tree item child to remove. + */ + remove: function(child) { + // If we removed the selected item we should become selected. + var tree = this.tree; + var selectedItem = tree.selectedItem; + if (selectedItem && child.contains(selectedItem)) + this.selected = true; + + this.lastElementChild.removeChild(child); + if (this.items.length == 0) + this.hasChildren_ = false; + }, + + /** + * The parent tree item. + * @type {!cr.ui.Tree|cr.ui.TreeItem} + */ + get parentItem() { + var p = this.parentNode; + while (p && !(p instanceof TreeItem) && !(p instanceof Tree)) { + p = p.parentNode; + } + return p; + }, + + /** + * The tree that the tree item belongs to or null of no added to a tree. + * @type {cr.ui.Tree} + */ + get tree() { + var t = this.parentItem; + while (t && !(t instanceof Tree)) { + t = t.parentItem; + } + return t; + }, + + /** + * Whether the tree item is expanded or not. + * @type {boolean} + */ + get expanded() { + return this.hasAttribute('expanded'); + }, + set expanded(b) { + if (this.expanded == b) + return; + + var treeChildren = this.lastElementChild; + + if (b) { + if (this.mayHaveChildren_) { + this.setAttribute('expanded', ''); + treeChildren.setAttribute('expanded', ''); + cr.dispatchSimpleEvent(this, 'expand', true, false); + this.scrollIntoViewIfNeeded(false); + } + } else { + var tree = this.tree; + if (tree && !this.selected) { + var oldSelected = tree.selectedItem; + if (oldSelected && this.contains(oldSelected)) + this.selected = true; + } + this.removeAttribute('expanded'); + treeChildren.removeAttribute('expanded'); + cr.dispatchSimpleEvent(this, 'collapse', true, false); + } + }, + + /** + * Expands all parent items. + */ + reveal: function() { + var pi = this.parentItem; + while (!(pi instanceof Tree)) { + pi.expanded = true; + pi = pi.parentItem; + } + }, + + /** + * The element representing the row that gets highlighted. + * @type {!HTMLElement} + */ + get rowElement() { + return this.firstElementChild; + }, + + /** + * The element containing the label text and the icon. + * @type {!HTMLElement} + */ + get labelElement() { + return this.firstElementChild.lastElementChild; + }, + + /** + * The label text. + * @type {string} + */ + get label() { + return this.labelElement.textContent; + }, + set label(s) { + this.labelElement.textContent = s; + }, + + /** + * The URL for the icon. + * @type {string} + */ + get icon() { + return window.getComputedStyle(this.labelElement). + backgroundImage.slice(4, -1); + }, + set icon(icon) { + return this.labelElement.style.backgroundImage = url(icon); + }, + + /** + * Whether the tree item is selected or not. + * @type {boolean} + */ + get selected() { + return this.hasAttribute('selected'); + }, + set selected(b) { + if (this.selected == b) + return; + var rowItem = this.firstElementChild; + var tree = this.tree; + if (b) { + this.setAttribute('selected', ''); + rowItem.setAttribute('selected', ''); + this.labelElement.scrollIntoViewIfNeeded(false); + if (tree) + tree.selectedItem = this; + } else { + this.removeAttribute('selected'); + rowItem.removeAttribute('selected'); + if (tree && tree.selectedItem == this) + tree.selectedItem = null; + } + }, + + /** + * Whether the tree item has children. + * @type {boolean} + */ + get mayHaveChildren_() { + return this.hasAttribute('may-have-children'); + }, + set mayHaveChildren_(b) { + var rowItem = this.firstElementChild; + if (b) { + this.setAttribute('may-have-children', ''); + rowItem.setAttribute('may-have-children', ''); + } else { + this.removeAttribute('may-have-children'); + rowItem.removeAttribute('may-have-children'); + } + }, + + /** + * Whether the tree item has children. + * @type {boolean} + */ + get hasChildren() { + return !!this.items[0]; + }, + + /** + * Whether the tree item has children. + * @type {boolean} + * @private + */ + set hasChildren_(b) { + var rowItem = this.firstElementChild; + this.setAttribute('has-children', b); + rowItem.setAttribute('has-children', b); + if (b) + this.mayHaveChildren_ = true; + }, + + /** + * Called when the user clicks on a tree item. This is forwarded from the + * cr.ui.Tree. + * @param {Event} e The click event. + */ + handleClick: function(e) { + if (e.target.className == 'expand-icon') + this.expanded = !this.expanded; + else + this.selected = true; + } + }; + + /** + * Helper function that returns the next visible tree item. + * @param {cr.ui.TreeItem} item The tree item. + * @retrun {cr.ui.TreeItem} The found item or null. + */ + function getNext(item) { + if (item.expanded) { + var firstChild = item.items[0]; + if (firstChild) { + return firstChild; + } + } + + return getNextHelper(item); + } + + /** + * Another helper function that returns the next visible tree item. + * @param {cr.ui.TreeItem} item The tree item. + * @retrun {cr.ui.TreeItem} The found item or null. + */ + function getNextHelper(item) { + if (!item) + return null; + + var nextSibling = item.nextElementSibling; + if (nextSibling) { + return nextSibling; + } + return getNextHelper(item.parentItem); + } + + /** + * Helper function that returns the previous visible tree item. + * @param {cr.ui.TreeItem} item The tree item. + * @retrun {cr.ui.TreeItem} The found item or null. + */ + function getPrevious(item) { + var previousSibling = item.previousElementSibling; + return previousSibling ? getLastHelper(previousSibling) : item.parentItem; + } + + /** + * Helper function that returns the last visible tree item in the subtree. + * @param {cr.ui.TreeItem} item The item to find the last visible item for. + * @return {cr.ui.TreeItem} The found item or null. + */ + function getLastHelper(item) { + if (!item) + return null; + if (item.expanded && item.hasChildren) { + var lastChild = item.items[item.items.length - 1]; + return getLastHelper(lastChild); + } + return item; + } + + // Export + return { + Tree: Tree, + TreeItem: TreeItem + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr_test.html b/chrome/browser/resources/bookmark_manager/js/cr_test.html new file mode 100644 index 0000000..5d6c16e --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr_test.html @@ -0,0 +1,231 @@ +<!DOCTYPE html> +<html> +<head> +<title></title> +<style> + +</style> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="cr.js"></script> +<script src="cr/event.js"></script> +<script src="cr/eventtarget.js"></script> +<script> + +goog.require('goog.testing.jsunit'); + +</script> + +</head> +<body> + +<script> + +const EventTarget = cr.EventTarget; + +function testDefineProperty() { + var obj = new EventTarget; + cr.defineProperty(obj, 'test'); + + obj.test = 1; + assertEquals(1, obj.test); + assertEquals(1, obj.test_); +} + +function testDefinePropertyOnClass() { + function C() {} + C.prototype = { + __proto__: EventTarget.prototype + }; + + cr.defineProperty(C, 'test'); + + var obj = new C; + obj.test = 1; + assertEquals(1, obj.test); + assertEquals(1, obj.test_); +} + +function testDefinePropertyWithDefault() { + var obj = new EventTarget; + + cr.defineProperty(obj, 'test', null, 1); + + assertEquals(1, obj.test); + assertEquals(1, obj.test_); + + obj.test = 2; + assertEquals(2, obj.test); + assertEquals(2, obj.test_); +} + +function testDefinePropertyEvent() { + var obj = new EventTarget; + cr.defineProperty(obj, 'test'); + obj.test = 1; + + var count = 0; + function f(e) { + assertEquals('testChange', e.type); + assertEquals('test', e.propertyName); + assertEquals(1, e.oldValue); + assertEquals(2, e.newValue); + count++; + } + + obj.addEventListener('testChange', f); + obj.test = 2; + assertEquals(2, obj.test); + assertEquals('Should have called the property change listener', 1, count); + + obj.test = 2; + assertEquals(1, count); +} + +function testDefinePropertyEventWithDefault() { + var obj = new EventTarget; + cr.defineProperty(obj, 'test', cr.PropertyKind.JS, 1); + + var count = 0; + function f(e) { + assertEquals('testChange', e.type); + assertEquals('test', e.propertyName); + assertEquals(1, e.oldValue); + assertEquals(2, e.newValue); + count++; + } + + obj.addEventListener('testChange', f); + + obj.test = 1; + assertEquals('Should not have called the property change listener', 0, count); + + obj.test = 2; + assertEquals(2, obj.test); + assertEquals('Should have called the property change listener', 1, count); + + obj.test = 2; + assertEquals(1, count); +} + +function testDefinePropertyAttr() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR); + + obj.test = 'a'; + assertEquals('a', obj.test); + assertEquals('a', obj.getAttribute('test')); +} + +function testDefinePropertyAttrOnClass() { + var obj = document.createElement('button'); + cr.defineProperty(HTMLButtonElement, 'test', cr.PropertyKind.ATTR); + + obj.test = 'a'; + assertEquals('a', obj.test); + assertEquals('a', obj.getAttribute('test')); +} + +function testDefinePropertyAttrWithDefault() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR, 'a'); + + assertEquals('a', obj.test); + assertFalse(obj.hasAttribute('test')); + + obj.test = 'b'; + assertEquals('b', obj.test); + assertEquals('b', obj.getAttribute('test')); +} + +function testDefinePropertyAttrEvent() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR); + obj.test = 'a'; + + var count = 0; + function f(e) { + assertEquals('testChange', e.type); + assertEquals('test', e.propertyName); + assertEquals('a', e.oldValue); + assertEquals('b', e.newValue); + count++; + } + + obj.addEventListener('testChange', f); + obj.test = 'b'; + assertEquals('b', obj.test); + assertEquals('Should have called the property change listener', 1, count); + + obj.test = 'b'; + assertEquals(1, count); +} + +function testDefinePropertyAttrEventWithDefault() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR, 'a'); + + var count = 0; + function f(e) { + assertEquals('testChange', e.type); + assertEquals('test', e.propertyName); + assertEquals('a', e.oldValue); + assertEquals('b', e.newValue); + count++; + } + + obj.addEventListener('testChange', f); + + obj.test = 'a'; + assertEquals('Should not have called the property change listener', 0, count); + + obj.test = 'b'; + assertEquals('b', obj.test); + assertEquals('Should have called the property change listener', 1, count); + + obj.test = 'b'; + assertEquals(1, count); +} + +function testDefinePropertyBoolAttr() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.BOOL_ATTR); + + assertFalse(obj.test); + assertFalse(obj.hasAttribute('test')); + + obj.test = true; + assertTrue(obj.test); + assertTrue(obj.hasAttribute('test')); + + obj.test = false; + assertFalse(obj.test); + assertFalse(obj.hasAttribute('test')); +} + +function testDefinePropertyBoolAttrEvent() { + var obj = document.createElement('div'); + cr.defineProperty(obj, 'test', cr.PropertyKind.BOOL_ATTR); + + var count = 0; + function f(e) { + assertEquals('testChange', e.type); + assertEquals('test', e.propertyName); + assertEquals(false, e.oldValue); + assertEquals(true, e.newValue); + count++; + } + + obj.addEventListener('testChange', f); + obj.test = true; + assertTrue(obj.test); + assertEquals('Should have called the property change listener', 1, count); + + obj.test = true; + assertEquals(1, count); +} + + +</script> + +</body> +</html> diff --git a/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js b/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js new file mode 100644 index 0000000..8166ddc --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js @@ -0,0 +1,104 @@ +/** + * @fileoverview This is a simple template engine inspired by JsTemplates + * optimized for i18n. + * + * It currently supports two handlers: + * + * * i18n-content which sets the textContent of the element + * + * <span i18n-content="myContent"></span> + * i18nTemplate.process(element, {'myContent': 'Content'}); + * + * * i18n-values is a list of attribute-value or property-value pairs. + * Properties are prefixed with a '.' and can contain nested properties. + * + * <span i18n-values="title:myTitle;.style.fontSize:fontSize"></span> + * i18nTemplate.process(element, { + * 'myTitle': 'Title', + * 'fontSize': '13px' + * }); + */ + +var i18nTemplate = (function() { + /** + * This provides the handlers for the templating engine. The key is used as + * the attribute name and the value is the function that gets called for every + * single node that has this attribute. + * @type {Object} + */ + var handlers = { + /** + * This handler sets the textContent of the element. + */ + 'i18n-content': function(element, attributeValue, obj) { + element.textContent = obj[attributeValue]; + }, + + /** + * This is used to set HTML attributes and DOM properties,. The syntax is: + * attributename:key; + * .domProperty:key; + * .nested.dom.property:key + */ + 'i18n-values': function(element, attributeValue, obj) { + var parts = attributeValue.replace(/\s/g, '').split(/;/); + for (var j = 0; j < parts.length; j++) { + var a = parts[j].match(/^([^:]+):(.+)$/); + if (a) { + var propName = a[1]; + var propExpr = a[2]; + + // Ignore missing properties + if (propExpr in obj) { + var value = obj[propExpr]; + if (propName.charAt(0) == '.') { + var path = propName.slice(1).split('.'); + var object = element; + while (object && path.length > 1) { + object = object[path.shift()]; + } + if (object) { + object[path] = value; + // In case we set innerHTML (ignoring others) we need to + // recursively check the content + if (path == 'innerHTML') { + process(element, obj); + } + } + } else { + element.setAttribute(propName, value); + } + } else { + console.warn('i18n-values: Missing value for "' + propExpr + '"'); + } + } + } + } + }; + + var attributeNames = []; + for (var key in handlers) { + attributeNames.push(key); + } + var selector = '[' + attributeNames.join('],[') + ']'; + + /** + * Processes a DOM tree with the {@code obj} map. + */ + function process(node, obj) { + var elements = node.querySelectorAll(selector); + for (var element, i = 0; element = elements[i]; i++) { + for (var j = 0; j < attributeNames.length; j++) { + var name = attributeNames[j]; + var att = element.getAttribute(name); + if (att != null) { + handlers[name](element, att, obj); + } + } + } + } + + return { + process: process + }; +})(); diff --git a/chrome/browser/resources/bookmark_manager/js/localstrings.js b/chrome/browser/resources/bookmark_manager/js/localstrings.js new file mode 100644 index 0000000..86f888b --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/localstrings.js @@ -0,0 +1,55 @@ +// Copyright (c) 2009-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. + +// TODO(arv): Namespace and share code with DOMUI + +/** + * The local strings get injected into the page usig a varaible named + * {@code templateData}. This class provides a simpler interface to access those + * strings. + * @constructor + */ +function LocalStrings() { +} + +LocalStrings.prototype = { + + /** + * The template data object. + * @type {Object} + */ + templateData: null, + + /** + * Gets a localized string by its id. + * @param {string} s The id of the string we want. + * @return {string} The localized string. + */ + getString: function(id) { + return this.templateData[id] || ''; + }, + + /** + * Returns a formatted localized string where all %s contents are replaced + * by the second argument and where $1 to $9 are replaced by the second to + * tenths arguments. + * @param {string} id The ID of the string we want. + * @param {string} v The string to include in the formatted string. + * @param {...string} The extra values to include in the fomatted output. + * @return {string} The formatted string. + */ + getStringF: function(id, v, var_args) { + // The localized messages should contain $n but they also use %s from time + // to time so we support both until all the messages have been unified. + var s = this.getString(id); + var args = arguments; + return s.replace(/%s|\$[$1-9]/g, function(m) { + if (m == '%s') + return v; + if (m == '$$') + return '$'; + return args[m[1]]; + }); + } +}; diff --git a/chrome/browser/resources/bookmark_manager/js/util.js b/chrome/browser/resources/bookmark_manager/js/util.js new file mode 100644 index 0000000..3fef5d2 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/util.js @@ -0,0 +1,57 @@ +// 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. + +/** + * The global object. + * @param {!Object} + */ +const global = this; + +/** + * Alias for document.getElementById. + * @param {string} id The ID of the element to find. + * @return {HTMLElement} The found element or null if not found. + */ +function $(id) { + return document.getElementById(id); +} + +/** + * Calls chrome.send with a callback and restores the original afterwards. + * @param {string} name The name of the message to send. + * @param {!Array} params The parameters to send. + * @param {string} callbackName The name of the function that the backend calls. + * @param {!Function} The function to call. + */ +function chromeSend(name, params, callbackName, callback) { + var old = global[callbackName]; + global[callbackName] = function() { + // restore + global[callbackName] = old; + + var args = Array.prototype.slice.call(arguments); + return callback.apply(global, args); + }; + chrome.send(name, params); +} + + +/** + * Generates a CSS url string. + * @param {string} s The URL to generate the CSS url for. + * @return {string} The CSS url string. + */ +function url(s) { + // http://www.w3.org/TR/css3-values/#uris + // Parentheses, commas, whitespace characters, single quotes (') and double + // quotes (") appearing in a URI must be escaped with a backslash + var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); + // WebKit has a bug when it comes to URLs that end with \ + // https://bugs.webkit.org/show_bug.cgi?id=28885 + if (/\\\\$/.test(s2)) { + // Add a space to work around the WebKit bug. + s2 += ' '; + } + return 'url("' + s2 + '")'; +} diff --git a/chrome/browser/resources/bookmark_manager/main.html b/chrome/browser/resources/bookmark_manager/main.html new file mode 100644 index 0000000..823e973 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/main.html @@ -0,0 +1,1684 @@ +<!DOCTYPE html> +<html> +<!-- + +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. + + +This is work in progress: + +i18n: Expose a chrome.experimental.bookmarkManager.getLocalStrings + +import/export: Expose in experimental extension API. + +Internal DnD: Buggy when dragging multiple items (the order of the dropped items +is not correct. + +External DnD: Chrome doesn't follow HTML5 and it limits the data types to text +and a single URL. Fixing Chrome is unreasonable given our current time frame. +There are two options here. Disable external DnD or expose enough hooks in the +experimental bookmarks manager extension API. + +Clipboard: Once again Chrome does not correctly implement HTML5 and it only +allows text and url. We can either disable the clipboard actions, only allow +internal clipboard or expose the hooks in the extension api. + +Favicons: chrome-extension: is not allowed to access chrome://favicon. We need +to whitelist it or expose a way to get the data URI for the favicon (slow and +sucky). + +Favicon of bmm does not work. No icon is showed. + +--> +<head> +<title i18n-content="title"></title> +<link rel="stylesheet" href="css/list.css"> +<link rel="stylesheet" href="css/tree.css"> +<script src="css/tree.css.js"></script> +<link rel="stylesheet" href="css/menu.css"> + +<script src="js/cr.js"></script> +<script src="js/cr/event.js"></script> +<script src="js/cr/eventtarget.js"></script> +<script src="js/cr/ui.js"></script> +<script src="js/cr/ui/listselectionmodel.js"></script> +<script src="js/cr/ui/listitem.js"></script> +<script src="js/cr/ui/list.js"></script> +<script src="js/cr/ui/tree.js"></script> +<script src="js/cr/ui/command.js"></script> +<script src="js/cr/ui/menuitem.js"></script> +<script src="js/cr/ui/menu.js"></script> +<script src="js/cr/ui/menubutton.js"></script> +<script src="js/cr/ui/contextmenuhandler.js"></script> + +<script src="js/util.js"></script> +<script src="js/localstrings.js"></script> +<script src="js/i18ntemplate.js"></script> + +<script src="js/bmm.js"></script> +<script src="js/bmm/bookmarklist.js"></script> +<script src="js/bmm/bookmarktree.js"></script> + +<style> + +html, body { + margin: 0; + width: 100%; + height: 100%; + /*-webkit-user-select: none;*/ + cursor: default; + font: 13px arial; +} + +list { + display: block; + overflow-x: hidden; + overflow-y: visible; /* let the container do the scrolling */ +} + +list > * { + text-decoration: none; + padding: 5px; +} + +list > * > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + color: black; + width: 100%; + -webkit-box-sizing: border-box; + background: 0 50% no-repeat; + -webkit-padding-start: 20px; +} + +list > * > * > span { + -webkit-transition: all .15s; + text-decoration: none; + color: #000; + cursor: pointer; + opacity: .7; +} + +list > * > :first-child { + font-weight: bold; + font-size: 14px; +} + +list > * > :last-child { + overflow: hidden; +} + +list > * > * > .folder { + background-image: url("images/folder_closed.png"); + background-repeat: no-repeat; + background-position: 0% 50%; + display: inline-block; + -webkit-padding-start: 18px; +} + +html[dir=rtl] list > * > * > .folder { + background-image: url("images/folder_closed_rtl.png"); + background-position: 100% 50%; +} + +html[os=mac] list > * > * > .folder { + background-image: url("images/bookmark_bar_folder_mac.png"); +} + +list > * > * > :hover { + text-decoration: underline; + color: -webkit-link; + opacity: 1; +} + +list > * > * > :active { + color: -webkit-activelink; +} + +html[dir=rtl] list .label { + background-position: 100% 50%; +} + +list > .folder > .label { + background-image: url("images/folder_closed.png"); +} + +html[dir=rtl] list > .folder > .label { + background-image: url("images/folder_closed_rtl.png"); +} + +html[os=mac] list > .folder > .label { + background-image: url("images/bookmark_bar_folder_mac.png"); +} + +html[os=mac] .tree-label { + background-image: url("images/bookmark_bar_folder_mac.png"); +} + +html[os=mac] .tree-row[selected] > .tree-label { + background-image: url("images/bookmark_bar_folder_mac.png"); +} + +.main { + position: absolute; + top: 75px; + left: 0; + right: 0; + bottom: 0; +} + +#tree-container { + position: absolute; + left: 0; + width: 200px; + top: 0; + bottom: 0; + overflow: auto; + -webkit-box-sizing: border-box; + padding: 0px 5px 5px 5px; +} + +#tree { + min-width: 100%; + overflow: visible; /* let the container do the scrolling */ + display: inline-block; +} + +#list { + position: absolute; + left: 200px; + right: 0; + top: 0; + bottom: 0; + -webkit-box-sizing: border-box; + padding: 0 5px 5px 5px; +} + +.logo { + -webkit-appearance: none; + border: 0; + background: transparent; + background: 50% 50% no-repeat url("images/bookmarks_section.png"); + width: 67px; + height: 67px; + cursor: pointer; + vertical-align: bottom; + margin: 5px; +} + +html[dir=rtl] #tree-container { + left: auto; + right: 0; +} + +html[dir=rtl] #list { + left: 0; + right: 200px; +} + +.header > div { + display: inline-block; + margin: 5px; +} + +#drop-overlay { + position: absolute; + display: none; + pointer-events: none; + border: 1px solid hsl(214, 91%, 85%);; + -webkit-border-radius: 3px; + -webkit-box-sizing: border-box; + background-color: hsla(214, 91%, 85%, .5); + overflow: hidden; + z-index: -1; +} + +#drop-overlay.line { + border: 3px solid black; + border-top-color: transparent; + border-bottom-color: transparent; + background-color: black; + background-clip: padding-box; + height: 8px; + -webkit-border-radius: 0; + z-index: 10; +} + +.toolbar button { + -webkit-appearance: none; + border: none; + background: transparent; + font: inherit; + padding: 0; +} + +</style> +<script> + +// Sometimes the extension API is not initialized. +if (!chrome.bookmarks) + window.location.reload(); + +// Allow platform specific CSS rules. +if (/Mac/.test(navigator.platform)) + document.documentElement.setAttribute('os', 'mac'); + +</script> +</head> +<body> + +<div class="header"> + <button onclick="resetSearch()" class="logo" tabindex=3></button> + <div> + <form onsubmit="setSearch(this.term.value); return false;" + class="form"> + <input type="text" id="term" tabindex=1 autofocus> + <input type="submit" i18n-values=".value:search_button" tabindex=1> + </form> + <div class=toolbar> + <button menu="#organize-menu" tabindex="-1" i18n-content="organize_menu"></button> + <button menu="#tools-menu" tabindex="-1" i18n-content="tools_menu"></button> + </div> + </div> +</div> + +<div class=main> + <div id=tree-container> + <tree id=tree tabindex=2></tree> + </div> + <list id=list tabindex=2></list> +</div> + + +<script> + +const BookmarkList = bmm.BookmarkList; +const BookmarkTree = bmm.BookmarkTree; +const ListItem = cr.ui.ListItem; +const TreeItem = cr.ui.TreeItem; + +/** + * The id of the bookmark root. + * @type {number} + */ +const ROOT_ID = '0'; + +var bookmarkCache = { + /** + * This returns a reference to the bookmark node that is cached by the tree + * or list. Use this funciton when we need to update the local cachea after + * changes. It only returns bookmarks that are used by the tree and/or the + * list. + * @param {string} The ID of the bookmark that we want to get. + * @return {BookmarkTreeNode} + */ + getById: function(id) { + var el = bmm.treeLookup[id] || bmm.listLookup[id]; + return el && el.bookmarkNode; + }, + + /** + * Removes the cached item from both the list and tree lookups. + */ + remove: function(id) { + delete bmm.listLookup[id]; + + var treeItem = bmm.treeLookup[id]; + if (treeItem) { + var items = treeItem.items; // is an HTMLCollection + for (var i = 0, item; item = items[i]; i++) { + var bookmarkNode = item.bookmarkNode; + delete bmm.treeLookup[bookmarkNode.id]; + } + delete bmm.treeLookup[id]; + } + }, + + /** + * Updates the underlying bookmark node for the tree items and list items by + * querying the bookmark backend. + * @param {string} id The id of the node to update the children for. + * @param {Function=} opt_f A funciton to call when done. + */ + updateChildren: function(id, opt_f) { + function updateItem(bookmarkNode) { + var treeItem = bmm.treeLookup[bookmarkNode.id]; + if (treeItem) { + treeItem.bookmarkNode = bookmarkNode; + } + var listItem = bmm.listLookup[bookmarkNode.id]; + if (listItem) { + listItem.bookmarkNode = bookmarkNode; + } + } + + chrome.bookmarks.getChildren(id, function(children) { + children.forEach(updateItem); + + if (opt_f) + opt_f(children); + }); + } +}; + +</script> +<script> + +BookmarkList.decorate(list); + +var searchTreeItem = new TreeItem({ + label: 'Search', + icon: 'images/bookmark_manager_search.png', + bookmarkId: 'q=' +}); +bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; + +var recentTreeItem = new TreeItem({ + label: 'Recent', + icon: 'images/bookmark_manager_recent.png', + bookmarkId: 'recent' +}); +bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem; + +BookmarkTree.decorate(tree); + +tree.addEventListener('change', function() { + navigateTo(tree.selectedItem.bookmarkId); +}); + +</script> +<script> + +/** + * Navigates to a bookmark ID. + * @param {string} id The ID to navigate to. + */ +function navigateTo(id) { + console.info('navigateTo', window.location.hash, id); + window.location.hash = id; + updateParentId(id); +} + +/** + * Updates the parent ID of the bookmark list and selects the correct tree item. + * @param {string} id The id. + */ +function updateParentId(id) { + list.parentId = id; + if (id in bmm.treeLookup) + tree.selectedItem = bmm.treeLookup[id]; +} + +// We listen to hashchange so that we can update the currently shown folder when +// the user goes back and forward in the history. +window.onhashchange = function(e) { + var id = window.location.hash.slice(1); + + var valid = false; + + // In case we got a search hash update the text input and the bmm.treeLookup + // to use the new id. + if (/^q=/.test(id)) { + delete bmm.treeLookup[searchTreeItem.bookmarkId]; + $('term').value = id.slice(2); + searchTreeItem.bookmarkId = id; + bmm.treeLookup[id] = searchTreeItem; + valid = true; + } else if (id == 'recent') { + valid = true; + } + + if (valid) { + updateParentId(id); + } else { + // We need to verify that this is a correct ID. + chrome.bookmarks.get(id, function(items) { + if (items && items.length == 1) + updateParentId(id); + }); + } +}; + +list.addEventListener('activate', function(e) { + var bookmarkNodes = getSelectedBookmarkNodes(); + + // If we double clicked or pressed enter on a single folder navigate to it. + if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) { + navigateTo(bookmarkNodes[0].id); + } else { + var command = $('open-in-new-tab-command'); + command.execute(); + } +}); + +// The list dispatches an event when the user clicks on the URL or the Show in +// folder part. +list.addEventListener('urlClicked', function(e) { + openUrls([e.url], e.kind); +}); + +/** + * Timer id used for delaying find-as-you-type + */ +var inputDelayTimer; + +// Capture input changes to the search term input element and delay searching +// for 250ms to reduce flicker. +$('term').oninput = function(e) { + clearTimeout(inputDelayTimer); + inputDelayTimer = setTimeout(function() { + setSearch($('term').value); + }, 250); +}; + +/** + * Navigates to the search results for the search text. + * @para {string} searchText The text to search for. + */ +function setSearch(searchText) { + navigateTo('q=' + searchText); +} + +/** + * Clears the search. + */ +function resetSearch() { + $('term').value = ''; + setSearch(''); + $('term').focus(); +} + +/** + * Called when the title of a bookmark changes. + * @param {string} id + * @param {!Object} changeInfo + */ +function handleBookmarkChanged(id, changeInfo) { + // console.log('handleBookmarkChanged', id, changeInfo); + list.handleBookmarkChanged(id, changeInfo); + tree.handleBookmarkChanged(id, changeInfo); +} + +/** + * Callback for when the user reorders by title. + * @param {string} id The id of the bookmark folder that was reordered. + * @param {!Object} reorderInfo The information about how the items where + * reordered. + */ +function handleChildrenReordered(id, reorderInfo) { + // console.info('handleChildrenReordered', id, reorderInfo); + list.handleChildrenReordered(id, reorderInfo); + tree.handleChildrenReordered(id, reorderInfo); + bookmarkCache.updateChildren(id); +} + +/** + * Callback for when a bookmark node is created. + * @param {string} id The id of the newly created bookmark node. + * @param {!Object} bookmarkNode The new bookmark node. + */ +function handleCreated(id, bookmarkNode) { + // console.info('handleCreated', id, bookmarkNode); + list.handleCreated(id, bookmarkNode); + tree.handleCreated(id, bookmarkNode); + bookmarkCache.updateChildren(bookmarkNode.parentId); +} + +function handleMoved(id, moveInfo) { + // console.info('handleMoved', id, moveInfo); + list.handleMoved(id, moveInfo); + tree.handleMoved(id, moveInfo); + + bookmarkCache.updateChildren(moveInfo.parentId); + if (moveInfo.parentId != moveInfo.oldParentId) + bookmarkCache.updateChildren(moveInfo.oldParentId); +} + +function handleRemoved(id, removeInfo) { + // console.info('handleRemoved', id, removeInfo); + list.handleRemoved(id, removeInfo); + tree.handleRemoved(id, removeInfo); + + bookmarkCache.updateChildren(removeInfo.parentId); + bookmarkCache.remove(id); +} + +function handleImportBegan() { + chrome.bookmarks.onCreated.removeListener(handleCreated); +} + +function handleImportEnded() { + chrome.bookmarks.onCreated.addListener(handleCreated); + chrome.bookmarks.getTree(function(node) { + var otherBookmarks = node[0].children[1].children; + var importedFolder = otherBookmarks[otherBookmarks.length - 1]; + var importId = importedFolder.id; + tree.insertSubtree(importedFolder); + navigateTo(importId) + }); +} + +/** + * Adds the listeners for the bookmark model change events. + */ +function addBookmarkModelListeners() { + chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); + chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered); + chrome.bookmarks.onCreated.addListener(handleCreated); + chrome.bookmarks.onMoved.addListener(handleMoved); + chrome.bookmarks.onRemoved.addListener(handleRemoved); + chrome.experimental.bookmarkManager.onImportBegan.addListener( + handleImportBegan); + chrome.experimental.bookmarkManager.onImportEnded.addListener( + handleImportEnded); +} + +/** + * This returns the user visible path to the folder where the bookmark is + * located. + * @param {number} parentId The ID of the parent folder. + * @return {string} The path to the the bookmark, + */ +function getFolder(parentId) { + var parentNode = tree.getBookmarkNodeById(parentId); + if (parentNode) { + var s = parentNode.title; + if (parentNode.parentId != ROOT_ID) { + return getFolder(parentNode.parentId) + '/' + s; + } + return s; + } +} + +tree.addEventListener('load', function(e) { + // Add hard coded tree items + tree.add(recentTreeItem); + tree.add(searchTreeItem); + + // Now we can select a tree item. + var hash = window.location.hash.slice(1); + if (!hash) { + // If we do not have a hash select first item in the tree. + hash = tree.items[0].bookmarkId; + } + + if (/^q=/.test(hash)) + $('term').value = hash.slice(2); + navigateTo(hash); +}); + +tree.buildTree(); +addBookmarkModelListeners(); + +</script> +<script> + +var dnd = { + + DND_EFFECT: cr.isMac ? 'move' : 'copy', // http://crbug.com/14654 + + dragBookmarkNodes: [], + + getBookmarkElement: function(el) { + while (el && !el.bookmarkNode) { + el = el.parentNode; + } + return el; + }, + + // If we are over the list and the list is showing recent or search result + // we cannot drop. + isOverRecentOrSearch: function(overElement) { + return (list.isRecent() || list.isSearch()) && list.contains(overElement); + }, + + checkEvery_: function(f, dragBookmarkNodes, overBookmarkNode, overElement) { + return dragBookmarkNodes.every(function(dragBookmarkNode) { + return f.call(this, dragBookmarkNode, overBookmarkNode, overElement); + }, this); + }, + + /** + * This is a first pass wether we can drop the dragged items. + * + * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are + * currently being dragged. + * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are + * currently dragging over. + * @param {!HTMLElement} overElement The element that we are currently + * dragging over. + * @return {boolean} If this returns false then we know we should not drop + * the items. If it returns true we still have to call canDropOn, + * canDropAbove and canDropBelow. + */ + canDrop: function(dragBookmarkNodes, overBookmarkNode, overElement) { + return this.checkEvery_(this.canDrop_, dragBookmarkNodes, + overBookmarkNode, overElement); + }, + + /** + * Helper for canDrop that only checks one bookmark node. + * @private + */ + canDrop_: function(dragBookmarkNode, overBookmarkNode, overElement) { + if (overBookmarkNode.id == dragBookmarkNode.id) + return false; + + if (this.isOverRecentOrSearch(overElement)) + return false; + + if (dragBookmarkNode.id == overBookmarkNode.id) + return false; + + // If we are dragging a folder we cannot drop it on any of its descendants + if (bmm.isFolder(dragBookmarkNode) && + bmm.contains(dragBookmarkNode, overBookmarkNode)) { + return false; + } + + return true; + }, + + /** + * Whether we can drop the dragged items above the drop target. + * + * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are + * currently being dragged. + * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are + * currently dragging over. + * @param {!HTMLElement} overElement The element that we are currently + * dragging over. + * @return {boolean} Whether we can drop the dragged items above the drop + * target. + */ + canDropAbove: function(dragBookmarkNodes, overBookmarkNode, overElement) { + return this.checkEvery_(this.canDropAbove_, dragBookmarkNodes, + overBookmarkNode, overElement); + }, + + /** + * Helper for canDropAbove that only checks one bookmark node. + * @private + */ + canDropAbove_: function(dragBookmarkNode, overBookmarkNode, overElement) { + if (overElement instanceof BookmarkList) + return false; + + // If dragBookmarkNode is a non folder and overElement is a tree item we + // cannot drop it above or below. + if (!bmm.isFolder(dragBookmarkNode) && overElement instanceof TreeItem) + return false; + + // We cannot drop between Bookmarks bar and Other bookmarks + if (overBookmarkNode.parentId == ROOT_ID) + return false; + + // We cannot drop above if the item below is already in the drag source + var previousElement = overElement.previousElementSibling; + if (previousElement && + previousElement.bookmarkNode.id == dragBookmarkNode.id) + return false; + + return true; + }, + + /** + * Whether we can drop the dragged items below the drop target. + * + * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are + * currently being dragged. + * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are + * currently dragging over. + * @param {!HTMLElement} overElement The element that we are currently + * dragging over. + * @return {boolean} Whether we can drop the dragged items below the drop + * target. + */ + canDropBelow: function(dragBookmarkNodes, overBookmarkNode, overElement) { + return this.checkEvery_(this.canDropBelow_, dragBookmarkNodes, + overBookmarkNode, overElement); + }, + + /** + * Helper for canDropBelow that only checks one bookmark node. + * @private + */ + canDropBelow_: function(dragBookmarkNode, overBookmarkNode, overElement) { + if (overElement instanceof BookmarkList) + return false; + + // The tree can only hold folders so if we are over a tree item we cannot + // drop a non folder. + if (!bmm.isFolder(dragBookmarkNode) && overElement instanceof TreeItem) + return false; + + // We cannot drop between Bookmarks bar and Other bookmarks + if (overBookmarkNode.parentId == ROOT_ID) + return false; + + // We cannot drop below if the item below is already in the drag source + var nextElement = overElement.nextElementSibling; + if (nextElement && + nextElement.bookmarkNode.id == dragBookmarkNode.id) + return false; + + // Don't allow dropping below an expanded tree item since it is confusing + // to the user anyway. + if (overElement instanceof TreeItem && overElement.expanded) + return false; + + return true; + }, + + /** + * Whether we can drop the dragged items on the drop target. + * + * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are + * currently being dragged. + * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are + * currently dragging over. + * @param {!HTMLElement} overElement The element that we are currently + * dragging over. + * @return {boolean} Whether we can drop the dragged items on the drop + * target. + */ + canDropOn: function(dragBookmarkNodes, overBookmarkNode, overElement) { + return this.checkEvery_(this.canDropOn_, dragBookmarkNodes, + overBookmarkNode, overElement); + }, + + /** + * Helper for canDropOn that only checks one bookmark node. + * @private + */ + canDropOn_: function(dragBookmarkNode, overBookmarkNode, overElement) { + // We can only drop on a folder... + if (!bmm.isFolder(overBookmarkNode)) + return false; + + if (overElement instanceof BookmarkList) { + // We are trying to drop an item after the last item in the list. This + // is allowed if the item is different from the last item in the list + var listItems = list.items; + var len = listItems.length; + if (len == 0 || + listItems[len - 1].bookmarkNode.id != dragBookmarkNode.id) { + return true; + } + } + + // Cannot drop on current parent + if (overBookmarkNode.id == dragBookmarkNode.parentId) + return false; + + return true; + }, + + /** + * Callback for the dragstart event. + * @param {Event} e The dragstart event. + */ + handleDragStart: function(e) { + // console.log(e.type); + + // Determine the selected bookmarks. + var target = e.target; + var draggedItems = []; + if (target instanceof ListItem) { + // Use selected items. + draggedItems = target.parentNode.selectedItems; + } else if (target instanceof TreeItem) { + draggedItems.push(target); + } + + this.dragBookmarkNodes = draggedItems.map(function(item) { + return item.bookmarkNode; + }); + + // console.log(draggedItems, this.dragBookmarkNodes) + + // TODO(arv): Fix this once we expose DnD in the extension API + // Mac requires setData to be called + e.dataTransfer.setData('text/uri-list', 'http://www.google.com/'); + e.dataTransfer.effectAllowed = this.DND_EFFECT; + + if (!this.dragBookmarkNodes.length) { + e.preventDefault(); + } + }, + + handleDragEnter: function(e) { + // console.log(e.type); + + e.preventDefault(); + }, + + /** + * Calback for the dragover event. + * @param {Event} e The dragover event. + */ + handleDragOver: function(e) { + // console.log(e.type); + + if (!this.dragBookmarkNodes.length) + return; + + var overElement = this.getBookmarkElement(e.target); + if (!overElement && e.target == list) + overElement = list; + + if (!overElement) + return; + + var dragBookmarkNodes = this.dragBookmarkNodes; + var overBookmarkNode = overElement.bookmarkNode; + + if (!this.canDrop(dragBookmarkNodes, overBookmarkNode, overElement)) + return; + + var bookmarkNode = overElement.bookmarkNode; + + var canDropAbove = this.canDropAbove(dragBookmarkNodes, overBookmarkNode, + overElement); + var canDropOn = this.canDropOn(dragBookmarkNodes, overBookmarkNode, + overElement); + var canDropBelow = this.canDropBelow(dragBookmarkNodes, overBookmarkNode, + overElement); + + if (!canDropAbove && !canDropOn && !canDropBelow) + return; + + // Now we know that we can drop. Determine if we will drop above, on or + // below based on mouse position etc. + + var dropPos; + e.preventDefault(); + // TODO(arv): Fix this once we expose DnD in the extension API + e.dataTransfer.dropEffect = this.DND_EFFECT; + + var rect; + if (overElement instanceof TreeItem) { + // We only want the rect of the row representing the item and not + // its children + rect = overElement.rowElement.getBoundingClientRect(); + } else { + rect = overElement.getBoundingClientRect(); + } + + if (canDropAbove && !canDropOn && !canDropBelow) { + dropPos = 'above'; + } else if (!canDropAbove && canDropOn && !canDropBelow) { + dropPos = 'on'; + } else if (!canDropAbove && !canDropOn && canDropBelow) { + dropPos = 'below'; + } else { + // We need to compare the mouse position with the element rect. + + var dy = e.clientY - rect.top; + var yRatio = dy / rect.height; + if (!canDropOn) { + dropPos = yRatio < .5 ? 'above' : 'below'; + } else if (!canDropAbove) { + dropPos = yRatio < .5 ? 'on' : 'below'; + } else if (!canDropBelow) { + dropPos = yRatio < .5 ? 'above' : 'on'; + } else { + dropPos = yRatio < .25 ? 'above' : yRatio > .75 ? 'below' : 'on'; + } + } + + function cloneClientRect(rect) { + var newRect = {}; + for (var key in rect) { + newRect[key] = rect[key]; + } + return newRect; + } + + // If we are dropping above or below a tree item adjust the width so + // that it is clearer where the item will be dropped. + if ((dropPos == 'above' || dropPos == 'below' ) && + overElement instanceof TreeItem) { + // ClientRect is read only so clone in into a read-write object. + rect = cloneClientRect(rect); + var rtl = getComputedStyle(overElement).direction == 'rtl'; + var labelElement = overElement.labelElement; + var labelRect = labelElement.getBoundingClientRect(); + if (rtl) { + rect.width = labelRect.left + labelRect.width - rect.left; + } else { + rect.left = labelRect.left; + rect.width -= rect.left + } + } + + var overlayType = dropPos; + + // If we are dropping on a list we want to show a overlay drop line after + // the last element + if (overElement instanceof BookmarkList) { + overlayType = 'below'; + + // Get the rect of the last list item. + var items = overElement.items; + var length = items.length; + if (length) { + dropPos = 'below'; + overElement = items[length - 1]; + rect = overElement.getBoundingClientRect(); + } else { + // If there are no items, collapse the height of the rect + rect = cloneClientRect(rect); + rect.height = 0; + // We do not use bottom so we don't care to adjust it. + } + } + + this.showDropOverlay_(rect, overlayType); + + // TODO(arv): Multiple selection DnD. + this.dropDestination = { + dropPos: dropPos, + relatedNode: overElement.bookmarkNode + }; + }, + + /** + * Shows and positions the drop marker overlay. + * @param {ClientRect} targetRect The drop target rect + * @param {string} overlayType The position relative to the target rect. + * @private + */ + showDropOverlay_: function(targetRect, overlayType) { + window.clearTimeout(this.hideDropOverlayTimer_); + var overlay = $('drop-overlay'); + if (overlayType == 'on') { + overlay.className = ''; + overlay.style.top = targetRect.top + 'px'; + overlay.style.height = targetRect.height + 'px'; + } else { + overlay.className = 'line'; + overlay.style.height = ''; + } + overlay.style.width = targetRect.width + 'px'; + overlay.style.left = targetRect.left + 'px'; + overlay.style.display = 'block'; + + if (overlayType != 'on') { + var overlayRect = overlay.getBoundingClientRect(); + if (overlayType == 'above') { + overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px'; + } else { + overlay.style.top = targetRect.top + targetRect.height - + overlayRect.height / 2 + 'px'; + } + } + }, + + /** + * Hides the drop overlay element. + * @private + */ + hideDropOverlay_: function() { + // Hide the overlay in a timeout to reduce flickering as we move between + // valid drop targets. + window.clearTimeout(this.hideDropOverlayTimer_); + this.hideDropOverlayTimer_ = window.setTimeout(function() { + $('drop-overlay').style.display = ''; + }, 100); + }, + + handleDragLeave: function(e) { + // console.log(e.type); + + this.hideDropOverlay_(); + }, + + handleDrop: function(e) { + // console.log(e.type); + + if (this.dropDestination && this.dragBookmarkNodes.length) { + // console.log('Drop', this.dragBookmarkNodes, this.dropDestination); + + var dropPos = this.dropDestination.dropPos; + var relatedNode = this.dropDestination.relatedNode; + var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId; + + var moveInfo = { + parentId: parentId + }; + + if (dropPos == 'above') { + moveInfo.index = relatedNode.index; + } else if (dropPos == 'below') { + moveInfo.index = relatedNode.index + 1; + } + + // TODO(arv): Add support for multiple move in bookmarks API? + this.dragBookmarkNodes.forEach(function(bookmarkNode) { + var id = bookmarkNode.id; + // console.info('Calling move', id, moveInfo.index, bookmarkNode); + chrome.bookmarks.move(id, moveInfo, + function(result) { + // console.log('chrome.bookmarks.move', arguments); + }); + moveInfo.index++; + }); + + // TODO(arv): Select the newly dropped items. + } + this.dropDestination = null; + this.hideDropOverlay_(); + }, + + handleDrag: function(e) { + // console.log(e.type); + }, + + handleDragEnd: function(e) { + // console.log(e.type); + + var self = this; + // Chromium Win incorrectly fires the dragend event before the drop event. + // http://code.google.com/p/chromium/issues/detail?id=31292 + window.setTimeout(function() { + self.dragBookmarkNodes = []; + self = null; + }, 1) + }, + + init: function() { + document.addEventListener('dragstart', cr.bind(this.handleDragStart, this)); + document.addEventListener('dragenter', cr.bind(this.handleDragEnter, this)); + document.addEventListener('dragover', cr.bind(this.handleDragOver, this)); + document.addEventListener('dragleave', cr.bind(this.handleDragLeave, this)); + document.addEventListener('drop', cr.bind(this.handleDrop, this)); + document.addEventListener('dragend', cr.bind(this.handleDragEnd, this)); + document.addEventListener('drag', cr.bind(this.handleDrag, this)); + } + +}; + +dnd.init(); + + +</script> + +<!-- Organize menu --> +<command i18n-values=".label:rename_folder" id="rename-folder-command"></command> +<command i18n-values=".label:edit" id="edit-command"></command> +<command i18n-values=".label:delete" id="delete-command"></command> +<command i18n-values=".label:show_in_folder" id="show-in-folder-command"></command> +<command i18n-values=".label:cut" id="cut-command"></command> +<command i18n-values=".label:copy" id="copy-command"></command> +<command i18n-values=".label:paste" id="paste-command"></command> +<command i18n-values=".label:sort" id="sort-command"></command> +<command i18n-values=".label:add_new_bookmark" id="add-new-bookmark-command"></command> +<command i18n-values=".label:new_folder" id="new-folder-command"></command> + +<!-- Tools menu --> +<command i18n-values=".label:import_menu" id="import-menu-command"></command> +<command i18n-values=".label:export_menu" id="export-menu-command"></command> + +<!-- open * are handled in canExecute handler --> +<command id="open-in-new-tab-command"></command> +<command id="open-in-new-window-command"></command> +<command id="open-incognito-window-command"></command> + +<!-- TODO(arv): I think the commands might be better created in code? --> + +<menu id="organize-menu"> + <button command="#rename-folder-command"></button> + <button command="#edit-command"></button> + <button command="#delete-command"></button> + <button command="#show-in-folder-command"></button> + <hr> + <button command="#cut-command"></button> + <button command="#copy-command"></button> + <button command="#paste-command"></button> + <hr> + <button command="#sort-command"></button> + <hr> + <button command="#add-new-bookmark-command"></button> + <button command="#new-folder-command"></button> +</menu> + +<menu id="tools-menu"> + <button command="#import-menu-command"></button> + <button command="#export-menu-command"></button> +</menu> + +<menu id="context-menu"> + <button command="#open-in-new-tab-command"></button> + <button command="#open-in-new-window-command"></button> + <button command="#open-incognito-window-command"></button> + <hr> + <button command="#rename-folder-command"></button> + <button command="#edit-command"></button> + <button command="#delete-command"></button> + <button command="#show-in-folder-command"></button> + <hr> + <button command="#cut-command"></button> + <button command="#copy-command"></button> + <button command="#paste-command"></button> + <hr> + <button command="#add-new-bookmark-command"></button> + <button command="#new-folder-command"></button> +</menu> + +<script> + +// Commands + +const Command = cr.ui.Command; +const CommandBinding = cr.ui.CommandBinding; +const Menu = cr.ui.Menu; +const MenuButton = cr.ui.MenuButton; + +cr.ui.decorate('menu', Menu); +cr.ui.decorate('button[menu]', MenuButton); +cr.ui.decorate('command', Command); + +cr.ui.contextMenuHandler.addContextMenuProperty(tree); +list.contextMenu = $('context-menu'); +tree.contextMenu = $('context-menu'); + +/** + * Helper function that updates the canExecute and labels for the open like + * commands. + * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system. + * @param {!cr.ui.Command} command The command we are currently precessing. + * @param {number} selectionCount The number of selected bookmarks. + */ +function updateOpenCommands(e, command, selectionCount) { + switch (command.id) { + case 'open-in-new-tab-command': + command.label = selectionCount == 1 ? + localStrings.getString('open_in_new_tab') : + localStrings.getString('open_all'); + break; + + case 'open-in-new-window-command': + command.label = selectionCount == 1 ? + localStrings.getString('open_in_new_window') : + localStrings.getString('open_all_new_window'); + break; + case 'open-incognito-window-command': + command.label = selectionCount == 1 ? + localStrings.getString('open_incognito') : + localStrings.getString('open_all_incognito'); + break; + } + e.canExecute = selectionCount > 0; +} + +/** + * Calls the backend to figure out if we can paste the clipboard into the active + * folder. + * @param {Function=} opt_f Function to call after the state has been + * updated. + */ +function updatePasteCommand(opt_f) { + function update(canPaste) { + var command = $('paste-command'); + command.disabled = !canPaste; + if (opt_f) + opt_f(); + } + // We cannot paste into search and recent view. + if (list.isSearch() || list.isRecent()) { + update(false); + } else { + chrome.experimental.bookmarkManager.canPaste(list.parentId, update); + } +} + +// We can always execute the import-menu and export-menu commands. +document.addEventListener('canExecute', function(e) { + var command = e.command; + var commandId = command.id; + if (commandId == 'import-menu-command' || commandId == 'export-menu-command') { + e.canExecute = true; + } +}); + +// Update canExecute for the commands when the list is the active element. +list.addEventListener('canExecute', function(e) { + var command = e.command; + var commandId = command.id; + + function hasSelected() { + return !!e.target.selectedItem; + } + + function hasSingleSelected() { + return e.target.selectedItems.length == 1; + } + + function isRecentOrSearch() { + return list.isRecent() || list.isSearch(); + } + + switch (commandId) { + case 'rename-folder-command': + // Show rename if a single folder is selected + var items = e.target.selectedItems; + if (items.length != 1) { + e.canExecute = false; + command.hidden = true; + } else { + var isFolder = bmm.isFolder(items[0].bookmarkNode); + e.canExecute = isFolder; + command.hidden = !isFolder; + } + break; + + case 'edit-command': + // Show the edit command if not a folder + var items = e.target.selectedItems; + if (items.length != 1) { + e.canExecute = false; + command.hidden = false; + } else { + var isFolder = bmm.isFolder(items[0].bookmarkNode); + e.canExecute = !isFolder; + command.hidden = isFolder; + } + break; + + case 'show-in-folder-command': + e.canExecute = isRecentOrSearch() && hasSingleSelected(); + break; + + case 'delete-command': + case 'cut-command': + case 'copy-command': + e.canExecute = hasSelected(); + break; + + case 'paste-command': + updatePasteCommand(); + break; + + case 'sort-command': + case 'add-new-bookmark-command': + case 'new-folder-command': + e.canExecute = !isRecentOrSearch(); + break; + + case 'open-in-new-tab-command': + case 'open-in-new-window-command': + case 'open-incognito-window-command': + updateOpenCommands(e, command, e.target.selectedItems.length); + break; + } +}); + +// Update canExecute for the commands when the tree is the active element. +tree.addEventListener('canExecute', function(e) { + var command = e.command; + var commandId = command.id; + + function hasSelected() { + return !!e.target.selectedItem; + } + + function isRecentOrSearch() { + var item = e.target.selectedItem; + return item == recentTreeItem || item == searchTreeItem; + } + + function isTopLevelItem() { + return e.target.selectedItem.parentNode == tree; + } + + switch (commandId) { + case 'rename-folder-command': + command.hidden = false; + e.canExecute = hasSelected() && !isTopLevelItem(); + break; + + case 'edit-command': + command.hidden = true; + e.canExecute = false; + break; + + case 'delete-command': + case 'cut-command': + case 'copy-command': + e.canExecute = hasSelected() && !isTopLevelItem(); + break; + + case 'paste-command': + updatePasteCommand(); + break; + + case 'sort-command': + case 'add-new-bookmark-command': + case 'new-folder-command': + e.canExecute = !isRecentOrSearch(); + break; + + case 'open-in-new-tab-command': + case 'open-in-new-window-command': + case 'open-incognito-window-command': + // We use "open all" when the tree is the activeElement and + // updateOpenCommands uses 0, 1 and > 1 to determine what to show. + updateOpenCommands(e, command, hasSelected() ? 2 : 0); + break; + } +}); + +/** + * Update the canExecute state of the commands when the selection changes. + * @param {Event} e The change event object. + */ +function updateCommandsBasedOnSelection(e) { + if (e.target == document.activeElement) { + // Paste only needs to updated when the tree selection changes. + var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit', + 'add-new-bookmark', 'new-folder', 'open-in-new-tab', + 'open-in-new-window', 'open-incognito-window']; + + if (e.target == tree) { + commandNames.push('paste', 'show-in-folder', 'sort'); + } + + commandNames.forEach(function(baseId) { + $(baseId + '-command').canExecuteChange(); + }); + } +} + +list.addEventListener('change', updateCommandsBasedOnSelection); +tree.addEventListener('change', updateCommandsBasedOnSelection); + +document.addEventListener('command', function(e) { + var command = e.command; + var commandId = command.id; + console.log(command.id, 'executed', 'on', e.target); + if (commandId == 'import-menu-command') { + chrome.experimental.bookmarkManager.import(); + } else if (command.id == 'export-menu-command') { + chrome.experimental.bookmarkManager.export(); + } +}); + +/** + * Navigates to the folder that the selected item is in and selects it. This is + * used for the show-in-folder command. + */ +function showInFolder() { + var bookmarkId = list.selectedItem.bookmarkNode.id; + var parentId = list.selectedItem.bookmarkNode.parentId; + + // After the list is loaded we should select the revealed item. + var f = function(e) { + var item = bmm.listLookup[bookmarkId]; + if (item) { + list.selectionModel.leadItem = item; + item.selected = true; + } + list.removeEventListener('load', f); + } + list.addEventListener('load', f); + var treeItem = bmm.treeLookup[parentId]; + treeItem.reveal(); + + navigateTo(parentId); +} + +/** + * Opens URLs in new tab, window or incognito mode. + * @param {!Array.<string>} urls The URLs to open. + * @param {string} kind The kind is either 'tab', 'window', or 'incognito'. + */ +function openUrls(urls, kind) { + if (urls.length < 1) + return; + + if (urls.length > 15) { + if (!confirm(localStrings.getStringF('should_open_all', urls.length))) + return; + } + + // Fix '#124' URLs since open those in a new window does not work. We prepend + // the base URL when we encounter those. + var base = window.location.href.split('#')[0]; + urls = urls.map(function(url) { + return url[0] == '#' ? base + url : url; + }); + + // Incognito mode is not yet supported by the extensions APIs. + // http://code.google.com/p/chromium/issues/detail?id=12658 + if (kind == 'window') { + chrome.windows.create({url: urls[0]}, function(window) { + urls.forEach(function(url, i) { + if (i > 0) + chrome.tabs.create({url: url, windowId: window.id, selected: false}); + }); + }); + } else if (kind == 'tab') { + urls.forEach(function(url, i) { + chrome.tabs.create({url: url, selected: !i}); + }); + } else { + window.location.href = urls[0]; + } +} + +/** + * Returns the selected bookmark nodes of the active element. Only call this + * if the list or the tree is focused. + * @return {!Array} Array of bookmark nodes. + */ +function getSelectedBookmarkNodes() { + if (document.activeElement == list) { + return list.selectedItems.map(function(item) { + return item.bookmarkNode; + }); + } else if (document.activeElement == tree) { + return [tree.selectedItem.bookmarkNode]; + } else { + throw Error('getSelectedBookmarkNodes called when wrong element focused.'); + } +} + +/** + * @return {!Array.<string>} An array of the selected bookmark IDs. + */ +function getSelectedBookmarkIds() { + return getSelectedBookmarkNodes().map(function(node) { + return node.id; + }); +} + +/** + * Opens the selected bookmarks. + */ +function openBookmarks(kind) { + // If we have selected any folders we need to find all items recursively. + // We can do several async calls to getChildren but instead we do a single + // call to getTree and only add the subtrees of the selected items. + + var urls = []; + var idMap = {}; + + // Traverses the tree until it finds a node tree that should be added. Then + // we switch over to use addNodes. We could merge these two functions into + // one but that would make the code less readable. + function traverseNodes(node) { + if (node.id in idMap) { + addNodes(node); + } else if (node.children) { + for (var i = 0; i < node.children.length; i++) { + traverseNodes(node.children[i]); + } + } + } + + // Adds the node and all the descendants + function addNodes(node) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + addNodes(node.children[i]); + } + } else { + urls.push(node.url); + } + } + + var nodes = getSelectedBookmarkNodes(); + + // Create a map for simpler lookup later. + nodes.forEach(function(node) { + idMap[node.id] = true; + }); + chrome.bookmarks.getTree(function(node) { + traverseNodes(node[0]); + openUrls(urls, kind); + }); +} + +/** + * Deletes the selected bookmarks. + */ +function deleteBookmarks() { + getSelectedBookmarkIds().forEach(function(id) { + chrome.bookmarks.removeTree(id); + }); +} + +function handleCommand(e) { + var command = e.command; + var commandId = command.id; + switch (commandId) { + case 'show-in-folder-command': + showInFolder(); + break; + case 'open-in-new-tab-command': + openBookmarks('tab'); + break; + case 'open-in-new-window-command': + openBookmarks('window'); + break; + case 'open-in-new-incognito-command': + openBookmarks('incognito'); + break; + case 'delete-command': + deleteBookmarks(); + break; + case 'copy-command': + chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds()); + break; + case 'cut-command': + chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds()); + break; + case 'paste-command': + chrome.experimental.bookmarkManager.paste(list.parentId); + break; + case 'sort-command': + chrome.experimental.bookmarkManager.sortChildren(list.parentId); + break; + } +} + +// TODO(arv): Move shortcut to HTML? + +// Meta+Backspace on Mac, Del on other platforms. +$('delete-command').shortcut = cr.isMac ? 'U+0008-meta' : 'U+007F'; + +list.addEventListener('command', handleCommand); +tree.addEventListener('command', handleCommand); + +// Listen to copy, cut and paste events and execute the associated commands. +document.addEventListener('copy', function(e) { + $('copy-command').execute(); +}); + +document.addEventListener('cut', function(e) { + $('cut-command').execute(); +}); + +document.addEventListener('paste', function(e) { + // Paste is a bit special since we need to do an async call to see if we can + // paste because the paste command might not be up to date. + updatePasteCommand(function() { + $('paste-command').execute(); + }); +}); + +</script> +<script> + +// TODO(arv): Remove hack when experimental API is available. + +var localStrings = new LocalStrings; + +/** + * Sets the i18n template data. + * @param {!Object} data The object with the i18n messages. + */ +function setTemplateData(data) { + // The strings may contain & which we need to strip. + for (var key in data) { + data[key] = data[key].replace(/&/, ''); + } + localStrings.templateData = data; + i18nTemplate.process(document, data); +} + +var useFallbackData = true; +if (chrome.experimental && + chrome.experimental.bookmarkManager && + chrome.experimental.bookmarkManager.getStrings) { + useFallbackData = false; + chrome.experimental.bookmarkManager.getStrings(function(data) { + setTemplateData(data); + }); +} + +if (useFallbackData) { + console.warn('The bookmark manager needs some experimental APIs'); + + // TODO(arv): This is just temporary while we are developing so that people + // without the experimental API can run this. + var fakeData = { + 'add_new_bookmark': 'Add page...', + 'copy': '&Copy', + 'cut': 'Cu&t', + 'delete': '&Delete', + 'edit': 'Edit...', + 'export_menu': 'Export bookmarks...', + 'import_menu': 'Import bookmarks...', + 'new_folder': 'Add folder...', + 'open_all': 'Open all bookmarks', + 'open_all_incognito': 'Open all bookmarks in incognito window', + 'open_all_new_window': 'Open all bookmarks in new window', + 'open_in_new_tab': 'Open in new tab', + 'open_in_new_window': 'Open in new window', + 'open_incognito': 'Open in incognito window', + 'organize_menu': 'Organize', + 'paste': '&Paste', + 'remove': 'Delete', + 'rename_folder': 'Rename...', + 'search_button': 'Search bookmarks', + 'should_open_all': 'Are you sure you want to open $1 tabs?', + 'show_in_folder': 'Show in folder', + 'sort': 'Reorder by title', + 'title': 'Bookmark Manager', + 'tools_menu': 'Tools' + }; + setTemplateData(fakeData); +} + +</script> + +<div id="drop-overlay"></div> + +</body> +</html> diff --git a/chrome/browser/resources/bookmark_manager/manifest.json b/chrome/browser/resources/bookmark_manager/manifest.json new file mode 100644 index 0000000..eca3190 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "Bookmark Manager", + "version": "0.1", + "description": "Bookmark Manager", + "permissions": [ + "bookmarks", + "experimental", + "tabs" + ] +} |