pastebin

Paste Search Dynamic
Recent pastes
/single.js
  1. /* Copyright 2015 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. // Single iframe for NTP tiles.
  6.  
  7. /**
  8.  * Controls rendering the Most Visited iframe.
  9.  * @return {Object} A limited interface for testing the iframe.
  10.  */
  11. function MostVisited() {
  12. 'use strict';
  13.  
  14. /**
  15.  * Enum for key codes.
  16.  * @enum {number}
  17.  * @const
  18.  */
  19. const KEYCODES = {
  20.   BACKSPACE: 8,
  21.   delete: 46,
  22.   DOWN: 40,
  23.   ENTER: 13,
  24.   ESC: 27,
  25.   LEFT: 37,
  26.   RIGHT: 39,
  27.   SPACE: 32,
  28.   TAB: 9,
  29.   UP: 38,
  30. };
  31.  
  32. /**
  33.  * Enum for ids.
  34.  * @enum {string}
  35.  * @const
  36.  */
  37. const IDS = {
  38.   MOST_VISITED: 'most-visited',  // Container for all tilesets.
  39.   MV_TILES: 'mv-tiles',          // Most Visited tiles container.
  40. };
  41.  
  42. /**
  43.  * Enum for classnames.
  44.  * @enum {string}
  45.  * @const
  46.  */
  47. const CLASSES = {
  48.   FAILED_FAVICON: 'failed-favicon',  // Applied when the favicon fails to load.
  49.   GRID_TILE: 'grid-tile',
  50.   GRID_TILE_CONTAINER: 'grid-tile-container',
  51.   REORDER: 'reorder',  // Applied to the tile being moved while reordering.
  52.   // Applied while we are reordering. Disables hover styling.
  53.   REORDERING: 'reordering',
  54.   MAC_CHROMEOS: 'mac-chromeos',  // Reduces font weight for MacOS and ChromeOS.
  55.   // Material Design classes.
  56.   MD_FALLBACK_LETTER: 'md-fallback-letter',
  57.   MD_ICON: 'md-icon',
  58.   MD_ADD_ICON: 'md-add-icon',
  59.   MD_MENU: 'md-menu',
  60.   MD_EDIT_MENU: 'md-edit-menu',
  61.   MD_TILE: 'md-tile',
  62.   MD_TILE_INNER: 'md-tile-inner',
  63.   MD_TITLE: 'md-title',
  64. };
  65.  
  66. /**
  67.  * The different types of events that are logged from the NTP.  This enum is
  68.  * used to transfer information from the NTP JavaScript to the renderer and is
  69.  * not used as a UMA enum histogram's logged value.
  70.  * Note: Keep in sync with common/ntp_logging_events.h
  71.  * @enum {number}
  72.  * @const
  73.  */
  74. const LOG_TYPE = {
  75.   // All NTP tiles have finished loading (successfully or failing).
  76.   NTP_ALL_TILES_LOADED: 11,
  77.   // The 'Add shortcut' link was clicked.
  78.   NTP_CUSTOMIZE_ADD_SHORTCUT_CLICKED: 44,
  79.   // The 'Edit shortcut' link was clicked.
  80.   NTP_CUSTOMIZE_EDIT_SHORTCUT_CLICKED: 45,
  81. };
  82.  
  83. /**
  84.  * The different (visual) types that an NTP tile can have.
  85.  * Note: Keep in sync with components/ntp_tiles/tile_visual_type.h
  86.  * @enum {number}
  87.  * @const
  88.  */
  89. const TileVisualType = {
  90.   NONE: 0,
  91.   ICON_REAL: 1,
  92.   ICON_COLOR: 2,
  93.   ICON_DEFAULT: 3,
  94. };
  95.  
  96. /**
  97.  * Timeout delay for the window.onresize event throttle. Set to 15 frame per
  98.  * second.
  99.  * @const {number}
  100.  */
  101. const RESIZE_TIMEOUT_DELAY = 66;
  102.  
  103. /**
  104.  * Maximum number of tiles if custom links is enabled.
  105.  * @const {number}
  106.  */
  107. const MD_MAX_NUM_CUSTOM_LINK_TILES = 10;
  108.  
  109. /**
  110.  * Maximum number of tiles if Most Visited is enabled.
  111.  * @const {number}
  112.  */
  113. const MD_MAX_NUM_MOST_VISITED_TILES = 8;
  114.  
  115. /**
  116.  * Maximum number of tiles per row for Material Design.
  117.  * @const {number}
  118.  */
  119. const MD_MAX_TILES_PER_ROW = 5;
  120.  
  121. /**
  122.  * Height of a tile for Material Design. Keep in sync with
  123.  * most_visited_single.css.
  124.  * @const {number}
  125.  */
  126. const MD_TILE_HEIGHT = 128;
  127.  
  128. /**
  129.  * Width of a tile for Material Design. Keep in sync with
  130.  * most_visited_single.css.
  131.  * @const {number}
  132.  */
  133. const MD_TILE_WIDTH = 112;
  134.  
  135. /**
  136.  * Number of tiles that will always be visible for Material Design. Calculated
  137.  * by dividing minimum |--content-width| (see local_ntp.css) by |MD_TILE_WIDTH|
  138.  * and multiplying by 2 rows.
  139.  * @const {number}
  140.  */
  141. const MD_NUM_TILES_ALWAYS_VISIBLE = 6;
  142.  
  143. /**
  144.  * The origin of this request, i.e. 'chrome-search://local-ntp' for the local
  145.  * NTP.
  146.  * @const {string}
  147.  */
  148. const DOMAIN_ORIGIN = 'chrome-search://most-visited';
  149.  
  150. /**
  151.  * Counter for DOM elements that we are waiting to finish loading. Starts out
  152.  * at 1 because initially we're waiting for the "show" message from the parent.
  153.  * @type {number}
  154.  */
  155. let loadedCounter = 1;
  156.  
  157. /**
  158.  * DOM element containing the tiles we are going to present next.
  159.  * Works as a double-buffer that is shown when we receive a "show" postMessage.
  160.  * @type {Element}
  161.  */
  162. let tiles = null;
  163.  
  164. /**
  165.  * List of parameters passed by query args.
  166.  * @type {Object}
  167.  */
  168. let queryArgs = {};
  169.  
  170. /**
  171.  * True if the custom links feature is enabled, i.e. when this is a Google NTP.
  172.  * Set when the iframe is initialized.
  173.  * @type {boolean}
  174.  */
  175. let customLinksFeatureEnabled = false;
  176.  
  177. /**
  178.  * The current grid of tiles.
  179.  * @type {?Grid}
  180.  */
  181. let currGrid = null;
  182.  
  183. /**
  184.  * Additional API for Array. Moves the item at index |from| to index |to|.
  185.  * @param {number} from Index of the item to move.
  186.  * @param {number} to Index to move the item to.
  187.  */
  188. Array.prototype.move = function(from, to) {
  189.   this.splice(to, 0, this.splice(from, 1)[0]);
  190. };
  191.  
  192. /**
  193.  * Class that handles layouts and animations for the tile grid. This includes
  194.  * animations for adding, deleting, and reordering.
  195.  */
  196. class Grid {
  197.   constructor() {
  198.     /** @private {number} */
  199.     this.tileHeight_ = 0;
  200.     /** @private {number} */
  201.     this.tileWidth_ = 0;
  202.     /** @private {number} */
  203.     this.tilesAlwaysVisible_ = 0;
  204.     /**
  205.      * The maximum number of tiles per row allowed by the grid parameters.
  206.      * @private {number}
  207.      */
  208.     this.maxTilesPerRow_ = 0;
  209.     /** @private {number} */
  210.     this.maxTiles_ = 0;
  211.  
  212.     /** @private {number} */
  213.     this.gridWidth_ = 0;
  214.     /**
  215.      * The maximum number of tiles per row allowed by the window width.
  216.      * @private {number}
  217.      */
  218.     this.maxTilesPerRowWindow_ = 0;
  219.  
  220.     /** @private {?Element} */
  221.     this.container_ = null;
  222.     /** @private {?HTMLCollection} */
  223.     this.tiles_ = null;
  224.  
  225.     /**
  226.      * Array that stores the {x,y} positions of the tile layout.
  227.      * @private {?Array<!Object>}
  228.      */
  229.     this.position_ = null;
  230.  
  231.     /**
  232.      * Stores the current order of the tiles. Index corresponds to grid
  233.      * position, while value is the index of the tile in |this.tiles_|.
  234.      * @private {?Array<number>}
  235.      */
  236.     this.order_ = null;
  237.  
  238.     /** @private {number} The index of the tile we're reordering. */
  239.     this.itemToReorder_ = -1;
  240.     /** @private {number} The index to move the tile we're reordering to. */
  241.     this.newIndexOfItemToReorder_ = -1;
  242.  
  243.     /** @private {boolean} True if the user is currently touching a tile. */
  244.     this.touchStarted_ = false;
  245.   }
  246.  
  247.   /**
  248.    * Sets up the grid for the new tileset in |container|. The old tileset is
  249.    * discarded.
  250.    * @param {!Element} container The grid container element.
  251.    * @param {Object=} params Customizable parameters for the grid. Used in
  252.    *     testing.
  253.    */
  254.   init(container, params = {}) {
  255.     this.container_ = container;
  256.  
  257.     this.tileHeight_ = params.tileHeight || MD_TILE_HEIGHT;
  258.     this.tileWidth_ = params.tileWidth || MD_TILE_WIDTH;
  259.     this.tilesAlwaysVisible_ =
  260.         params.tilesAlwaysVisible || MD_NUM_TILES_ALWAYS_VISIBLE;
  261.     this.maxTilesPerRow_ = params.maxTilesPerRow || MD_MAX_TILES_PER_ROW;
  262.     this.maxTiles_ = params.maxTiles || getMaxNumTiles();
  263.  
  264.     this.maxTilesPerRowWindow_ = this.getMaxTilesPerRow_();
  265.  
  266.     this.tiles_ =
  267.         this.container_.getElementsByClassName(CLASSES.GRID_TILE_CONTAINER);
  268.     // Ignore any tiles past the maximum allowed.
  269.     this.position_ = new Array(this.maxTiles_);
  270.     this.order_ = new Array(this.maxTiles_);
  271.     for (let i = 0; i < this.maxTiles_; i++) {
  272.       this.position_[i] = {x: 0, y: 0};
  273.       this.order_[i] = i;
  274.     }
  275.  
  276.     if (isCustomLinksEnabled() || params.enableReorder) {
  277.       // Set up reordering for all tiles except the add shortcut button.
  278.       for (let i = 0; i < this.tiles_.length; i++) {
  279.         if (this.tiles_[i].getAttribute('add') !== 'true') {
  280.           this.setupReorder_(this.tiles_[i], i);
  281.         }
  282.       }
  283.     }
  284.  
  285.     this.updateLayout();
  286.   }
  287.  
  288.   /**
  289.    * Returns a grid tile wrapper that contains |tile|.
  290.    * @param {!Element} tile The tile element.
  291.    * @param {number} rid The tile's restricted id.
  292.    * @param {boolean} isAddButton True if this is the add shortcut button.
  293.    * @return {!Element} A grid tile wrapper.
  294.    */
  295.   createGridTile(tile, rid, isAddButton) {
  296.     const gridTileContainer = document.createElement('div');
  297.     gridTileContainer.className = CLASSES.GRID_TILE_CONTAINER;
  298.     gridTileContainer.setAttribute('rid', rid);
  299.     gridTileContainer.setAttribute('add', isAddButton);
  300.     const gridTile = document.createElement('div');
  301.     gridTile.className = CLASSES.GRID_TILE;
  302.     gridTile.appendChild(tile);
  303.     gridTileContainer.appendChild(gridTile);
  304.     return gridTileContainer;
  305.   }
  306.  
  307.   /**
  308.    * Updates the layout of the tiles. This is called for new tilesets and when
  309.    * the window is resized or zoomed. Translates each tile's
  310.    * |CLASSES.GRID_TILE_CONTAINER| to the correct position.
  311.    */
  312.   updateLayout() {
  313.     const tilesPerRow = this.getTilesPerRow_();
  314.  
  315.     this.gridWidth_ = tilesPerRow * this.tileWidth_;
  316.     this.container_.style.width = this.gridWidth_ + 'px';
  317.  
  318.     const maxVisibleTiles = tilesPerRow * 2;
  319.     let x = 0;
  320.     let y = 0;
  321.     for (let i = 0; i < this.tiles_.length; i++) {
  322.       const tile = this.tiles_[i];
  323.       // Reset the offset for row 2.
  324.       if (i === tilesPerRow) {
  325.         x = this.getRow2Offset_(tilesPerRow);
  326.         y = this.tileHeight_;
  327.       }
  328.       // Update the tile's position.
  329.       this.translate_(tile, x, y);
  330.       this.position_[i].x = x;
  331.       this.position_[i].y = y;
  332.       x += this.tileWidth_;  // Increment for the next tile.
  333.  
  334.       // Update visibility for tiles that may be hidden by the iframe border in
  335.       // order to prevent keyboard navigation from reaching them. Ignores tiles
  336.       // that will always be visible, since changing 'display' prevents
  337.       // transitions from working.
  338.       if (i >= this.tilesAlwaysVisible_) {
  339.         const isVisible = i < maxVisibleTiles;
  340.         tile.style.display = isVisible ? 'block' : 'none';
  341.       }
  342.     }
  343.   }
  344.  
  345.   /**
  346.    * Called when the window is resized/zoomed. Recalculates maximums for the new
  347.    * window size and calls |updateLayout| if necessary.
  348.    */
  349.   onresize() {
  350.     // Update the layout if the max number of tiles per row changes due to the
  351.     // new window size.
  352.     const maxPerRowWindow = this.getMaxTilesPerRow_();
  353.     if (maxPerRowWindow !== this.maxTilesPerRowWindow_) {
  354.       this.maxTilesPerRowWindow_ = maxPerRowWindow;
  355.       this.updateLayout();
  356.     }
  357.   }
  358.  
  359.   /**
  360.    * Returns the number of tiles per row. This may be balanced in order to make
  361.    * even rows.
  362.    * @return {number} The number of tiles per row.
  363.    * @private
  364.    */
  365.   getTilesPerRow_() {
  366.     const maxTilesPerRow =
  367.         Math.min(this.maxTilesPerRow_, this.maxTilesPerRowWindow_);
  368.     if (this.tiles_.length >= maxTilesPerRow * 2) {
  369.       // We have enough for two full rows, so just return the max.
  370.       return maxTilesPerRow;
  371.     } else if (this.tiles_.length > maxTilesPerRow) {
  372.       // We have have a little more than one full row, so we need to rebalance.
  373.       return Math.ceil(this.tiles_.length / 2);
  374.     } else {
  375.       // We have (less than) a full row, so just return the tiles we have.
  376.       return this.tiles_.length;
  377.     }
  378.   }
  379.  
  380.   /**
  381.    * Returns the maximum number of tiles per row allowed by the window size.
  382.    * @return {number} The maximum number of tiles per row.
  383.    * @private
  384.    */
  385.   getMaxTilesPerRow_() {
  386.     return Math.floor(window.innerWidth / this.tileWidth_);
  387.   }
  388.  
  389.   /**
  390.    * Returns row 2's x offset from row 1 in px. This will either be 0 or half a
  391.    * tile length.
  392.    * @param {number} tilesPerRow The number of tiles per row.
  393.    * @return {number} The offset for row 2.
  394.    * @private
  395.    */
  396.   getRow2Offset_(tilesPerRow) {
  397.     // An odd number of tiles requires a half tile offset in the second row,
  398.     // unless both rows are full (i.e. for smaller window widths).
  399.     if (this.tiles_.length % 2 === 1 && this.tiles_.length / tilesPerRow < 2) {
  400.       return Math.round(this.tileWidth_ / 2);
  401.     }
  402.     return 0;
  403.   }
  404.  
  405.   /**
  406.    * Returns true if the browser is in RTL.
  407.    * @return {boolean}
  408.    * @private
  409.    */
  410.   isRtl_() {
  411.     return document.documentElement.dir === 'rtl';
  412.   }
  413.  
  414.   /**
  415.    * Translates the |element| by (x, y).
  416.    * @param {?Element} element The element to apply the transform to.
  417.    * @param {number} x The x value.
  418.    * @param {number} y The y value.
  419.    * @private
  420.    */
  421.   translate_(element, x, y) {
  422.     if (!element) {
  423.       throw new Error('Invalid element: cannot apply transform');
  424.     }
  425.     const rtlX = x * (this.isRtl_() ? -1 : 1);
  426.     element.style.transform = 'translate(' + rtlX + 'px, ' + y + 'px)';
  427.   }
  428.  
  429.   /**
  430.    * Sets up event listeners necessary for tile reordering.
  431.    * @param {!Element} tile Tile on which to set the event listeners.
  432.    * @param {number} index The tile's index.
  433.    * @private
  434.    */
  435.   setupReorder_(tile, index) {
  436.     tile.setAttribute('index', index);
  437.  
  438.     // Set up mouse support.
  439.     // Listen for the drag event on the tile instead of the tile container. The
  440.     // tile container remains static during the reorder flow.
  441.     tile.firstChild.draggable = true;
  442.     // Prevent default drag events on the shortcut link.
  443.     const tileItem = tile.firstChild.firstChild;
  444.     tileItem.draggable = false;
  445.     tile.firstChild.addEventListener('dragstart', (event) => {
  446.       // Support link dragging (i.e. dragging the URL to the omnibox).
  447.       event.dataTransfer.setData('text/uri-list', tileItem.href);
  448.       // Remove the ghost image that appears when dragging.
  449.       const emptyImg = new Image();
  450.       event.dataTransfer.setDragImage(emptyImg, 0, 0);
  451.  
  452.       this.startReorder_(tile, event, /*mouseMode=*/ true);
  453.     });
  454.     // Show a 'move' cursor while dragging the tile within the grid bounds. This
  455.     // is mostly intended for Windows, which will otherwise show a 'prohibited'
  456.     // cursor.
  457.     tile.addEventListener('dragover', (event) => {
  458.       event.preventDefault();
  459.       event.dataTransfer.dropEffect = 'move';
  460.     });
  461.  
  462.     // Set up touch support.
  463.     tile.firstChild.addEventListener('touchstart', (startEvent) => {
  464.       // Ignore subsequent touchstart events, which can be triggered if a
  465.       // different finger is placed on this tile.
  466.       if (this.touchStarted_) {
  467.         return;
  468.       }
  469.       this.touchStarted_ = true;
  470.  
  471.       // Start the reorder flow once the user moves their finger.
  472.       const startReorder = (moveEvent) => {
  473.         // Use the cursor position from 'touchstart' as the starting location.
  474.         this.startReorder_(tile, startEvent, /*mouseMode=*/ false);
  475.       };
  476.       // Insert the held tile at the index we are hovering over.
  477.       const moveOver = (moveEvent) => {
  478.         // Touch events do not have a 'mouseover' equivalent, so we need to
  479.         // manually check if we are hovering over a tile. If so, insert the held
  480.         // tile there.
  481.         // Note: The first item in |changedTouches| is the current position.
  482.         const x = moveEvent.changedTouches[0].pageX;
  483.         const y = moveEvent.changedTouches[0].pageY;
  484.         this.reorderToIndexAtPoint_(x, y);
  485.       };
  486.       // Allow 'touchstart' events again when reordering stops/was never
  487.       // started.
  488.       const touchEnd = (endEvent) => {
  489.         tile.firstChild.removeEventListener('touchmove', startReorder);
  490.         tile.firstChild.removeEventListener('touchmove', moveOver);
  491.         tile.firstChild.removeEventListener('touchend', touchEnd);
  492.         tile.firstChild.removeEventListener('touchcancel', touchEnd);
  493.         this.touchStarted_ = false;
  494.       };
  495.  
  496.       tile.firstChild.addEventListener('touchmove', startReorder, {once: true});
  497.       tile.firstChild.addEventListener('touchmove', moveOver);
  498.       tile.firstChild.addEventListener('touchend', touchEnd, {once: true});
  499.       tile.firstChild.addEventListener('touchcancel', touchEnd, {once: true});
  500.     });
  501.   }
  502.  
  503.   /**
  504.    * Starts the reorder flow. Updates the visual style of the held tile to
  505.    * indicate that it is being moved and sets up the relevant event listeners.
  506.    * @param {!Element} tile Tile that is being moved.
  507.    * @param {!Event} event The 'dragstart'/'touchmove' event. Used to obtain the
  508.    *     current cursor position
  509.    * @param {boolean} mouseMode True if the user is using a mouse.
  510.    * @private
  511.    */
  512.   startReorder_(tile, event, mouseMode) {
  513.     const index = Number(tile.getAttribute('index'));
  514.  
  515.     this.itemToReorder_ = index;
  516.     this.newIndexOfItemToReorder_ = index;
  517.  
  518.     // Apply reorder styling.
  519.     tile.classList.add(CLASSES.REORDER);
  520.     // Disable other hover/active styling for all tiles.
  521.     document.body.classList.add(CLASSES.REORDERING);
  522.  
  523.     // Set up event listeners for the reorder flow. Listen for drag events if
  524.     // |mouseMode|, touch events otherwise.
  525.     if (mouseMode) {
  526.       const trackCursor =
  527.           this.trackCursor_(tile, event.pageX, event.pageY, true);
  528.       // The 'dragover' event must be tracked at the document level, since the
  529.       // currently dragged tile will interfere with 'dragover' events on the
  530.       // other tiles.
  531.       const dragOver = (dragOverEvent) => {
  532.         trackCursor(dragOverEvent);
  533.         // Since the 'dragover' event is not tied to a specific tile, we need to
  534.         // manually check if we are hovering over a tile. If so, insert the held
  535.         // tile there.
  536.         this.reorderToIndexAtPoint_(dragOverEvent.pageX, dragOverEvent.pageY);
  537.       };
  538.       document.addEventListener('dragover', dragOver);
  539.       document.addEventListener('dragend', () => {
  540.         document.removeEventListener('dragover', dragOver);
  541.         this.stopReorder_(tile);
  542.       }, {once: true});
  543.     } else {
  544.       // Track the cursor on subsequent 'touchmove' events (the first
  545.       // 'touchmove' event that starts the reorder flow is ignored).
  546.       const trackCursor = this.trackCursor_(
  547.           tile, event.changedTouches[0].pageX, event.changedTouches[0].pageY,
  548.           false);
  549.       const touchEnd = (touchEndEvent) => {
  550.         tile.firstChild.removeEventListener('touchmove', trackCursor);
  551.         tile.firstChild.removeEventListener('touchend', touchEnd);
  552.         tile.firstChild.removeEventListener('touchcancel', touchEnd);
  553.         this.stopReorder_(tile);  // Stop the reorder flow.
  554.       };
  555.       tile.firstChild.addEventListener('touchmove', trackCursor);
  556.       tile.firstChild.addEventListener('touchend', touchEnd, {once: true});
  557.       tile.firstChild.addEventListener('touchcancel', touchEnd, {once: true});
  558.     }
  559.   }
  560.  
  561.   /**
  562.    * Stops the reorder flow. Resets the held tile's visual style and tells the
  563.    * EmbeddedSearchAPI that a tile has been moved.
  564.    * @param {!Element} tile Tile that has been moved.
  565.    * @private
  566.    */
  567.   stopReorder_(tile) {
  568.     const index = Number(tile.getAttribute('index'));
  569.  
  570.     // Remove reorder styling.
  571.     tile.classList.remove(CLASSES.REORDER);
  572.     document.body.classList.remove(CLASSES.REORDERING);
  573.  
  574.     // Move the tile to its new position and notify EmbeddedSearchAPI that the
  575.     // tile has been moved.
  576.     this.applyReorder_(tile, this.newIndexOfItemToReorder_);
  577.     chrome.embeddedSearch.newTabPage.reorderCustomLink(
  578.         Number(this.tiles_[index].getAttribute('rid')),
  579.         this.newIndexOfItemToReorder_);
  580.  
  581.     this.itemToReorder_ = -1;
  582.     this.newIndexOfItemToReorder_ = -1;
  583.   }
  584.  
  585.   /**
  586.    * Attempts to insert the currently held tile at the index located at (x, y).
  587.    * Does nothing if there is no tile at (x, y) or the reorder flow is not
  588.    * ongoing.
  589.    * @param {number} x The x coordinate.
  590.    * @param {number} y The y coordinate.
  591.    * @private
  592.    */
  593.   reorderToIndexAtPoint_(x, y) {
  594.     const elements = document.elementsFromPoint(x, y);
  595.     for (let i = 0; i < elements.length; i++) {
  596.       if (elements[i].classList.contains(CLASSES.GRID_TILE_CONTAINER) &&
  597.           elements[i].getAttribute('index') !== null) {
  598.         this.reorderToIndex_(Number(elements[i].getAttribute('index')));
  599.         return;
  600.       }
  601.     }
  602.   }
  603.  
  604.   /**
  605.    * Executed only when the reorder flow is ongoing. Inserts the currently held
  606.    * tile at |index| and shifts tiles accordingly.
  607.    * @param {number} index The index to insert the held tile at.
  608.    * @private
  609.    */
  610.   reorderToIndex_(index) {
  611.     if (this.newIndexOfItemToReorder_ === index ||
  612.         !document.body.classList.contains(CLASSES.REORDERING)) {
  613.       return;
  614.     }
  615.  
  616.     // Moves the held tile from its current position to |index|.
  617.     this.order_.move(this.newIndexOfItemToReorder_, index);
  618.     this.newIndexOfItemToReorder_ = index;
  619.     // Shift tiles according to the new order.
  620.     for (let i = 0; i < this.tiles_.length; i++) {
  621.       const tileIndex = this.order_[i];
  622.       // Don't move the tile we're holding nor the add shortcut button.
  623.       if (tileIndex === this.itemToReorder_ ||
  624.           this.tiles_[i].getAttribute('add') === 'true') {
  625.         continue;
  626.       }
  627.       this.applyReorder_(this.tiles_[tileIndex], i);
  628.     }
  629.   }
  630.  
  631.   /**
  632.    * Translates the |tile|'s |CLASSES.GRID_TILE| from |index| to |newIndex|.
  633.    * This is done to prevent interference with event listeners on the |tile|'s
  634.    * |CLASSES.GRID_TILE_CONTAINER|, particularly 'mouseover'.
  635.    * @param {!Element} tile Tile that is being shifted.
  636.    * @param {number} newIndex New index for the tile.
  637.    * @private
  638.    */
  639.   applyReorder_(tile, newIndex) {
  640.     if (tile.getAttribute('index') === null) {
  641.       throw new Error('Tile does not have an index.');
  642.     }
  643.     const index = Number(tile.getAttribute('index'));
  644.     const x = this.position_[newIndex].x - this.position_[index].x;
  645.     const y = this.position_[newIndex].y - this.position_[index].y;
  646.     this.translate_(tile.children[0], x, y);
  647.   }
  648.  
  649.   /**
  650.    * Moves |tile| so that it tracks the cursor's position. This is done by
  651.    * translating the |tile|'s |CLASSES.GRID_TILE|, which prevents interference
  652.    * with event listeners on the |tile|'s |CLASSES.GRID_TILE_CONTAINER|.
  653.    * @param {!Element} tile Tile that is being moved.
  654.    * @param {number} origCursorX Original x cursor position.
  655.    * @param {number} origCursorY Original y cursor position.
  656.    * @param {boolean} mouseMode True if the user is using a mouse.
  657.    * @private
  658.    */
  659.   trackCursor_(tile, origCursorX, origCursorY, mouseMode) {
  660.     const index = Number(tile.getAttribute('index'));
  661.     // RTL positions align with the right side of the grid. Therefore, the x
  662.     // value must be recalculated to align with the left.
  663.     const origPosX = this.isRtl_() ?
  664.         (this.gridWidth_ - (this.position_[index].x + this.tileWidth_)) :
  665.         this.position_[index].x;
  666.     const origPosY = this.position_[index].y;
  667.  
  668.     // Get the max translation allowed by the grid boundaries. This will be the
  669.     // x of the last tile in a row and the y of the tiles in the second row.
  670.     const maxTranslateX = this.gridWidth_ - this.tileWidth_;
  671.     const maxTranslateY = this.tileHeight_;
  672.  
  673.     const maxX = maxTranslateX - origPosX;
  674.     const maxY = maxTranslateY - origPosY;
  675.     const minX = 0 - origPosX;
  676.     const minY = 0 - origPosY;
  677.  
  678.     return (event) => {
  679.       const currX = mouseMode ? event.pageX : event.changedTouches[0].pageX;
  680.       const currY = mouseMode ? event.pageY : event.changedTouches[0].pageY;
  681.       // Do not exceed the iframe borders.
  682.       const x = Math.max(Math.min(currX - origCursorX, maxX), minX);
  683.       const y = Math.max(Math.min(currY - origCursorY, maxY), minY);
  684.       tile.firstChild.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
  685.     };
  686.   }
  687. }
  688.  
  689. /**
  690.  * Log an event on the NTP.
  691.  * @param {number} eventType Event from LOG_TYPE.
  692.  */
  693. function logEvent(eventType) {
  694.   chrome.embeddedSearch.newTabPage.logEvent(eventType);
  695. }
  696.  
  697. /**
  698.  * Log impression of an NTP tile.
  699.  * @param {number} tileIndex Position of the tile, >= 0 and < getMaxNumTiles().
  700.  * @param {number} tileTitleSource The source of the tile's title as received
  701.  *     from getMostVisitedItemData.
  702.  * @param {number} tileSource The tile's source as received from
  703.  *     getMostVisitedItemData.
  704.  * @param {number} tileType The tile's visual type from TileVisualType.
  705.  * @param {Date} dataGenerationTime Timestamp representing when the tile was
  706.  *     produced by a ranking algorithm.
  707.  */
  708. function logMostVisitedImpression(
  709.     tileIndex, tileTitleSource, tileSource, tileType, dataGenerationTime) {
  710.   chrome.embeddedSearch.newTabPage.logMostVisitedImpression(
  711.       tileIndex, tileTitleSource, tileSource, tileType, dataGenerationTime);
  712. }
  713.  
  714. /**
  715.  * Log click on an NTP tile.
  716.  * @param {number} tileIndex Position of the tile, >= 0 and < getMaxNumTiles().
  717.  * @param {number} tileTitleSource The source of the tile's title as received
  718.  *     from getMostVisitedItemData.
  719.  * @param {number} tileSource The tile's source as received from
  720.  *     getMostVisitedItemData.
  721.  * @param {number} tileType The tile's visual type from TileVisualType.
  722.  * @param {Date} dataGenerationTime Timestamp representing when the tile was
  723.  *     produced by a ranking algorithm.
  724.  */
  725. function logMostVisitedNavigation(
  726.     tileIndex, tileTitleSource, tileSource, tileType, dataGenerationTime) {
  727.   chrome.embeddedSearch.newTabPage.logMostVisitedNavigation(
  728.       tileIndex, tileTitleSource, tileSource, tileType, dataGenerationTime);
  729. }
  730.  
  731. /**
  732.  * Returns true if custom links are enabled.
  733.  * @return {boolean}
  734.  */
  735. function isCustomLinksEnabled() {
  736.   return customLinksFeatureEnabled &&
  737.       !chrome.embeddedSearch.newTabPage.isUsingMostVisited;
  738. }
  739.  
  740. /**
  741.  * Returns the maximum number of tiles to show at any time. This can be changed
  742.  * depending on what feature is enabled.
  743.  * @return {number}
  744.  */
  745. function getMaxNumTiles() {
  746.   return isCustomLinksEnabled() ? MD_MAX_NUM_CUSTOM_LINK_TILES :
  747.                                   MD_MAX_NUM_MOST_VISITED_TILES;
  748. }
  749.  
  750. /**
  751.  * Down counts the DOM elements that we are waiting for the page to load.
  752.  * When we get to 0, we send a message to the parent window.
  753.  * This is usually used as an EventListener of onload/onerror.
  754.  */
  755. function countLoad() {
  756.   loadedCounter -= 1;
  757.   if (loadedCounter <= 0) {
  758.     swapInNewTiles();
  759.     logEvent(LOG_TYPE.NTP_ALL_TILES_LOADED);
  760.     let tilesAreCustomLinks = isCustomLinksEnabled() &&
  761.         chrome.embeddedSearch.newTabPage.isCustomLinks;
  762.     // Tell the parent page whether to show the restore default shortcuts option
  763.     // in the menu.
  764.     window.parent.postMessage(
  765.         {cmd: 'loaded', showRestoreDefault: tilesAreCustomLinks},
  766.         DOMAIN_ORIGIN);
  767.     tilesAreCustomLinks = false;
  768.     // Reset to 1, so that any further 'show' message will cause us to swap in
  769.     // fresh tiles.
  770.     loadedCounter = 1;
  771.   }
  772. }
  773.  
  774. /**
  775.  * Handles postMessages coming from the host page to the iframe.
  776.  * Mostly, it dispatches every command to handleCommand.
  777.  */
  778. function handlePostMessage(event) {
  779.   if (event.data instanceof Array) {
  780.     for (let i = 0; i < event.data.length; ++i) {
  781.       handleCommand(event.data[i]);
  782.     }
  783.   } else {
  784.     handleCommand(event.data);
  785.   }
  786. }
  787.  
  788. /**
  789.  * Handles a single command coming from the host page to the iframe.
  790.  * We try to keep the logic here to a minimum and just dispatch to the relevant
  791.  * functions.
  792.  */
  793. function handleCommand(data) {
  794.   const cmd = data.cmd;
  795.  
  796.   if (cmd == 'tile') {
  797.     addTile(data);
  798.   } else if (cmd == 'show') {
  799.     // TODO(crbug.com/946225): If this happens before we have finished loading
  800.     // the previous tiles, we probably get into a bad state. If/when the iframe
  801.     // is removed this might no longer be a concern.
  802.     showTiles();
  803.   } else if (cmd == 'updateTheme') {
  804.     updateTheme(data);
  805.   } else if (cmd === 'focusMenu') {
  806.     focusTileMenu(data);
  807.   } else {
  808.     console.error('Unknown command: ' + JSON.stringify(data));
  809.   }
  810. }
  811.  
  812. /**
  813.  * Handler for the 'show' message from the host page.
  814.  */
  815. function showTiles() {
  816.   utils.setPlatformClass(document.body);
  817.   countLoad();
  818. }
  819.  
  820. /**
  821.  * Handler for the 'updateTheme' message from the host page.
  822.  * @param {!Object} info Data received in the message.
  823.  */
  824. function updateTheme(info) {
  825.   document.body.style.setProperty('--tile-title-color', info.tileTitleColor);
  826.   document.body.style.setProperty(
  827.       '--icon-background-color', info.iconBackgroundColor);
  828.   document.body.classList.toggle('dark-theme', info.isThemeDark);
  829.   document.body.classList.toggle('use-title-container', info.useTitleContainer);
  830.   document.body.classList.toggle('custom-background', info.customBackground);
  831.   document.body.classList.toggle('use-white-add-icon', info.useWhiteAddIcon);
  832.  
  833.   // Reduce font weight on the default(white) background for Mac and CrOS.
  834.   document.body.classList.toggle(
  835.       CLASSES.MAC_CHROMEOS,
  836.       !info.isThemeDark && !info.useTitleContainer &&
  837.           (navigator.userAgent.indexOf('Mac') > -1 ||
  838.            navigator.userAgent.indexOf('CrOS') > -1));
  839. }
  840.  
  841. /**
  842.  * Handler for 'focusMenu' message from the host page. Focuses the edited tile's
  843.  * menu or the add shortcut tile after closing the custom link edit dialog
  844.  * without saving.
  845.  * @param {!Object} info Data received in the message.
  846.  */
  847. function focusTileMenu(info) {
  848.   const tile = document.querySelector(`a.md-tile[data-rid="${info.rid}"]`);
  849.   if (info.rid === -1 /* Add shortcut tile */) {
  850.     tile.focus();
  851.   } else {
  852.     tile.parentNode.childNodes[1].focus();
  853.   }
  854. }
  855.  
  856. /**
  857.  * Removes all old instances of |IDS.MV_TILES| that are pending for deletion.
  858.  */
  859. function removeAllOldTiles() {
  860.   const parent = document.querySelector('#' + IDS.MOST_VISITED);
  861.   const oldList = parent.querySelectorAll('.mv-tiles-old');
  862.   for (let i = 0; i < oldList.length; ++i) {
  863.     parent.removeChild(oldList[i]);
  864.   }
  865. }
  866.  
  867. /**
  868.  * Called when all tiles have finished loading (successfully or not), and we are
  869.  * ready to show the new tiles and drop the old ones.
  870.  */
  871. function swapInNewTiles() {
  872.   // Store the tiles on the current closure.
  873.   const cur = tiles;
  874.  
  875.   // Add an "add new custom link" button if we haven't reached the maximum
  876.   // number of tiles.
  877.   if (isCustomLinksEnabled() && cur.childNodes.length < getMaxNumTiles()) {
  878.     const data = {
  879.       'rid': -1,
  880.       'title': queryArgs['addLink'],
  881.       'url': '',
  882.       'isAddButton': true,
  883.       'dataGenerationTime': new Date(),
  884.       'tileSource': -1,
  885.       'tileTitleSource': -1
  886.     };
  887.     tiles.appendChild(renderTile(data));
  888.   }
  889.  
  890.   const parent = document.querySelector('#' + IDS.MOST_VISITED);
  891.  
  892.   const old = parent.querySelector('#' + IDS.MV_TILES);
  893.   if (old) {
  894.     // Mark old tile DIV for removal after the transition animation is done.
  895.     old.removeAttribute('id');
  896.     old.classList.add('mv-tiles-old');
  897.     old.style.opacity = 0.0;
  898.     cur.addEventListener('transitionend', function(ev) {
  899.       if (ev.target === cur) {
  900.         removeAllOldTiles();
  901.       }
  902.     });
  903.   }
  904.  
  905.   // Add new tileset.
  906.   cur.id = IDS.MV_TILES;
  907.   parent.appendChild(cur);
  908.  
  909.   // Initialize the new tileset before modifying opacity. This will prevent the
  910.   // transform transition from applying after the tiles fade in.
  911.   currGrid.init(cur);
  912.  
  913.   const flushOpacity = () => window.getComputedStyle(cur).opacity;
  914.  
  915.   // getComputedStyle causes the initial style (opacity 0) to be applied, so
  916.   // that when we then set it to 1, that triggers the CSS transition.
  917.   flushOpacity();
  918.   cur.style.opacity = 1.0;
  919.  
  920.   // Make sure the tiles variable contain the next tileset we'll use if the host
  921.   // page sends us an updated set of tiles.
  922.   tiles = document.createElement('div');
  923. }
  924.  
  925. /**
  926.  * Explicitly hide tiles that are not visible in order to prevent keyboard
  927.  * navigation.
  928.  */
  929. function updateTileVisibility() {
  930.   const allTiles =
  931.       document.querySelectorAll('#' + IDS.MV_TILES + ' .' + CLASSES.MD_TILE);
  932.   if (allTiles.length === 0) {
  933.     return;
  934.   }
  935.  
  936.   // Get the current number of tiles per row. Hide any tile after the first two
  937.   // rows.
  938.   const tilesPerRow = Math.trunc(document.body.offsetWidth / MD_TILE_WIDTH);
  939.   for (let i = MD_NUM_TILES_ALWAYS_VISIBLE; i < allTiles.length; i++) {
  940.     allTiles[i].style.display = (i < tilesPerRow * 2) ? 'block' : 'none';
  941.   }
  942. }
  943.  
  944. /**
  945.  * Handler for the 'show' message from the host page, called when it wants to
  946.  * add a suggestion tile.
  947.  * @param {!MostVisitedData} args Data for the tile to be rendered.
  948.  */
  949. function addTile(args) {
  950.   if (!isFinite(args.rid)) {
  951.     return;
  952.   }
  953.  
  954.   // Grab the tile's data from the embeddedSearch API.
  955.   const data =
  956.       chrome.embeddedSearch.newTabPage.getMostVisitedItemData(args.rid);
  957.   if (!data) {
  958.     return;
  959.   }
  960.  
  961.   if (!data.faviconUrl) {
  962.     data.faviconUrl = 'chrome-search://favicon/size/16@' +
  963.         window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.rid;
  964.   }
  965.   tiles.appendChild(renderTile(data));
  966. }
  967.  
  968. /**
  969.  * Called when the user decided to add a tile to the blacklist.
  970.  * It sets off the animation for the blacklist and sends the blacklisted id
  971.  * to the host page.
  972.  * @param {Element} tile DOM node of the tile we want to remove.
  973.  */
  974. function blacklistTile(tile) {
  975.   const rid = Number(tile.getAttribute('data-rid'));
  976.  
  977.   if (isCustomLinksEnabled()) {
  978.     chrome.embeddedSearch.newTabPage.deleteMostVisitedItem(rid);
  979.   } else {
  980.     tile.classList.add('blacklisted');
  981.     tile.addEventListener('transitionend', function(ev) {
  982.       if (ev.propertyName != 'width') {
  983.         return;
  984.       }
  985.       window.parent.postMessage(
  986.           {cmd: 'tileBlacklisted', rid: Number(rid)}, DOMAIN_ORIGIN);
  987.     });
  988.   }
  989. }
  990.  
  991. /**
  992.  * Starts edit custom link flow. Tells host page to show the edit custom link
  993.  * dialog and pre-populate it with data obtained using the link's id.
  994.  * @param {?number} rid Restricted id of the tile we want to edit.
  995.  */
  996. function editCustomLink(rid) {
  997.   window.parent.postMessage({cmd: 'startEditLink', rid: rid}, DOMAIN_ORIGIN);
  998. }
  999.  
  1000. /**
  1001.  * Renders a MostVisited tile (i.e. shortcut) to the DOM.
  1002.  * @param {!MostVisitedData} data Object containing rid, url, title, favicon,
  1003.  *     and optionally isAddButton. isAddButton is true if you want to construct
  1004.  *     an add custom link button, and can only be set if custom links is
  1005.  *     enabled.
  1006.  * @return {Element}
  1007.  */
  1008. function renderTile(data) {
  1009.   const mdTile = document.createElement('a');
  1010.   mdTile.className = CLASSES.MD_TILE;
  1011.  
  1012.   // The tile will be appended to |tiles|.
  1013.   const position = tiles.children.length;
  1014.   // This is set in the load/error event for the favicon image.
  1015.   let tileType = TileVisualType.NONE;
  1016.  
  1017.   mdTile.setAttribute('data-rid', data.rid);
  1018.   mdTile.setAttribute('data-pos', position);
  1019.   if (utils.isSchemeAllowed(data.url)) {
  1020.     mdTile.href = data.url;
  1021.   }
  1022.   mdTile.setAttribute('aria-label', data.title);
  1023.   mdTile.title = data.title;
  1024.  
  1025.   mdTile.addEventListener('click', function(ev) {
  1026.     if (data.isAddButton) {
  1027.       editCustomLink(null);
  1028.       logEvent(LOG_TYPE.NTP_CUSTOMIZE_ADD_SHORTCUT_CLICKED);
  1029.     } else {
  1030.       logMostVisitedNavigation(
  1031.           position, data.tileTitleSource, data.tileSource, tileType,
  1032.           data.dataGenerationTime);
  1033.     }
  1034.   });
  1035.   mdTile.addEventListener('keydown', function(event) {
  1036.     if ((event.keyCode === KEYCODES.delete ||
  1037.          event.keyCode === KEYCODES.BACKSPACE) &&
  1038.         !data.isAddButton) {
  1039.       event.preventDefault();
  1040.       event.stopPropagation();
  1041.       blacklistTile(mdTile);
  1042.     } else if (
  1043.         event.keyCode === KEYCODES.ENTER || event.keyCode === KEYCODES.SPACE) {
  1044.       event.preventDefault();
  1045.       this.click();
  1046.     } else if (event.keyCode === KEYCODES.LEFT) {
  1047.       const tiles = document.querySelectorAll(
  1048.           '#' + IDS.MV_TILES + ' .' + CLASSES.MD_TILE);
  1049.       tiles[Math.max(Number(this.getAttribute('data-pos')) - 1, 0)].focus();
  1050.     } else if (event.keyCode === KEYCODES.RIGHT) {
  1051.       const tiles = document.querySelectorAll(
  1052.           '#' + IDS.MV_TILES + ' .' + CLASSES.MD_TILE);
  1053.       tiles[Math.min(
  1054.                 Number(this.getAttribute('data-pos')) + 1, tiles.length - 1)]
  1055.           .focus();
  1056.     }
  1057.   });
  1058.   utils.disableOutlineOnMouseClick(mdTile);
  1059.  
  1060.   const mdTileInner = document.createElement('div');
  1061.   mdTileInner.className = CLASSES.MD_TILE_INNER;
  1062.  
  1063.   if (data.isAddButton) {
  1064.     mdTile.tabIndex = 0;
  1065.  
  1066.     const mdIconAdd = document.createElement('div');
  1067.     mdIconAdd.classList.add(CLASSES.MD_ICON);
  1068.     mdIconAdd.classList.add(CLASSES.MD_ADD_ICON);
  1069.  
  1070.     mdTileInner.appendChild(mdIconAdd);
  1071.   } else {
  1072.     const mdIcon = document.createElement('img');
  1073.     mdIcon.classList.add(CLASSES.MD_ICON);
  1074.     // Set title and alt to empty so screen readers won't say the image name.
  1075.     mdIcon.title = '';
  1076.     mdIcon.alt = '';
  1077.     const url = new URL('chrome-search://ntpicon/');
  1078.     url.searchParams.set('size', '24@' + window.devicePixelRatio + 'x');
  1079.     url.searchParams.set('url', data.url);
  1080.     mdIcon.src = url.toString();
  1081.     loadedCounter += 1;
  1082.     mdIcon.addEventListener('load', function(ev) {
  1083.       // Store the type for a potential later navigation.
  1084.       tileType = TileVisualType.ICON_REAL;
  1085.       logMostVisitedImpression(
  1086.           position, data.tileTitleSource, data.tileSource, tileType,
  1087.           data.dataGenerationTime);
  1088.       // Note: It's important to call countLoad last, because that might emit
  1089.       // the NTP_ALL_TILES_LOADED event, which must happen after the impression
  1090.       // log.
  1091.       countLoad();
  1092.     });
  1093.     mdIcon.addEventListener('error', function(ev) {
  1094.       const fallbackBackground = document.createElement('div');
  1095.       fallbackBackground.className = CLASSES.MD_ICON;
  1096.       const fallbackLetter = document.createElement('div');
  1097.       fallbackLetter.className = CLASSES.MD_FALLBACK_LETTER;
  1098.       fallbackLetter.textContent = data.title.charAt(0).toUpperCase();
  1099.       fallbackBackground.classList.add(CLASSES.FAILED_FAVICON);
  1100.  
  1101.       fallbackBackground.appendChild(fallbackLetter);
  1102.       mdTileInner.replaceChild(fallbackBackground, mdIcon);
  1103.  
  1104.       // Store the type for a potential later navigation.
  1105.       tileType = TileVisualType.ICON_DEFAULT;
  1106.       logMostVisitedImpression(
  1107.           position, data.tileTitleSource, data.tileSource, tileType,
  1108.           data.dataGenerationTime);
  1109.       // Note: It's important to call countLoad last, because that might emit
  1110.       // the NTP_ALL_TILES_LOADED event, which must happen after the impression
  1111.       // log.
  1112.       countLoad();
  1113.     });
  1114.  
  1115.     mdTileInner.appendChild(mdIcon);
  1116.   }
  1117.  
  1118.   const mdTitle = document.createElement('div');
  1119.   mdTitle.className = CLASSES.MD_TITLE;
  1120.   mdTitle.style.direction = data.direction || 'ltr';
  1121.   const mdTitleTextwrap = document.createElement('span');
  1122.   mdTitleTextwrap.innerText = data.title;
  1123.   mdTitle.appendChild(mdTitleTextwrap);
  1124.   mdTileInner.appendChild(mdTitle);
  1125.   mdTile.appendChild(mdTileInner);
  1126.  
  1127.   if (!data.isAddButton) {
  1128.     const mdMenu = document.createElement('button');
  1129.     mdMenu.className = CLASSES.MD_MENU;
  1130.     if (isCustomLinksEnabled()) {
  1131.       mdMenu.classList.add(CLASSES.MD_EDIT_MENU);
  1132.       mdMenu.title = queryArgs['editLinkTooltip'] || '';
  1133.       mdMenu.setAttribute(
  1134.           'aria-label',
  1135.           (queryArgs['editLinkTooltip'] || '') + ' ' + data.title);
  1136.       mdMenu.addEventListener('click', function(ev) {
  1137.         editCustomLink(data.rid);
  1138.         ev.preventDefault();
  1139.         ev.stopPropagation();
  1140.         logEvent(LOG_TYPE.NTP_CUSTOMIZE_EDIT_SHORTCUT_CLICKED);
  1141.       });
  1142.     } else {
  1143.       mdMenu.title = queryArgs['removeTooltip'] || '';
  1144.       mdMenu.setAttribute(
  1145.           'aria-label', (queryArgs['removeTooltip'] || '') + ' ' + data.title);
  1146.       mdMenu.addEventListener('click', function(ev) {
  1147.         removeAllOldTiles();
  1148.         blacklistTile(mdTile);
  1149.         ev.preventDefault();
  1150.         ev.stopPropagation();
  1151.       });
  1152.     }
  1153.     // Don't allow the event to bubble out to the containing tile, as that would
  1154.     // trigger navigation to the tile URL.
  1155.     mdMenu.addEventListener('keydown', function(ev) {
  1156.       ev.stopPropagation();
  1157.     });
  1158.     utils.disableOutlineOnMouseClick(mdMenu);
  1159.  
  1160.     mdTile.appendChild(mdMenu);
  1161.   }
  1162.  
  1163.   return currGrid.createGridTile(mdTile, data.rid, !!data.isAddButton);
  1164. }
  1165.  
  1166. /**
  1167.  * Does some initialization and parses the query arguments passed to the iframe.
  1168.  */
  1169. function init() {
  1170.   // Create a new DOM element to hold the tiles. The tiles will be added
  1171.   // one-by-one via addTile, and the whole thing will be inserted into the page
  1172.   // in swapInNewTiles, after the parent has sent us the 'show' message, and all
  1173.   // favicons have loaded.
  1174.   tiles = document.createElement('div');
  1175.  
  1176.   // Parse query arguments.
  1177.   const query = window.location.search.substring(1).split('&');
  1178.   queryArgs = {};
  1179.   for (let i = 0; i < query.length; ++i) {
  1180.     const val = query[i].split('=');
  1181.     if (val[0] == '') {
  1182.       continue;
  1183.     }
  1184.     queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]);
  1185.   }
  1186.  
  1187.   document.title = queryArgs['title'];
  1188.  
  1189.   // Enable RTL.
  1190.   if (queryArgs['rtl'] == '1') {
  1191.     document.documentElement.dir = 'rtl';
  1192.   }
  1193.  
  1194.   // Enable custom links.
  1195.   if (queryArgs['enableCustomLinks'] == '1') {
  1196.     customLinksFeatureEnabled = true;
  1197.   }
  1198.  
  1199.   currGrid = new Grid();
  1200.   // Set up layout updates on window resize. Throttled according to
  1201.   // |RESIZE_TIMEOUT_DELAY|.
  1202.   let resizeTimeout;
  1203.   window.onresize = () => {
  1204.     if (resizeTimeout) {
  1205.       window.clearTimeout(resizeTimeout);
  1206.     }
  1207.     resizeTimeout = window.setTimeout(() => {
  1208.       resizeTimeout = null;
  1209.       currGrid.onresize();
  1210.     }, RESIZE_TIMEOUT_DELAY);
  1211.   };
  1212.  
  1213.   window.addEventListener('message', handlePostMessage);
  1214. }
  1215.  
  1216. /**
  1217.  * Binds event listeners.
  1218.  */
  1219. function listen() {
  1220.   document.addEventListener('DOMContentLoaded', init);
  1221. }
  1222.  
  1223. return {
  1224.   Grid: Grid,  // Exposed for testing.
  1225.   init: init,  // Exposed for testing.
  1226.   listen: listen,
  1227. };
  1228. }
  1229.  
  1230. if (!window.mostVisitedUnitTest) {
  1231.   MostVisited().listen();
  1232. }
Parsed in 0.183 seconds