OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 |
| 6 cr.define('bmm', function() { |
| 7 // require cr.ui.define |
| 8 // require cr.ui.limitInputWidth. |
| 9 // require cr.ui.contextMenuHandler |
| 10 const List = cr.ui.List; |
| 11 const ListItem = cr.ui.ListItem; |
| 12 |
| 13 var listLookup = {}; |
| 14 |
| 15 /** |
| 16 * Removes all children and appends a new child. |
| 17 * @param {!Node} parent The node to remove all children from. |
| 18 * @param {!Node} newChild The new child to append. |
| 19 */ |
| 20 function replaceAllChildren(parent, newChild) { |
| 21 var n; |
| 22 while ((n = parent.lastChild)) { |
| 23 parent.removeChild(n); |
| 24 } |
| 25 parent.appendChild(newChild); |
| 26 } |
| 27 |
| 28 /** |
| 29 * Creates a new bookmark list. |
| 30 * @param {Object=} opt_propertyBag Optional properties. |
| 31 * @constructor |
| 32 * @extends {HTMLButtonElement} |
| 33 */ |
| 34 var BookmarkList = cr.ui.define('list'); |
| 35 |
| 36 BookmarkList.prototype = { |
| 37 __proto__: List.prototype, |
| 38 |
| 39 decorate: function() { |
| 40 List.prototype.decorate.call(this); |
| 41 this.addEventListener('click', this.handleClick_); |
| 42 }, |
| 43 |
| 44 parentId_: '', |
| 45 get parentId() { |
| 46 return this.parentId_; |
| 47 }, |
| 48 set parentId(parentId) { |
| 49 if (this.parentId_ == parentId) |
| 50 return; |
| 51 |
| 52 var oldParentId = this.parentId_; |
| 53 this.parentId_ = parentId; |
| 54 |
| 55 var callback = cr.bind(this.handleBookmarkCallback, this); |
| 56 |
| 57 if (!parentId) { |
| 58 callback([]); |
| 59 } else if (/^q=/.test(parentId)) { |
| 60 chrome.bookmarks.search(parentId.slice(2), callback); |
| 61 } else if (parentId == 'recent') { |
| 62 chrome.bookmarks.getRecent(50, callback); |
| 63 } else { |
| 64 chrome.bookmarks.getChildren(parentId, callback); |
| 65 } |
| 66 |
| 67 cr.dispatchPropertyChange(this, 'parentId', parentId, oldParentId); |
| 68 }, |
| 69 |
| 70 handleBookmarkCallback: function(items) { |
| 71 if (!items) { |
| 72 // Failed to load bookmarks. Most likely due to the bookmark beeing |
| 73 // removed. |
| 74 cr.dispatchSimpleEvent(this, 'invalidId'); |
| 75 return; |
| 76 } |
| 77 // Remove all fields without recreating the object since other code |
| 78 // references it. |
| 79 for (var id in listLookup){ |
| 80 delete listLookup[id]; |
| 81 } |
| 82 this.clear(); |
| 83 var showFolder = this.showFolder(); |
| 84 items.forEach(function(item) { |
| 85 var li = createListItem(item, showFolder); |
| 86 this.add(li); |
| 87 }, this); |
| 88 cr.dispatchSimpleEvent(this, 'load'); |
| 89 }, |
| 90 |
| 91 /** |
| 92 * The bookmark node that the list is currently displaying. If we are curren
tly |
| 93 * displaying recent or search this returns null. |
| 94 * @type {BookmarkTreeNode} |
| 95 */ |
| 96 get bookmarkNode() { |
| 97 if (this.isSearch() || this.isRecent()) |
| 98 return null; |
| 99 var treeItem = bmm.treeLookup[this.parentId]; |
| 100 return treeItem && treeItem.bookmarkNode; |
| 101 }, |
| 102 |
| 103 showFolder: function() { |
| 104 return this.isSearch() || this.isRecent(); |
| 105 }, |
| 106 |
| 107 isSearch: function() { |
| 108 return this.parentId_[0] == 'q'; |
| 109 }, |
| 110 |
| 111 isRecent: function() { |
| 112 return this.parentId_ == 'recent'; |
| 113 }, |
| 114 |
| 115 /** |
| 116 * Handles the clicks on the list so that we can check if the user clicked |
| 117 * on a link or an folder. |
| 118 * @private |
| 119 * @param {Event} e The click event object. |
| 120 */ |
| 121 handleClick_: function(e) { |
| 122 |
| 123 var el = e.target; |
| 124 if (el.href) { |
| 125 var event = this.ownerDocument.createEvent('Event'); |
| 126 event.initEvent('urlClicked', true, false); |
| 127 event.url = el.href; |
| 128 event.kind = e.shiftKey ? 'window' : e.button == 1 ? 'tab' : 'self'; |
| 129 this.dispatchEvent(event); |
| 130 } |
| 131 }, |
| 132 |
| 133 // Bookmark model update callbacks |
| 134 handleBookmarkChanged: function(id, changeInfo) { |
| 135 var listItem = listLookup[id]; |
| 136 if (listItem) { |
| 137 listItem.bookmarkNode.title = changeInfo.title; |
| 138 if ('url' in changeInfo) |
| 139 listItem.bookmarkNode.url = changeInfo['url']; |
| 140 updateListItem(listItem, listItem.bookmarkNode, list.showFolder()); |
| 141 } |
| 142 }, |
| 143 |
| 144 handleChildrenReordered: function(id, reorderInfo) { |
| 145 if (this.parentId == id) { |
| 146 var self = this; |
| 147 reorderInfo.childIds.forEach(function(id, i) { |
| 148 var li = listLookup[id] |
| 149 self.addAt(li, i); |
| 150 // At this point we do not read the index from the bookmark node so we |
| 151 // do not need to update it. |
| 152 li.bookmarkNode.index = i; |
| 153 }); |
| 154 } |
| 155 }, |
| 156 |
| 157 handleCreated: function(id, bookmarkNode) { |
| 158 if (this.parentId == bookmarkNode.parentId) { |
| 159 var li = createListItem(bookmarkNode, false); |
| 160 this.addAt(li, bookmarkNode.index); |
| 161 } |
| 162 }, |
| 163 |
| 164 handleMoved: function(id, moveInfo) { |
| 165 if (moveInfo.parentId == this.parentId || |
| 166 moveInfo.oldParentId == this.parentId) { |
| 167 |
| 168 if (moveInfo.oldParentId == moveInfo.parentId) { |
| 169 var listItem = listLookup[id]; |
| 170 if (listItem) { |
| 171 this.remove(listItem); |
| 172 this.addAt(listItem, moveInfo.index); |
| 173 } |
| 174 } else { |
| 175 if (moveInfo.oldParentId == this.parentId) { |
| 176 var listItem = listLookup[id]; |
| 177 if (listItem) { |
| 178 this.remove(listItem); |
| 179 delete listLookup[id]; |
| 180 } |
| 181 } |
| 182 |
| 183 if (moveInfo.parentId == list.parentId) { |
| 184 var self = this; |
| 185 chrome.bookmarks.get(id, function(bookmarkNodes) { |
| 186 var bookmarkNode = bookmarkNodes[0]; |
| 187 var li = createListItem(bookmarkNode, false); |
| 188 self.addAt(li, bookmarkNode.index); |
| 189 }); |
| 190 } |
| 191 } |
| 192 } |
| 193 }, |
| 194 |
| 195 handleRemoved: function(id, removeInfo) { |
| 196 var listItem = listLookup[id]; |
| 197 if (listItem) { |
| 198 this.remove(listItem); |
| 199 delete listLookup[id]; |
| 200 } |
| 201 } |
| 202 }; |
| 203 |
| 204 /** |
| 205 * The contextMenu property. |
| 206 * @type {cr.ui.Menu} |
| 207 */ |
| 208 cr.ui.contextMenuHandler.addContextMenuProperty(BookmarkList); |
| 209 |
| 210 /** |
| 211 * Creates a new bookmark list item. |
| 212 * @param {Object=} opt_propertyBag Optional properties. |
| 213 * @constructor |
| 214 * @extends {cr.ui.ListItem} |
| 215 */ |
| 216 var BookmarkListItem = cr.ui.define('li'); |
| 217 |
| 218 BookmarkListItem.prototype = { |
| 219 __proto__: ListItem.prototype, |
| 220 /** |
| 221 * Whether the user is currently able to edit the list item. |
| 222 * @type {boolean} |
| 223 */ |
| 224 get editing() { |
| 225 return this.hasAttribute('editing'); |
| 226 }, |
| 227 set editing(editing) { |
| 228 var oldEditing = this.editing; |
| 229 if (oldEditing == editing) |
| 230 return; |
| 231 |
| 232 var url = this.bookmarkNode.url; |
| 233 var title = this.bookmarkNode.title; |
| 234 var isFolder = bmm.isFolder(this.bookmarkNode); |
| 235 var listItem = this; |
| 236 var labelEl = this.firstChild; |
| 237 var urlEl = this.querySelector('.url'); |
| 238 var labelInput, urlInput; |
| 239 |
| 240 // Handles enter and escape which trigger reset and commit respectively. |
| 241 function handleKeydown(e) { |
| 242 // Make sure that the tree does not handle the key. |
| 243 e.stopPropagation(); |
| 244 |
| 245 // Calling list.focus blurs the input which will stop editing the list |
| 246 // item. |
| 247 switch (e.keyIdentifier) { |
| 248 case 'U+001B': // Esc |
| 249 labelInput.value = title; |
| 250 if (!isFolder) |
| 251 urlInput.value = url; |
| 252 // fall through |
| 253 cr.dispatchSimpleEvent(listItem, 'canceledit', true); |
| 254 case 'Enter': |
| 255 if (listItem.parentNode) |
| 256 listItem.parentNode.focus(); |
| 257 } |
| 258 } |
| 259 |
| 260 function handleBlur(e) { |
| 261 // When the blur event happens we do not know who is getting focus so we |
| 262 // delay this a bit since we want to know if the other input got focus |
| 263 // before deciding if we should exit edit mode. |
| 264 var doc = e.target.ownerDocument; |
| 265 window.setTimeout(function() { |
| 266 var activeElement = doc.activeElement; |
| 267 if (activeElement != urlInput && activeElement != labelInput) { |
| 268 listItem.editing = false; |
| 269 } |
| 270 }, 50); |
| 271 } |
| 272 |
| 273 var doc = this.ownerDocument; |
| 274 if (editing) { |
| 275 this.setAttribute('editing', ''); |
| 276 this.draggable = false; |
| 277 |
| 278 labelInput = doc.createElement('input'); |
| 279 labelInput.placeholder = |
| 280 localStrings.getString('name_input_placeholder'); |
| 281 replaceAllChildren(labelEl, labelInput); |
| 282 labelInput.value = title; |
| 283 |
| 284 if (!isFolder) { |
| 285 // To use :invalid we need to put the input inside a form |
| 286 // https://bugs.webkit.org/show_bug.cgi?id=34733 |
| 287 var form = doc.createElement('form'); |
| 288 urlInput = doc.createElement('input'); |
| 289 urlInput.type = 'url'; |
| 290 urlInput.required = true; |
| 291 urlInput.placeholder = |
| 292 localStrings.getString('url_input_placeholder'); |
| 293 |
| 294 // We also need a name for the input for the CSS to work. |
| 295 urlInput.name = '-url-input-' + cr.createUid(); |
| 296 form.appendChild(urlInput); |
| 297 replaceAllChildren(urlEl, form); |
| 298 urlInput.value = url; |
| 299 } |
| 300 |
| 301 function stopPropagation(e) { |
| 302 e.stopPropagation(); |
| 303 } |
| 304 |
| 305 var eventsToStop = ['mousedown', 'mouseup', 'contextmenu', 'dblclick']; |
| 306 eventsToStop.forEach(function(type) { |
| 307 labelInput.addEventListener(type, stopPropagation); |
| 308 }); |
| 309 labelInput.addEventListener('keydown', handleKeydown); |
| 310 labelInput.addEventListener('blur', handleBlur); |
| 311 cr.ui.limitInputWidth(labelInput, this, 200); |
| 312 labelInput.focus(); |
| 313 labelInput.select(); |
| 314 |
| 315 if (!isFolder) { |
| 316 eventsToStop.forEach(function(type) { |
| 317 urlInput.addEventListener(type, stopPropagation); |
| 318 }); |
| 319 urlInput.addEventListener('keydown', handleKeydown); |
| 320 urlInput.addEventListener('blur', handleBlur); |
| 321 cr.ui.limitInputWidth(urlInput, this, 200); |
| 322 } |
| 323 |
| 324 } else { |
| 325 |
| 326 // Check that we have a valid URL and if not we do not change the |
| 327 // editing mode. |
| 328 if (!isFolder) { |
| 329 var urlInput = this.querySelector('.url input'); |
| 330 var newUrl = urlInput.value; |
| 331 if (!urlInput.validity.valid) { |
| 332 // WebKit does not do URL fix up so we manually test if prepending |
| 333 // 'http://' would make the URL valid. |
| 334 // https://bugs.webkit.org/show_bug.cgi?id=29235 |
| 335 urlInput.value = 'http://' + newUrl; |
| 336 if (!urlInput.validity.valid) { |
| 337 // still invalid |
| 338 urlInput.value = newUrl; |
| 339 |
| 340 // In case the item was removed before getting here we should |
| 341 // not alert. |
| 342 if (listItem.parentNode) { |
| 343 alert(localStrings.getString('invalid_url')); |
| 344 } |
| 345 urlInput.focus(); |
| 346 urlInput.select(); |
| 347 return; |
| 348 } |
| 349 newUrl = 'http://' + newUrl; |
| 350 } |
| 351 urlEl.textContent = this.bookmarkNode.url = newUrl; |
| 352 } |
| 353 |
| 354 this.removeAttribute('editing'); |
| 355 this.draggable = true; |
| 356 |
| 357 labelInput = this.querySelector('.label input'); |
| 358 var newLabel = labelInput.value; |
| 359 labelEl.textContent = this.bookmarkNode.title = newLabel; |
| 360 |
| 361 if (isFolder) { |
| 362 if (newLabel != title) { |
| 363 cr.dispatchSimpleEvent(this, 'rename', true); |
| 364 } |
| 365 } else if (newLabel != title || newUrl != url) { |
| 366 cr.dispatchSimpleEvent(this, 'edit', true); |
| 367 } |
| 368 } |
| 369 } |
| 370 }; |
| 371 |
| 372 function createListItem(bookmarkNode, showFolder) { |
| 373 var li = listItemPromo.cloneNode(true); |
| 374 BookmarkListItem.decorate(li); |
| 375 updateListItem(li, bookmarkNode, showFolder); |
| 376 li.bookmarkId = bookmarkNode.id; |
| 377 li.bookmarkNode = bookmarkNode; |
| 378 li.draggable = true; |
| 379 listLookup[bookmarkNode.id] = li; |
| 380 return li; |
| 381 } |
| 382 |
| 383 function updateListItem(el, bookmarkNode, showFolder) { |
| 384 var labelEl = el.firstChild; |
| 385 labelEl.textContent = bookmarkNode.title; |
| 386 if (!bmm.isFolder(bookmarkNode)) { |
| 387 labelEl.style.backgroundImage = url('chrome://favicon/' + |
| 388 bookmarkNode.url); |
| 389 var urlEl = el.childNodes[1].firstChild; |
| 390 urlEl.textContent = urlEl.href = bookmarkNode.url; |
| 391 } else { |
| 392 el.className = 'folder'; |
| 393 } |
| 394 |
| 395 var folderEl = el.lastChild.firstChild; |
| 396 if (showFolder) { |
| 397 folderEl.style.display = ''; |
| 398 folderEl.textContent = getFolder(bookmarkNode.parentId); |
| 399 folderEl.href = '#' + bookmarkNode.parentId; |
| 400 } else { |
| 401 folderEl.style.display = 'none'; |
| 402 } |
| 403 } |
| 404 |
| 405 var listItemPromo = (function() { |
| 406 var div = cr.doc.createElement('div'); |
| 407 div.innerHTML = '<div>' + |
| 408 '<div class=label></div>' + |
| 409 '<div><span class=url></span></div>' + |
| 410 '<div><span class=folder></span></div>' + |
| 411 '</div>'; |
| 412 return div.firstChild; |
| 413 })(); |
| 414 |
| 415 return { |
| 416 createListItem: createListItem, |
| 417 BookmarkList: BookmarkList, |
| 418 listLookup: listLookup |
| 419 }; |
| 420 }); |
OLD | NEW |