Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(679)

Side by Side Diff: chrome/browser/resources/ntp/most_visited.js

Issue 8036002: ntp: remove ntp3 resources (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: . Created 9 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2011 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 // Dependencies that we should remove/formalize:
6 // util.js
7 //
8 // afterTransition
9 // chrome.send
10 // hideNotification
11 // isRtl
12 // localStrings
13 // logEvent
14 // showNotification
15
16
17 var MostVisited = (function() {
18
19 function addPinnedUrl(item, index) {
20 chrome.send('addPinnedURL', [item.url, item.title, item.faviconUrl || '',
21 item.thumbnailUrl || '', String(index)]);
22 }
23
24 function getItem(el) {
25 return findAncestorByClass(el, 'thumbnail-container');
26 }
27
28 function updatePinnedDom(el, pinned) {
29 el.querySelector('.pin').title = localStrings.getString(pinned ?
30 'unpinthumbnailtooltip' : 'pinthumbnailtooltip');
31 if (pinned) {
32 el.classList.add('pinned');
33 } else {
34 el.classList.remove('pinned');
35 }
36 }
37
38 function getThumbnailIndex(el) {
39 var nodes = el.parentNode.querySelectorAll('.thumbnail-container');
40 return Array.prototype.indexOf.call(nodes, el);
41 }
42
43 function MostVisited(el, miniview, menu, useSmallGrid, visible) {
44 this.element = el;
45 this.miniview = miniview;
46 this.menu = menu;
47 this.useSmallGrid_ = useSmallGrid;
48 this.visible_ = visible;
49
50 this.createThumbnails_();
51 this.applyMostVisitedRects_();
52
53 el.addEventListener('click', this.handleClick_.bind(this));
54 el.addEventListener('keydown', this.handleKeyDown_.bind(this));
55
56 document.addEventListener('DOMContentLoaded',
57 this.ensureSmallGridCorrect.bind(this));
58
59 // Commands
60 document.addEventListener('command', this.handleCommand_.bind(this));
61 document.addEventListener('canExecute', this.handleCanExecute_.bind(this));
62
63 // DND
64 el.addEventListener('dragstart', this.handleDragStart_.bind(this));
65 el.addEventListener('dragenter', this.handleDragEnter_.bind(this));
66 el.addEventListener('dragover', this.handleDragOver_.bind(this));
67 el.addEventListener('dragleave', this.handleDragLeave_.bind(this));
68 el.addEventListener('drop', this.handleDrop_.bind(this));
69 el.addEventListener('dragend', this.handleDragEnd_.bind(this));
70 el.addEventListener('drag', this.handleDrag_.bind(this));
71 el.addEventListener('mousedown', this.handleMouseDown_.bind(this));
72 }
73
74 MostVisited.prototype = {
75 togglePinned_: function(el) {
76 var index = getThumbnailIndex(el);
77 var item = this.data[index];
78 item.pinned = !item.pinned;
79 if (item.pinned) {
80 addPinnedUrl(item, index);
81 } else {
82 chrome.send('removePinnedURL', [item.url]);
83 }
84 updatePinnedDom(el, item.pinned);
85 },
86
87 swapPosition_: function(source, destination) {
88 var nodes = source.parentNode.querySelectorAll('.thumbnail-container');
89 var sourceIndex = getThumbnailIndex(source);
90 var destinationIndex = getThumbnailIndex(destination);
91 swapDomNodes(source, destination);
92
93 var sourceData = this.data[sourceIndex];
94 addPinnedUrl(sourceData, destinationIndex);
95 sourceData.pinned = true;
96 updatePinnedDom(source, true);
97
98 var destinationData = this.data[destinationIndex];
99 // Only update the destination if it was pinned before.
100 if (destinationData.pinned) {
101 addPinnedUrl(destinationData, sourceIndex);
102 }
103 this.data[destinationIndex] = sourceData;
104 this.data[sourceIndex] = destinationData;
105
106 chrome.send('recordAction', ['MostVisitedReordered']);
107 },
108
109 updateSettingsLink: function(hasBlacklistedUrls) {
110 if (hasBlacklistedUrls)
111 $('most-visited-settings').classList.add('has-blacklist');
112 else
113 $('most-visited-settings').classList.remove('has-blacklist');
114 },
115
116 blacklist: function(el) {
117 var self = this;
118 var url = el.href;
119 chrome.send('blacklistURLFromMostVisited', [url]);
120
121 el.classList.add('hide');
122
123 // Find the old item.
124 var oldUrls = {};
125 var oldIndex = -1;
126 var oldItem;
127 var data = this.data;
128 for (var i = 0; i < data.length; i++) {
129 if (data[i].url == url) {
130 oldItem = data[i];
131 oldIndex = i;
132 }
133 oldUrls[data[i].url] = true;
134 }
135
136 // Send 'getMostVisitedPages' with a callback since we want to find the
137 // new page and add that in the place of the removed page.
138 chromeSend('getMostVisited', [], 'setMostVisitedPages',
139 function(data, hasBlacklistedUrls) {
140 // Update settings link.
141 self.updateSettingsLink(hasBlacklistedUrls);
142
143 // Find new item.
144 var newItem;
145 for (var i = 0; i < data.length; i++) {
146 if (!(data[i].url in oldUrls)) {
147 newItem = data[i];
148 break;
149 }
150 }
151
152 if (!newItem) {
153 // If no other page is available to replace the blacklisted item,
154 // we need to reorder items s.t. all filler items are in the rightmost
155 // indices.
156 self.data = data;
157
158 // Replace old item with new item in the most visited data array.
159 } else if (oldIndex != -1) {
160 var oldData = self.data.concat();
161 oldData.splice(oldIndex, 1, newItem);
162 self.data = oldData;
163 el.classList.add('fade-in');
164 }
165
166 // We wrap the title in a <span class=blacklisted-title>. We pass an
167 // empty string to the notifier function and use DOM to insert the real
168 // string.
169 var actionText = localStrings.getString('undothumbnailremove');
170
171 // Show notification and add undo callback function.
172 var wasPinned = oldItem.pinned;
173 showNotification('', actionText, function() {
174 self.removeFromBlackList(url);
175 if (wasPinned) {
176 addPinnedUrl(oldItem, oldIndex);
177 }
178 chrome.send('getMostVisited');
179 });
180
181 // Now change the DOM.
182 var removeText = localStrings.getString('thumbnailremovednotification');
183 var notifyMessageEl = document.querySelector('#notification > *');
184 notifyMessageEl.textContent = removeText;
185
186 // Focus the undo link.
187 var undoLink = document.querySelector(
188 '#notification > .link > [tabindex]');
189 undoLink.focus();
190 });
191 },
192
193 removeFromBlackList: function(url) {
194 chrome.send('removeURLsFromMostVisitedBlacklist', [url]);
195 },
196
197 clearAllBlacklisted: function() {
198 chrome.send('clearMostVisitedURLsBlacklist', []);
199 hideNotification();
200 },
201
202 dirty_: false,
203 invalidate_: function() {
204 this.dirty_ = true;
205 },
206
207 visible_: true,
208 get visible() {
209 return this.visible_;
210 },
211 set visible(visible) {
212 if (this.visible_ != visible) {
213 this.visible_ = visible;
214 this.invalidate_();
215 }
216 },
217
218 useSmallGrid_: false,
219 get useSmallGrid() {
220 return this.useSmallGrid_;
221 },
222 set useSmallGrid(b) {
223 if (this.useSmallGrid_ != b) {
224 this.useSmallGrid_ = b;
225 this.invalidate_();
226 }
227 },
228
229 layout: function() {
230 if (!this.dirty_)
231 return;
232 var d0 = Date.now();
233 this.applyMostVisitedRects_();
234 this.dirty_ = false;
235 logEvent('mostVisited.layout: ' + (Date.now() - d0));
236 },
237
238 createThumbnails_: function() {
239 var singleHtml =
240 '<a class="thumbnail-container filler" tabindex="1">' +
241 '<div class="edit-mode-border">' +
242 '<div class="edit-bar">' +
243 '<div class="pin"></div>' +
244 '<div class="spacer"></div>' +
245 '<div class="remove"></div>' +
246 '</div>' +
247 '<span class="thumbnail-wrapper">' +
248 '<span class="thumbnail"></span>' +
249 '</span>' +
250 '</div>' +
251 '<div class="title">' +
252 '<div></div>' +
253 '</div>' +
254 '</a>';
255 this.element.innerHTML = Array(8 + 1).join(singleHtml);
256 var children = this.element.children;
257 for (var i = 0; i < 8; i++) {
258 children[i].id = 't' + i;
259 children[i].onmouseover = this.handleMouseOver_.bind(this);
260 children[i].onmouseout = this.handleMouseOut_.bind(this);
261 }
262 },
263
264 getMostVisitedLayoutRects_: function() {
265 var small = this.useSmallGrid;
266
267 var cols = 4;
268 var rows = 2;
269 var marginWidth = 10;
270 var marginHeight = 7;
271 var borderWidth = 4;
272 var thumbWidth = small ? 150 : 207;
273 var thumbHeight = small ? 93 : 129;
274 var w = thumbWidth + 2 * borderWidth + 2 * marginWidth;
275 var h = thumbHeight + 40 + 2 * marginHeight;
276 var sumWidth = cols * w - 2 * marginWidth;
277 var topSpacing = 10;
278
279 var rtl = isRtl();
280 var rects = [];
281
282 if (this.visible) {
283 for (var i = 0; i < rows * cols; i++) {
284 var row = Math.floor(i / cols);
285 var col = i % cols;
286 var left = rtl ? sumWidth - col * w - thumbWidth - 2 * borderWidth :
287 col * w;
288
289 var top = row * h + topSpacing;
290
291 rects[i] = {left: left, top: top};
292 }
293 }
294 return rects;
295 },
296
297 applyMostVisitedRects_: function() {
298 if (this.visible) {
299 var rects = this.getMostVisitedLayoutRects_();
300 var children = this.element.children;
301 for (var i = 0; i < 8; i++) {
302 var t = children[i];
303 t.style.left = rects[i].left + 'px';
304 t.style.top = rects[i].top + 'px';
305 t.style.right = '';
306 var innerStyle = t.firstElementChild.style;
307 innerStyle.left = innerStyle.top = '';
308 }
309 }
310 },
311
312 // Work around for http://crbug.com/25329
313 ensureSmallGridCorrect: function(expected) {
314 if (expected != this.useSmallGrid)
315 this.applyMostVisitedRects_();
316 },
317
318 getRectByIndex_: function(index) {
319 return this.getMostVisitedLayoutRects_()[index];
320 },
321
322 // Commands
323
324 handleCommand_: function(e) {
325 var commandId = e.command.id;
326 switch (commandId) {
327 case 'clear-all-blacklisted':
328 this.clearAllBlacklisted();
329 chrome.send('getMostVisited');
330 break;
331 }
332 },
333
334 handleCanExecute_: function(e) {
335 if (e.command.id == 'clear-all-blacklisted')
336 e.canExecute = true;
337 },
338
339 // DND
340
341 currentOverItem_: null,
342 get currentOverItem() {
343 return this.currentOverItem_;
344 },
345 set currentOverItem(item) {
346 var style;
347 if (item != this.currentOverItem_) {
348 if (this.currentOverItem_) {
349 style = this.currentOverItem_.firstElementChild.style;
350 style.left = style.top = '';
351 }
352 this.currentOverItem_ = item;
353
354 if (item) {
355 // Make the drag over item move 15px towards the source. The movement
356 // is done by only moving the edit-mode-border (as in the mocks) and
357 // it is done with relative positioning so that the movement does not
358 // change the drop target.
359 var dragIndex = getThumbnailIndex(this.dragItem_);
360 var overIndex = getThumbnailIndex(item);
361 if (dragIndex == -1 || overIndex == -1) {
362 return;
363 }
364
365 var dragRect = this.getRectByIndex_(dragIndex);
366 var overRect = this.getRectByIndex_(overIndex);
367
368 var x = dragRect.left - overRect.left;
369 var y = dragRect.top - overRect.top;
370 var z = Math.sqrt(x * x + y * y);
371 var z2 = 15;
372 var x2 = x * z2 / z;
373 var y2 = y * z2 / z;
374
375 style = this.currentOverItem_.firstElementChild.style;
376 style.left = x2 + 'px';
377 style.top = y2 + 'px';
378 }
379 }
380 },
381 dragItem_: null,
382 startX_: 0,
383 startY_: 0,
384 startScreenX_: 0,
385 startScreenY_: 0,
386 dragEndTimer_: null,
387 hoverStartTime_: null,
388
389 isDragging: function() {
390 return !!this.dragItem_;
391 },
392
393 handleDragStart_: function(e) {
394 // For the purpose of recording histograms, treat this as the end of
395 // hovering over the thumbnail.
396 this.RecordHoverTime_(false);
397
398 var thumbnail = getItem(e.target);
399 if (thumbnail) {
400 // Don't set data since HTML5 does not allow setting the name for
401 // url-list. Instead, we just rely on the dragging of link behavior.
402 this.dragItem_ = thumbnail;
403 this.dragItem_.classList.add('dragging');
404 this.dragItem_.style.zIndex = 2;
405 e.dataTransfer.effectAllowed = 'copyLinkMove';
406 }
407 },
408
409 handleDragEnter_: function(e) {
410 if (this.canDropOnElement_(this.currentOverItem)) {
411 e.preventDefault();
412 }
413 },
414
415 handleDragOver_: function(e) {
416 var item = getItem(e.target);
417 this.currentOverItem = item;
418 if (this.canDropOnElement_(item)) {
419 e.preventDefault();
420 e.dataTransfer.dropEffect = 'move';
421 }
422 },
423
424 handleDragLeave_: function(e) {
425 var item = getItem(e.target);
426 if (item) {
427 e.preventDefault();
428 }
429
430 this.currentOverItem = null;
431 },
432
433 handleDrop_: function(e) {
434 var dropTarget = getItem(e.target);
435 if (this.canDropOnElement_(dropTarget)) {
436 dropTarget.style.zIndex = 1;
437 this.swapPosition_(this.dragItem_, dropTarget);
438 // The timeout below is to allow WebKit to see that we turned off
439 // pointer-event before moving the thumbnails so that we can get out of
440 // hover mode.
441 window.setTimeout((function() {
442 this.invalidate_();
443 this.layout();
444 }).bind(this), 10);
445 e.preventDefault();
446 if (this.dragEndTimer_) {
447 window.clearTimeout(this.dragEndTimer_);
448 this.dragEndTimer_ = null;
449 }
450 afterTransition(function() {
451 dropTarget.style.zIndex = '';
452 });
453 }
454 },
455
456 handleDragEnd_: function(e) {
457 var dragItem = this.dragItem_;
458 if (dragItem) {
459 dragItem.style.pointerEvents = '';
460 dragItem.classList.remove('dragging');
461
462 afterTransition(function() {
463 // Delay resetting zIndex to let the animation finish.
464 dragItem.style.zIndex = '';
465 // Same for overflow.
466 dragItem.parentNode.style.overflow = '';
467 });
468
469 this.invalidate_();
470 this.layout();
471 this.dragItem_ = null;
472 }
473 },
474
475 handleDrag_: function(e) {
476 // Moves the drag item making sure that it is not displayed outside the
477 // browser viewport.
478 var item = getItem(e.target);
479 var rect = this.element.getBoundingClientRect();
480 item.style.pointerEvents = 'none';
481
482 var x = this.startX_ + e.screenX - this.startScreenX_;
483 var y = this.startY_ + e.screenY - this.startScreenY_;
484
485 // The position of the item is relative to #most-visited so we need to
486 // subtract that when calculating the allowed position.
487 x = Math.max(x, -rect.left);
488 x = Math.min(x, document.body.clientWidth - rect.left - item.offsetWidth -
489 2);
490 // The shadow is 2px
491 y = Math.max(-rect.top, y);
492 y = Math.min(y, document.body.clientHeight - rect.top -
493 item.offsetHeight - 2);
494
495 // Override right in case of RTL.
496 item.style.right = 'auto';
497 item.style.left = x + 'px';
498 item.style.top = y + 'px';
499 item.style.zIndex = 2;
500 },
501
502 // We listen to mousedown to get the relative position of the cursor for
503 // dnd.
504 handleMouseDown_: function(e) {
505 var item = getItem(e.target);
506 if (item) {
507 this.startX_ = item.offsetLeft;
508 this.startY_ = item.offsetTop;
509 this.startScreenX_ = e.screenX;
510 this.startScreenY_ = e.screenY;
511
512 // We don't want to focus the item on mousedown. However, to prevent
513 // focus one has to call preventDefault but this also prevents the drag
514 // and drop (sigh) so we only prevent it when the user is not doing a
515 // left mouse button drag.
516 if (e.button != 0) // LEFT
517 e.preventDefault();
518 }
519 },
520
521 canDropOnElement_: function(el) {
522 return this.dragItem_ && el &&
523 el.classList.contains('thumbnail-container') &&
524 !el.classList.contains('filler');
525 },
526
527 // Thumbnail hovering
528
529 // TODO(mmenke): Either implement preconnect/prerendering based on
530 // hovering, or remove this code.
531
532 /**
533 * Record the time the mouse has been hovering over a thumbnail.
534 * |clicked| must be true if the thumbnail was clicked, or false if
535 * the cursor was moved off of the thumbnail.
536 */
537 RecordHoverTime_: function(clicked) {
538 if (!this.hoverStartTime_)
539 return;
540 var hoverDuration = (new Date()).getTime() - this.hoverStartTime_;
541 if (hoverDuration > 500)
542 hoverDuration = 500;
543 chrome.send('recordInHistogram',
544 [clicked ? 'NewTabPage.HoverTimeClicked'
545 : 'NewTabPage.HoverTimeNotClicked',
546 hoverDuration,
547 500]);
548 this.hoverStartTime_ = null;
549 },
550
551 /**
552 * Record the time the cursor started hovering over a thumbnail.
553 * Do nothing if currently dragging the thumbnail.
554 */
555 handleMouseOver_: function() {
556 if (!this.isDragging())
557 this.hoverStartTime_ = (new Date()).getTime();
558 },
559
560 /**
561 * Record the time the cursor spend hovering over the thumbnail.
562 */
563 handleMouseOut_: function() {
564 this.RecordHoverTime_(false);
565 },
566
567
568 /// data
569
570 data_: null,
571 get data() {
572 return this.data_;
573 },
574 set data(data) {
575 // We append the class name with the "filler" so that we can style fillers
576 // differently.
577 var maxItems = 8;
578 data.length = Math.min(maxItems, data.length);
579 var len = data.length;
580 for (var i = len; i < maxItems; i++) {
581 data[i] = {filler: true};
582 }
583
584 // On setting we need to update the items
585 this.data_ = data;
586 this.updateMostVisited_();
587 this.updateMiniview_();
588 this.updateMenu_();
589 },
590
591 updateMostVisited_: function() {
592
593 function getThumbnailClassName(item) {
594 return 'thumbnail-container' +
595 (item.pinned ? ' pinned' : '') +
596 (item.filler ? ' filler' : '');
597 }
598
599 var data = this.data;
600 var children = this.element.children;
601 for (var i = 0; i < data.length; i++) {
602 var d = data[i];
603 var t = children[i];
604
605 // If we have a filler continue
606 var oldClassName = t.className;
607 var newClassName = getThumbnailClassName(d);
608 if (oldClassName != newClassName) {
609 t.className = newClassName;
610 }
611
612 // No need to continue if this is a filler.
613 if (newClassName == 'thumbnail-container filler') {
614 // Make sure the user cannot tab to the filler.
615 t.tabIndex = -1;
616 t.querySelector('.thumbnail-wrapper').style.backgroundImage = '';
617 continue;
618 }
619 // Allow focus.
620 t.tabIndex = 1;
621
622 t.href = d.url;
623 t.setAttribute('ping',
624 getAppPingUrl('PING_BY_URL', d.url, 'NTP_MOST_VISITED'));
625 t.querySelector('.pin').title = localStrings.getString(d.pinned ?
626 'unpinthumbnailtooltip' : 'pinthumbnailtooltip');
627 t.querySelector('.remove').title =
628 localStrings.getString('removethumbnailtooltip');
629
630 // There was some concern that a malformed malicious URL could cause an
631 // XSS attack but setting style.backgroundImage = 'url(javascript:...)'
632 // does not execute the JavaScript in WebKit.
633
634 var thumbnailUrl = d.thumbnailUrl || 'chrome://thumb/' + d.url;
635 t.querySelector('.thumbnail-wrapper').style.backgroundImage =
636 url(thumbnailUrl);
637 var titleDiv = t.querySelector('.title > div');
638 titleDiv.xtitle = titleDiv.textContent = d.title;
639 var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url;
640 titleDiv.style.backgroundImage = url(faviconUrl);
641 titleDiv.dir = d.direction;
642 }
643 },
644
645 updateMiniview_: function() {
646 this.miniview.textContent = '';
647 var data = this.data.slice(0, MAX_MINIVIEW_ITEMS);
648 for (var i = 0, item; item = data[i]; i++) {
649 if (item.filler) {
650 continue;
651 }
652
653 var span = document.createElement('span');
654 var a = span.appendChild(document.createElement('a'));
655 a.href = item.url;
656 a.setAttribute('ping',
657 getAppPingUrl('PING_BY_URL', item.url, 'NTP_MOST_VISITED'));
658 a.textContent = item.title;
659 a.style.backgroundImage = url('chrome://favicon/' + item.url);
660 a.className = 'item';
661 this.miniview.appendChild(span);
662 }
663 updateMiniviewClipping(this.miniview);
664 },
665
666 updateMenu_: function() {
667 clearClosedMenu(this.menu);
668 var data = this.data.slice(0, MAX_MINIVIEW_ITEMS);
669 for (var i = 0, item; item = data[i]; i++) {
670 if (!item.filler) {
671 addClosedMenuEntry(
672 this.menu, item.url, item.title, 'chrome://favicon/' + item.url,
673 getAppPingUrl('PING_BY_URL', item.url, 'NTP_MOST_VISITED'));
674 }
675 }
676 addClosedMenuFooter(
677 this.menu, 'most-visited', MENU_THUMB, Section.THUMB);
678 },
679
680 handleClick_: function(e) {
681 var target = e.target;
682 if (target.classList.contains('pin')) {
683 this.togglePinned_(getItem(target));
684 e.preventDefault();
685 } else if (target.classList.contains('remove')) {
686 this.blacklist(getItem(target));
687 e.preventDefault();
688 } else {
689 var item = getItem(target);
690 if (item) {
691 var index = Array.prototype.indexOf.call(item.parentNode.children,
692 item);
693 this.RecordHoverTime_(true);
694 if (index != -1)
695 chrome.send('recordInHistogram',
696 ['NewTabPage.MostVisited', index, 8]);
697 }
698 }
699 },
700
701 /**
702 * Allow blacklisting most visited site using the keyboard.
703 */
704 handleKeyDown_: function(e) {
705 if (!cr.isMac && e.keyCode == 46 || // Del
706 cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
707 this.blacklist(e.target);
708 }
709 }
710 };
711
712 return MostVisited;
713 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698