diff options
author | Moonchild <mcwerewolf@gmail.com> | 2018-06-22 12:55:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-22 12:55:34 +0200 |
commit | 4b65237d5d6e6f84a69571435e7b9fa5b36f444f (patch) | |
tree | 2e64ef05d542aca638ea2970d620c987d113e2d5 /application/palemoon/base/content/newtab | |
parent | e6f765a27070a6b922742e61d0a14dcc2c18baba (diff) | |
parent | 576124e629862cd75769074d572dbf4ee8149945 (diff) | |
download | UXP-4b65237d5d6e6f84a69571435e7b9fa5b36f444f.tar UXP-4b65237d5d6e6f84a69571435e7b9fa5b36f444f.tar.gz UXP-4b65237d5d6e6f84a69571435e7b9fa5b36f444f.tar.lz UXP-4b65237d5d6e6f84a69571435e7b9fa5b36f444f.tar.xz UXP-4b65237d5d6e6f84a69571435e7b9fa5b36f444f.zip |
Merge pull request #525 from MoonchildProductions/newtab-page-work
Newtab page work
Diffstat (limited to 'application/palemoon/base/content/newtab')
-rw-r--r-- | application/palemoon/base/content/newtab/drag.js | 2 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/dragDataHelper.js | 2 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/dropTargetShim.js | 154 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/grid.js | 217 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/newTab.css | 261 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/newTab.js | 17 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/newTab.xhtml | 60 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/newTab.xul | 55 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/page.js | 214 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/search.js | 134 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/sites.js | 227 | ||||
-rw-r--r-- | application/palemoon/base/content/newtab/transformations.js | 27 |
12 files changed, 1051 insertions, 319 deletions
diff --git a/application/palemoon/base/content/newtab/drag.js b/application/palemoon/base/content/newtab/drag.js index fbd688faa..e3928ebd0 100644 --- a/application/palemoon/base/content/newtab/drag.js +++ b/application/palemoon/base/content/newtab/drag.js @@ -140,7 +140,7 @@ var gDrag = { // drag image with its default opacity. let dragElement = document.createElementNS(HTML_NAMESPACE, "div"); dragElement.classList.add("newtab-drag"); - let scrollbox = document.getElementById("newtab-scrollbox"); + let scrollbox = document.getElementById("newtab-vertical-margin"); scrollbox.appendChild(dragElement); dt.setDragImage(dragElement, 0, 0); diff --git a/application/palemoon/base/content/newtab/dragDataHelper.js b/application/palemoon/base/content/newtab/dragDataHelper.js index 54348ab14..675ff2671 100644 --- a/application/palemoon/base/content/newtab/dragDataHelper.js +++ b/application/palemoon/base/content/newtab/dragDataHelper.js @@ -11,7 +11,7 @@ var gDragDataHelper = { getLinkFromDragEvent: function DragDataHelper_getLinkFromDragEvent(aEvent) { let dt = aEvent.dataTransfer; - if (!dt || !dt.types.contains(this.mimeType)) { + if (!dt || !dt.types.includes(this.mimeType)) { return null; } diff --git a/application/palemoon/base/content/newtab/dropTargetShim.js b/application/palemoon/base/content/newtab/dropTargetShim.js index 046dbea1e..57a97fa00 100644 --- a/application/palemoon/base/content/newtab/dropTargetShim.js +++ b/application/palemoon/base/content/newtab/dropTargetShim.js @@ -23,27 +23,53 @@ var gDropTargetShim = { /** * Initializes the drop target shim. */ - init: function DropTargetShim_init() { - let node = gGrid.node; + init: function () { + gGrid.node.addEventListener("dragstart", this, true); + }, + + /** + * Add all event listeners needed during a drag operation. + */ + _addEventListeners: function () { + gGrid.node.addEventListener("dragend", this); - // Add drag event handlers. - node.addEventListener("dragstart", this, true); - node.addEventListener("dragend", this, true); + let docElement = document.documentElement; + docElement.addEventListener("dragover", this); + docElement.addEventListener("dragenter", this); + docElement.addEventListener("drop", this); + }, + + /** + * Remove all event listeners that were needed during a drag operation. + */ + _removeEventListeners: function () { + gGrid.node.removeEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.removeEventListener("dragover", this); + docElement.removeEventListener("dragenter", this); + docElement.removeEventListener("drop", this); }, /** * Handles all shim events. */ - handleEvent: function DropTargetShim_handleEvent(aEvent) { + handleEvent: function (aEvent) { switch (aEvent.type) { case "dragstart": - this._start(aEvent); + this._dragstart(aEvent); + break; + case "dragenter": + aEvent.preventDefault(); break; case "dragover": this._dragover(aEvent); break; + case "drop": + this._drop(aEvent); + break; case "dragend": - this._end(aEvent); + this._dragend(aEvent); break; } }, @@ -52,69 +78,63 @@ var gDropTargetShim = { * Handles the 'dragstart' event. * @param aEvent The 'dragstart' event. */ - _start: function DropTargetShim_start(aEvent) { + _dragstart: function (aEvent) { if (aEvent.target.classList.contains("newtab-link")) { gGrid.lock(); - - // XXX bug 505521 - Listen for dragover on the document. - document.documentElement.addEventListener("dragover", this, false); + this._addEventListeners(); } }, /** - * Handles the 'drag' event and determines the current drop target. - * @param aEvent The 'drag' event. + * Handles the 'dragover' event. + * @param aEvent The 'dragover' event. */ - _drag: function DropTargetShim_drag(aEvent) { - // Let's see if we find a drop target. - let target = this._findDropTarget(aEvent); - - if (target != this._lastDropTarget) { - if (this._lastDropTarget) - // We left the last drop target. - this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); - - if (target) - // We're now hovering a (new) drop target. - this._dispatchEvent(aEvent, "dragenter", target); + _dragover: function (aEvent) { + // XXX bug 505521 - Use the dragover event to retrieve the + // current mouse coordinates while dragging. + let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; + gDrag.drag(sourceNode._newtabSite, aEvent); - if (this._lastDropTarget) - // We left the last drop target. - this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + // Find the current drop target, if there's one. + this._updateDropTarget(aEvent); - this._lastDropTarget = target; + // If we have a valid drop target, + // let the drag-and-drop service know. + if (this._lastDropTarget) { + aEvent.preventDefault(); } }, /** - * Handles the 'dragover' event as long as bug 505521 isn't fixed to get - * current mouse cursor coordinates while dragging. - * @param aEvent The 'dragover' event. + * Handles the 'drop' event. + * @param aEvent The 'drop' event. */ - _dragover: function DropTargetShim_dragover(aEvent) { - let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; - gDrag.drag(sourceNode._newtabSite, aEvent); + _drop: function (aEvent) { + // We're accepting all drops. + aEvent.preventDefault(); - this._drag(aEvent); + // remember that drop event was seen, this explicitly + // assumes that drop event preceeds dragend event + this._dropSeen = true; + + // Make sure to determine the current drop target + // in case the dragover event hasn't been fired. + this._updateDropTarget(aEvent); + + // A site was successfully dropped. + this._dispatchEvent(aEvent, "drop", this._lastDropTarget); }, /** * Handles the 'dragend' event. * @param aEvent The 'dragend' event. */ - _end: function DropTargetShim_end(aEvent) { - // Make sure to determine the current drop target in case the dragenter - // event hasn't been fired. - this._drag(aEvent); - + _dragend: function (aEvent) { if (this._lastDropTarget) { - if (aEvent.dataTransfer.mozUserCancelled) { - // The drag operation was cancelled. + if (aEvent.dataTransfer.mozUserCancelled || !this._dropSeen) { + // The drag operation was cancelled or no drop event was generated this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); - } else { - // A site was successfully dropped. - this._dispatchEvent(aEvent, "drop", this._lastDropTarget); } // Clean up. @@ -122,10 +142,35 @@ var gDropTargetShim = { this._cellPositions = null; } + this._dropSeen = false; gGrid.unlock(); + this._removeEventListeners(); + }, - // XXX bug 505521 - Remove the document's dragover listener. - document.documentElement.removeEventListener("dragover", this, false); + /** + * Tries to find the current drop target and will fire + * appropriate dragenter, dragexit, and dragleave events. + * @param aEvent The current drag event. + */ + _updateDropTarget: function (aEvent) { + // Let's see if we find a drop target. + let target = this._findDropTarget(aEvent); + + if (target != this._lastDropTarget) { + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + + if (target) + // We're now hovering a (new) drop target. + this._dispatchEvent(aEvent, "dragenter", target); + + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + + this._lastDropTarget = target; + } }, /** @@ -133,7 +178,7 @@ var gDropTargetShim = { * against all cells in the grid. * @return The currently hovered drop target or null. */ - _findDropTarget: function DropTargetShim_findDropTarget() { + _findDropTarget: function () { // These are the minimum intersection values - we want to use the cell if // the site is >= 50% hovering its position. let minWidth = gDrag.cellWidth / 2; @@ -174,13 +219,12 @@ var gDropTargetShim = { * @param aType The event type. * @param aTarget The target node that receives the event. */ - _dispatchEvent: - function DropTargetShim_dispatchEvent(aEvent, aType, aTarget) { - + _dispatchEvent: function (aEvent, aType, aTarget) { let node = aTarget.node; - let event = document.createEvent("DragEvents"); + let event = document.createEvent("DragEvent"); - event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false, + // The event should not bubble to prevent recursion. + event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, node, aEvent.dataTransfer); node.dispatchEvent(event); diff --git a/application/palemoon/base/content/newtab/grid.js b/application/palemoon/base/content/newtab/grid.js index fbeb7bd27..be5a57c4b 100644 --- a/application/palemoon/base/content/newtab/grid.js +++ b/application/palemoon/base/content/newtab/grid.js @@ -5,6 +5,12 @@ #endif /** + * Define various fixed dimensions + */ +const GRID_BOTTOM_EXTRA = 7; // title's line-height extends 7px past the margin +const GRID_WIDTH_EXTRA = 1; // provide 1px buffer to allow for rounding error + +/** * This singleton represents the grid that contains all sites. */ var gGrid = { @@ -12,6 +18,7 @@ var gGrid = { * The DOM node of the grid. */ _node: null, + _gridDefaultContent: null, get node() { return this._node; }, /** @@ -22,7 +29,7 @@ var gGrid = { /** * All cells contained in the grid. */ - _cells: null, + _cells: [], get cells() { return this._cells; }, /** @@ -31,7 +38,10 @@ var gGrid = { get sites() { return [for (cell of this.cells) cell.site]; }, // Tells whether the grid has already been initialized. - get ready() { return !!this._node; }, + get ready() { return !!this._ready; }, + + // Returns whether the page has finished loading yet. + get isDocumentLoaded() { return document.readyState == "complete"; }, /** * Initializes the grid. @@ -39,8 +49,26 @@ var gGrid = { */ init: function Grid_init() { this._node = document.getElementById("newtab-grid"); + this._gridDefaultContent = this._node.lastChild; this._createSiteFragment(); - this._render(); + + gLinks.populateCache(() => { + this._refreshGrid(); + this._ready = true; + + // If fetching links took longer than loading the page itself then + // we need to resize the grid as that was blocked until now. + // We also want to resize now if the page was already loaded when + // initializing the grid (the user toggled the page). + this._resizeGrid(); + + addEventListener("resize", this); + }); + + // Resize the grid as soon as the page loads. + if (!this.isDocumentLoaded) { + addEventListener("load", this); + } }, /** @@ -56,25 +84,15 @@ var gGrid = { }, /** - * Refreshes the grid and re-creates all sites. + * Handles all grid events. */ - refresh: function Grid_refresh() { - let cells = this.cells; - if (!cells) { - return; + handleEvent: function Grid_handleEvent(aEvent) { + switch (aEvent.type) { + case "load": + case "resize": + this._resizeGrid(); + break; } - - // Remove all sites. - cells.forEach(function (cell) { - let node = cell.node; - let child = node.firstElementChild; - - if (child) - node.removeChild(child); - }, this); - - // Render the grid again. - this._render(); }, /** @@ -92,34 +110,62 @@ var gGrid = { }, /** - * Creates the newtab grid. + * Renders and resizes the gird. _resizeGrid() call is needed to ensure + * that scrollbar disappears when the bottom row becomes empty following + * the block action, or tile display is turmed off via cog menu + */ + + refresh() { + this._refreshGrid(); + this._resizeGrid(); + }, + + /** + * Renders the grid, including cells and sites. */ - _renderGrid: function Grid_renderGrid() { - let row = document.createElementNS(HTML_NAMESPACE, "div"); + _refreshGrid() { let cell = document.createElementNS(HTML_NAMESPACE, "div"); - row.classList.add("newtab-row"); cell.classList.add("newtab-cell"); - // Clear the grid - this._node.innerHTML = ""; - - // Creates the structure of one row - for (let i = 0; i < gGridPrefs.gridColumns; i++) { - row.appendChild(cell.cloneNode(true)); + // Creates all the cells up to the maximum + let fragment = document.createDocumentFragment(); + for (let i = 0; i < gGridPrefs.gridColumns * gGridPrefs.gridRows; i++) { + fragment.appendChild(cell.cloneNode(true)); } - // Creates the grid - for (let j = 0; j < gGridPrefs.gridRows; j++) { - this._node.appendChild(row.cloneNode(true)); + + // Create cells. + let cells = Array.from(fragment.childNodes, (cell) => new Cell(this, cell)); + + // Fetch links. + let links = gLinks.getLinks(); + + // Create sites. + let numLinks = Math.min(links.length, cells.length); + let hasHistoryTiles = false; + for (let i = 0; i < numLinks; i++) { + if (links[i]) { + this.createSite(links[i], cells[i]); + if (links[i].type == "history") { + hasHistoryTiles = true; + } + } } - // (Re-)initialize all cells. - let cellElements = this.node.querySelectorAll(".newtab-cell"); - // Tycho: this._cells = [new Cell(this, cell) for (cell of cellElements)]; - this._cells = []; - - for (let cellItem of cellElements) { - this._cells.push(new Cell(this, cellItem)); + this._cells = cells; + while (this._gridDefaultContent.nextSibling) { + this._gridDefaultContent.nextSibling.remove(); } + this._node.appendChild(fragment); + }, + + /** + * Calculate the height for a number of rows up to the maximum rows + * @param rows Number of rows defaulting to the max + */ + _computeHeight: function Grid_computeHeight(aRows) { + let {gridRows} = gGridPrefs; + aRows = aRows === undefined ? gridRows : Math.min(gridRows, aRows); + return aRows * this._cellHeight + GRID_BOTTOM_EXTRA; }, /** @@ -133,7 +179,8 @@ var gGrid = { // Create the site's inner HTML code. site.innerHTML = '<a class="newtab-link">' + - ' <span class="newtab-thumbnail"/>' + + ' <span class="newtab-thumbnail placeholder"/>' + + ' <span class="newtab-thumbnail thumbnail"/>' + ' <span class="newtab-title"/>' + '</a>' + '<input type="button" title="' + newTabString("pin") + '"' + @@ -146,36 +193,80 @@ var gGrid = { }, /** - * Renders the sites, creates all sites and puts them into their cells. + * Test a tile at a given position for being pinned or history + * @param position Position in sites array */ - _renderSites: function Grid_renderSites() { - let cells = this.cells; - // Put sites into the cells. - let links = gLinks.getLinks(); - let length = Math.min(links.length, cells.length); - - for (let i = 0; i < length; i++) { - if (links[i]) - this.createSite(links[i], cells[i]); - } + _isHistoricalTile: function Grid_isHistoricalTile(aPos) { + let site = this.sites[aPos]; + return site && (site.isPinned() || site.link && site.link.type == "history"); }, /** - * Renders the grid. + * Make sure the correct number of rows and columns are visible */ - _render: function Grid_render() { - if (this._shouldRenderGrid()) { - this._renderGrid(); + _resizeGrid: function Grid_resizeGrid() { + // If we're somehow called before the page has finished loading, + // let's bail out to avoid caching zero heights and widths. + // We'll be called again when DOMContentLoaded fires. + // Same goes for the grid if that's not ready yet. + if (!this.isDocumentLoaded || !this._ready) { + return; } - this._renderSites(); - }, + // Save the cell's computed height/width including margin and border + if (this._cellHeight === undefined) { + let refCell = document.querySelector(".newtab-cell"); + let style = getComputedStyle(refCell); + this._cellHeight = refCell.offsetHeight + + parseFloat(style.marginTop) + parseFloat(style.marginBottom); + this._cellWidth = refCell.offsetWidth + + parseFloat(style.marginLeft) + parseFloat(style.marginRight); + } + + let searchContainer = document.querySelector("#searchContainer"); + // Save search-container margin height + if (this._searchContainerMargin === undefined) { + let style = getComputedStyle(searchContainer); + this._searchContainerMargin = parseFloat(style.marginBottom) + + parseFloat(style.marginTop); + } + + // Find the number of rows we can place into view port + let availHeight = document.documentElement.clientHeight - + searchContainer.offsetHeight - this._searchContainerMargin; + let visibleRows = Math.floor(availHeight / this._cellHeight); + + // Find the number of columns that fit into view port + let maxGridWidth = gGridPrefs.gridColumns * this._cellWidth + GRID_WIDTH_EXTRA; + // available width is current grid width, but no greater than maxGridWidth + let availWidth = Math.min(document.querySelector("#newtab-grid").clientWidth, + maxGridWidth); + // finally get the number of columns we can fit into view port + let gridColumns = Math.floor(availWidth / this._cellWidth); + // walk sites backwords until a pinned or history tile is found or visibleRows reached + let tileIndex = Math.min(gGridPrefs.gridRows * gridColumns, this.sites.length) - 1; + while (tileIndex >= visibleRows * gridColumns) { + if (this._isHistoricalTile(tileIndex)) { + break; + } + tileIndex--; + } - _shouldRenderGrid : function Grid_shouldRenderGrid() { - let rowsLength = this._node.querySelectorAll(".newtab-row").length; - let cellsLength = this._node.querySelectorAll(".newtab-cell").length; + // Compute the actual number of grid rows we will display (potentially + // with a scroll bar). tileIndex now points to a historical tile with + // heighest index or to the last index of the visible row, if none found + // Dividing tileIndex by number of tiles in a column gives the rows + let gridRows = Math.floor(tileIndex / gridColumns) + 1; - return (rowsLength != gGridPrefs.gridRows || - cellsLength != (gGridPrefs.gridRows * gGridPrefs.gridColumns)); + // we need to set grid width, for otherwise the scrollbar may shrink + // the grid when shown and cause grid layout to be different from + // what being computed above. This, in turn, may cause scrollbar shown + // for directory tiles, and introduce jitter when grid width is aligned + // exactly on the column boundary + this._node.style.width = gridColumns * this._cellWidth + "px"; + this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth + + GRID_WIDTH_EXTRA + "px"; + this._node.style.height = this._computeHeight() + "px"; + this._node.style.maxHeight = this._computeHeight(gridRows) + "px"; } }; diff --git a/application/palemoon/base/content/newtab/newTab.css b/application/palemoon/base/content/newtab/newTab.css index 830e4a8c1..a5431cf65 100644 --- a/application/palemoon/base/content/newtab/newTab.css +++ b/application/palemoon/base/content/newtab/newTab.css @@ -2,26 +2,37 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -input[type=button] { - cursor: pointer; +html { + width: 100%; + height: 100%; } -/* SCROLLBOX */ -#newtab-scrollbox { +body { + font: message-box; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + background-color: #F9F9F9; display: -moz-box; position: relative; -moz-box-flex: 1; -moz-user-focus: normal; + -moz-box-orient: vertical; } -#newtab-scrollbox:not([page-disabled]) { - overflow: auto; +input { + font: message-box; + font-size: 16px; +} + +input[type=button] { + cursor: pointer; } /* UNDO */ #newtab-undo-container { transition: opacity 100ms ease-out; - display: -moz-box; -moz-box-align: center; -moz-box-pack: center; } @@ -31,18 +42,6 @@ input[type=button] { pointer-events: none; } -/* TOGGLE */ -#newtab-toggle { - position: absolute; - top: 12px; - right: 12px; -} - -#newtab-toggle:-moz-locale-dir(rtl) { - left: 12px; - right: auto; -} - /* MARGINS */ #newtab-vertical-margin { display: -moz-box; @@ -51,40 +50,52 @@ input[type=button] { -moz-box-orient: vertical; } -#newtab-margin-top { - min-height: 50px; - max-height: 80px; +#newtab-margin-undo-container { + display: -moz-box; + left: 6px; + position: absolute; + top: 6px; + z-index: 1; +} + +#newtab-margin-undo-container:dir(rtl) { + left: auto; + right: 6px; +} + +#newtab-undo-close-button:dir(rtl) { + float:left; +} + +#newtab-horizontal-margin { display: -moz-box; -moz-box-flex: 1; - -moz-box-align: center; - -moz-box-pack: center; } +#newtab-margin-top, #newtab-margin-bottom { - min-height: 40px; - max-height: 100px; + display: -moz-box; + position: relative; +} + +#newtab-margin-top { -moz-box-flex: 1; } -#newtab-horizontal-margin { - display: -moz-box; - -moz-box-flex: 5; +#newtab-margin-bottom { + -moz-box-flex: 2; } .newtab-side-margin { - min-width: 40px; - max-width: 300px; + min-width: 10px; -moz-box-flex: 1; } /* GRID */ #newtab-grid { - display: -moz-box; -moz-box-flex: 5; - -moz-box-orient: vertical; - min-width: 600px; - min-height: 400px; - transition: 100ms ease-out; + overflow: hidden; + transition: 300ms ease-out; transition-property: opacity; } @@ -97,25 +108,25 @@ input[type=button] { pointer-events: none; } -/* ROWS */ -.newtab-row { - display: -moz-box; - -moz-box-orient: horizontal; - -moz-box-direction: normal; - -moz-box-flex: 1; -} - +/* + * If you change the sizes here, make sure you + * change the preferences: + * toolkit.pageThumbs.minWidth + * toolkit.pageThumbs.minHeight + */ /* CELLS */ .newtab-cell { display: -moz-box; - -moz-box-flex: 1; + height: 180px; + margin: 15px 10px 30px; + width: 250px; } /* SITES */ .newtab-site { position: relative; -moz-box-flex: 1; - transition: 100ms ease-out; + transition: 200ms ease-out; transition-property: top, left, opacity; } @@ -139,38 +150,35 @@ input[type=button] { bottom: 0; } -.newtab-thumbnail { - opacity: .8; - transition: opacity 100ms ease-out; -} - -.newtab-thumbnail[dragged], -.newtab-link:-moz-focusring > .newtab-thumbnail, -.newtab-site:hover > .newtab-link > .newtab-thumbnail { - opacity: 1; -} - /* TITLES */ .newtab-title { + overflow: hidden; position: absolute; - left: 0; right: 0; + text-align: center; +} + +.newtab-title { bottom: 0; white-space: nowrap; - overflow: hidden; text-overflow: ellipsis; + vertical-align: middle; +} + +.newtab-title { + left: 0; + padding: 0 4px; } /* CONTROLS */ .newtab-control { position: absolute; - top: 4px; opacity: 0; transition: opacity 100ms ease-out; } .newtab-control:-moz-focusring, -.newtab-site:hover > .newtab-control { +.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control { opacity: 1; } @@ -184,16 +192,6 @@ input[type=button] { } } -.newtab-control-pin:-moz-locale-dir(ltr), -.newtab-control-block:-moz-locale-dir(rtl) { - left: 4px; -} - -.newtab-control-block:-moz-locale-dir(ltr), -.newtab-control-pin:-moz-locale-dir(rtl) { - right: 4px; -} - /* DRAG & DROP */ /* @@ -207,3 +205,124 @@ input[type=button] { background-color: #fff; opacity: 0.01; } + +/* SEARCH */ +#searchContainer { + display: -moz-box; + position: relative; + -moz-box-pack: center; + margin: 40px 0 15px; +} + +#searchContainer[page-disabled] { + opacity: 0; + pointer-events: none; +} + +#searchForm { + display: -moz-box; + position: relative; + height: 36px; + -moz-box-flex: 1; + max-width: 600px; /* 2 * (290 cell width + 10 cell margin) */ +} + +#searchEngineLogo { + border: 1px transparent; + padding: 2px 4px; + margin: 0; + width: 32px; + height: 32px; + position: absolute; +} + +#searchText { + -moz-box-flex: 1; + padding-top: 6px; + padding-bottom: 6px; + padding-inline-start: 42px; + padding-inline-end: 8px; + background: hsla(0,0%,100%,.9) padding-box; + border: 1px solid; + border-spacing: 0; + border-radius: 2px 0 0 2px; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset, + 0 0 2px hsla(210,65%,9%,.1) inset, + 0 1px 0 hsla(0,0%,100%,.2); + color: inherit; + unicode-bidi: plaintext; +} + +#searchText:dir(rtl) { + border-radius: 0 2px 2px 0; +} + +#searchText[aria-expanded="true"] { + border-radius: 2px 0 0 0; +} + +#searchText[aria-expanded="true"]:dir(rtl) { + border-radius: 0 2px 0 0; +} + +#searchText[keepfocus], +#searchText:focus { + border-color: hsla(216,100%,60%,.6) hsla(216,76%,52%,.6) hsla(214,100%,40%,.6); +} + +#searchSubmit { + margin-inline-start: -1px; + padding: 0; + border: 1px solid; + background-color: #e0e0e0; + color: black; + border-color: hsla(220,54%,20%,.15) hsla(220,54%,20%,.17) hsla(220,54%,20%,.2); + border-radius: 0 2px 2px 0; + border-inline-start: 1px solid transparent; + box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset, + 0 1px 0 hsla(0,0%,100%,.2); + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; + width: 50px; +} + +#searchSubmit:dir(rtl) { + border-radius: 2px 0 0 2px; +} + +#searchSubmit:hover { + background-color: hsl(220,54%,20%); + color: white; +} + +#searchText:focus + #searchSubmit, +#searchText + #searchSubmit:hover { + border-color: #5985fc #4573e7 #3264d5; +} + +#searchText:focus + #searchSubmit, +#searchText[keepfocus] + #searchSubmit { + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(220,54%,20%,.03); +} + +#searchText + #searchSubmit:hover { + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(220,54%,20%,.03), + 0 0 4px hsla(216,100%,20%,.2); +} + +#searchText + #searchSubmit:hover:active { + box-shadow: 0 1px 1px hsla(221,79%,6%,.1) inset, + 0 0 1px hsla(221,79%,6%,.2) inset; + transition-duration: 0ms; +} + +.contentSearchSuggestionTable { + font: message-box; + font-size: 16px; +} diff --git a/application/palemoon/base/content/newtab/newTab.js b/application/palemoon/base/content/newtab/newTab.js index bea545ab5..0022f21bb 100644 --- a/application/palemoon/base/content/newtab/newTab.js +++ b/application/palemoon/base/content/newtab/newTab.js @@ -10,6 +10,7 @@ var Ci = Components.interfaces; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PageThumbs.jsm"); +Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm"); Cu.import("resource://gre/modules/NewTabUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Rect", @@ -31,13 +32,24 @@ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() { createBundle("chrome://browser/locale/newTab.properties"); }); -function newTabString(name) gStringBundle.GetStringFromName('newtab.' + name); +function newTabString(name, args) { + let stringName = "newtab." + name; + if (!args) { + return gStringBundle.GetStringFromName(stringName); + } + return gStringBundle.formatStringFromName(stringName, args, args.length); +} function inPrivateBrowsingMode() { - return PrivateBrowsingUtils.isWindowPrivate(window); + return PrivateBrowsingUtils.isContentWindowPrivate(window); } const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; +const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const TILES_EXPLAIN_LINK = "https://support.mozilla.org/kb/how-do-tiles-work-firefox"; +const TILES_INTRO_LINK = "https://www.mozilla.org/firefox/tiles/"; +const TILES_PRIVACY_LINK = "https://www.mozilla.org/privacy/"; #include transformations.js #include page.js @@ -51,6 +63,7 @@ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; #include dropPreview.js #include updater.js #include undo.js +#include search.js // Everything is loaded. Initialize the New Tab Page. gPage.init(); diff --git a/application/palemoon/base/content/newtab/newTab.xhtml b/application/palemoon/base/content/newtab/newTab.xhtml new file mode 100644 index 000000000..eac62c987 --- /dev/null +++ b/application/palemoon/base/content/newtab/newTab.xhtml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd"> + %newTabDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> + %browserDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&newtab.pageTitle;</title> + + <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/newtab/newTab.css" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/newtab/newTab.css" /> +</head> + +<body dir="&locale.dir;"> + <div id="newtab-vertical-margin"> + <div id="newtab-margin-top"/> + + <div id="newtab-margin-undo-container"> + <div id="newtab-undo-container" undo-disabled="true"> + <label id="newtab-undo-label">&newtab.undo.removedLabel;</label> + <button id="newtab-undo-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.undoButton;</button> + <button id="newtab-undo-restore-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.restoreButton;</button> + <button id="newtab-undo-close-button" tabindex="-1" title="&newtab.undo.closeTooltip;"/> + </div> + </div> + + <div id="searchContainer"> + <form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)"> + <div id="searchLogoContainer"><img id="searchEngineLogo"/></div> + <input type="text" name="q" value="" id="searchText" maxlength="256"/> + <input id="searchSubmit" type="submit" value="&newtab.searchEngineButton.label;"/> + </form> + </div> + + <div id="newtab-horizontal-margin"> + <div class="newtab-side-margin"/> + <div id="newtab-grid"> + <!-- site grid --> + </div> + <div class="newtab-side-margin"/> + </div> + + <div id="newtab-margin-bottom"/> + </div> +</body> +<script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/> +</html> diff --git a/application/palemoon/base/content/newtab/newTab.xul b/application/palemoon/base/content/newtab/newTab.xul deleted file mode 100644 index 6fc202f29..000000000 --- a/application/palemoon/base/content/newtab/newTab.xul +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?> -<?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?> - -<!DOCTYPE window [ - <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd"> - %newTabDTD; -]> - -<xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - title="&newtab.pageTitle;"> - - <div id="newtab-scrollbox"> - - <div id="newtab-vertical-margin"> - <div id="newtab-margin-top"> - <div id="newtab-undo-container" undo-disabled="true"> - <xul:label id="newtab-undo-label" - value="&newtab.undo.removedLabel;" /> - <xul:button id="newtab-undo-button" tabindex="-1" - label="&newtab.undo.undoButton;" - class="newtab-undo-button" /> - <xul:button id="newtab-undo-restore-button" tabindex="-1" - label="&newtab.undo.restoreButton;" - class="newtab-undo-button" /> - <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1" - class="close-icon" - tooltiptext="&newtab.undo.closeTooltip;" /> - </div> - </div> - - <div id="newtab-horizontal-margin"> - <div class="newtab-side-margin"/> - - <div id="newtab-grid"> - </div> - - <div class="newtab-side-margin"/> - </div> - - <div id="newtab-margin-bottom"/> - </div> - <input id="newtab-toggle" type="button"/> - </div> - - <xul:script type="text/javascript;version=1.8" - src="chrome://browser/content/newtab/newTab.js"/> -</xul:window> diff --git a/application/palemoon/base/content/newtab/page.js b/application/palemoon/base/content/newtab/page.js index fc836a55e..cbd6750b6 100644 --- a/application/palemoon/base/content/newtab/page.js +++ b/application/palemoon/base/content/newtab/page.js @@ -4,6 +4,9 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ #endif +// The amount of time we wait while coalescing updates for hidden pages. +const SCHEDULE_UPDATE_TIMEOUT_MS = 1000; + /** * This singleton represents the whole 'New Tab Page' and takes care of * initializing all its components. @@ -19,9 +22,10 @@ var gPage = { // Listen for 'unload' to unregister this page. addEventListener("unload", this, false); - // Listen for toggle button clicks. - let button = document.getElementById("newtab-toggle"); - button.addEventListener("click", this, false); + // XXX bug 991111 - Not all click events are correctly triggered when + // listening from xhtml nodes -- in particular middle clicks on sites, so + // listen from the xul window and filter then delegate + addEventListener("click", this, false); // Check if the new tab feature is enabled. let enabled = gAllPages.enabled; @@ -34,26 +38,65 @@ var gPage = { /** * Listens for notifications specific to this page. */ - observe: function Page_observe() { - let enabled = gAllPages.enabled; - this._updateAttributes(enabled); + observe: function Page_observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + let enabled = gAllPages.enabled; + this._updateAttributes(enabled); - // Initialize the whole page if we haven't done that, yet. - if (enabled) { - this._init(); - } else { - gUndoDialog.hide(); + // Update thumbnails to the new enhanced setting + if (aData == "browser.newtabpage.enhanced") { + this.update(); + } + + // Initialize the whole page if we haven't done that, yet. + if (enabled) { + this._init(); + } else { + gUndoDialog.hide(); + } + } else if (aTopic == "page-thumbnail:create" && gGrid.ready) { + for (let site of gGrid.sites) { + if (site && site.url === aData) { + site.refreshThumbnail(); + } + } } }, /** - * Updates the whole page and the grid when the storage has changed. + * Updates the page's grid right away for visible pages. If the page is + * currently hidden, i.e. in a background tab or in the preloader, then we + * batch multiple update requests and refresh the grid once after a short + * delay. Accepts a single parameter the specifies the reason for requesting + * a page update. The page may decide to delay or prevent a requested updated + * based on the given reason. */ - update: function Page_update() { - // The grid might not be ready yet as we initialize it asynchronously. - if (gGrid.ready) { - gGrid.refresh(); + update(reason = "") { + // Update immediately if we're visible. + if (!document.hidden) { + // Ignore updates where reason=links-changed as those signal that the + // provider's set of links changed. We don't want to update visible pages + // in that case, it is ok to wait until the user opens the next tab. + if (reason != "links-changed" && gGrid.ready) { + gGrid.refresh(); + } + + return; + } + + // Bail out if we scheduled before. + if (this._scheduleUpdateTimeout) { + return; } + + this._scheduleUpdateTimeout = setTimeout(() => { + // Refresh if the grid is ready. + if (gGrid.ready) { + gGrid.refresh(); + } + + this._scheduleUpdateTimeout = null; + }, SCHEDULE_UPDATE_TIMEOUT_MS); }, /** @@ -66,19 +109,28 @@ var gPage = { this._initialized = true; - gLinks.populateCache(function () { - // Initialize and render the grid. - gGrid.init(); + // Set submit button label for when CSS background are disabled (e.g. + // high contrast mode). + document.getElementById("searchSubmit").value = + document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0"; + + if (document.hidden) { + addEventListener("visibilitychange", this); + } else { + setTimeout(() => this.onPageFirstVisible()); + } + + // Initialize and render the grid. + gGrid.init(); - // Initialize the drop target shim. - gDropTargetShim.init(); + // Initialize the drop target shim. + gDropTargetShim.init(); #ifdef XP_MACOSX - // Workaround to prevent a delay on MacOSX due to a slow drop animation. - document.addEventListener("dragover", this, false); - document.addEventListener("drop", this, false); + // Workaround to prevent a delay on MacOSX due to a slow drop animation. + document.addEventListener("dragover", this, false); + document.addEventListener("drop", this, false); #endif - }.bind(this)); }, /** @@ -87,7 +139,7 @@ var gPage = { */ _updateAttributes: function Page_updateAttributes(aValue) { // Set the nodes' states. - let nodeSelector = "#newtab-scrollbox, #newtab-toggle, #newtab-grid"; + let nodeSelector = "#newtab-grid, #searchContainer"; for (let node of document.querySelectorAll(nodeSelector)) { if (aValue) node.removeAttribute("page-disabled"); @@ -98,15 +150,28 @@ var gPage = { // Enables/disables the control and link elements. let inputSelector = ".newtab-control, .newtab-link"; for (let input of document.querySelectorAll(inputSelector)) { - if (aValue) + if (aValue) input.removeAttribute("tabindex"); else input.setAttribute("tabindex", "-1"); } + }, - // Update the toggle button's title. - let toggle = document.getElementById("newtab-toggle"); - toggle.setAttribute("title", newTabString(aValue ? "hide" : "show")); + /** + * Handles unload event + */ + _handleUnloadEvent: function Page_handleUnloadEvent() { + gAllPages.unregister(this); + // compute page life-span and send telemetry probe: using milli-seconds will leave + // many low buckets empty. Instead we use half-second precision to make low end + // of histogram linear and not lose the change in user attention + let delta = Math.round((Date.now() - this._firstVisibleTime) / 500); + if (this._suggestedTilePresent) { + Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN_SUGGESTED").add(delta); + } + else { + Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN").add(delta); + } }, /** @@ -114,11 +179,22 @@ var gPage = { */ handleEvent: function Page_handleEvent(aEvent) { switch (aEvent.type) { + case "load": + this.onPageVisibleAndLoaded(); + break; case "unload": - gAllPages.unregister(this); + this._handleUnloadEvent(); break; case "click": - gAllPages.enabled = !gAllPages.enabled; + let {button, target} = aEvent; + // Go up ancestors until we find a Site or not + while (target) { + if (target.hasOwnProperty("_newtabSite")) { + target._newtabSite.onClick(aEvent); + break; + } + target = target.parentNode; + } break; case "dragover": if (gDrag.isValid(aEvent) && gDrag.draggedSite) @@ -130,6 +206,78 @@ var gPage = { aEvent.stopPropagation(); } break; + case "visibilitychange": + // Cancel any delayed updates for hidden pages now that we're visible. + if (this._scheduleUpdateTimeout) { + clearTimeout(this._scheduleUpdateTimeout); + this._scheduleUpdateTimeout = null; + + // An update was pending so force an update now. + this.update(); + } + + setTimeout(() => this.onPageFirstVisible()); + removeEventListener("visibilitychange", this); + break; + } + }, + + onPageFirstVisible: function () { + // Record another page impression. + Services.telemetry.getHistogramById("NEWTAB_PAGE_SHOWN").add(true); + + for (let site of gGrid.sites) { + if (site) { + // The site may need to modify and/or re-render itself if + // something changed after newtab was created by preloader. + // For example, the suggested tile endTime may have passed. + site.onFirstVisible(); + } + } + + // save timestamp to compute page life-span delta + this._firstVisibleTime = Date.now(); + + if (document.readyState == "complete") { + this.onPageVisibleAndLoaded(); + } else { + addEventListener("load", this); } - } + }, + + onPageVisibleAndLoaded() { + // Send the index of the last visible tile. + this.reportLastVisibleTileIndex(); + // Maybe tell the user they can undo an initial automigration + this.maybeShowAutoMigrationUndoNotification(); + }, + + reportLastVisibleTileIndex() { + let cwu = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let rect = cwu.getBoundsWithoutFlushing(gGrid.node); + let nodes = cwu.nodesFromRect(rect.left, rect.top, 0, rect.width, + rect.height, 0, true, false); + + let i = -1; + let lastIndex = -1; + let sites = gGrid.sites; + + for (let node of nodes) { + if (node.classList && node.classList.contains("newtab-cell")) { + if (sites[++i]) { + lastIndex = i; + if (sites[i].link.targetedSite) { + // record that suggested tile is shown to use suggested-tiles-histogram + this._suggestedTilePresent = true; + } + } + } + } + }, + + maybeShowAutoMigrationUndoNotification() { + // sendAsyncMessage("NewTab:MaybeShowAutoMigrationUndoNotification"); + }, }; diff --git a/application/palemoon/base/content/newtab/search.js b/application/palemoon/base/content/newtab/search.js new file mode 100644 index 000000000..8bc959eee --- /dev/null +++ b/application/palemoon/base/content/newtab/search.js @@ -0,0 +1,134 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +const SEARCH_ENGINES = { + "DuckDuckGo": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAACT1BMVEXvISn/////9/fvUlr3ra3/" + + "zs7/7+/va2v/5+f/xsbvMTn/tbX/3t7/vb3vOUL3WmPvQkr/zgDvKTHvSlL3hIT3paX/1tbnISn3" + + "c3v3e3v3a3P3jIz3nJz/tb33c3PvKSn3lJT39/cAc73vSkr3e4Tv7+/3Yxj3pa3/tQj3jJT3nKX3" + + "Y2P/xs73hIzvQkL/vQjvQiHn5+f3hBD/ztbvMTH/vcb/3ucIc733lJz/pQilzufe7/fvMSHOzs73" + + "//cQrUpKvVprxmP3Y2vvShiUzmvWlJRzzmMYtUrvOTnn7/davVrWra3v9//nY2PvISGUxudztd7e" + + "3t7/76XvKSHea2v/xgDnOUK93vfW5/f/1t73Uhj/52ut3q2l3rXO784pjMZrrdb/rQjera3/5+/e" + + "paWMxufO79aEazkYrUr/nAj3jBD3axj3lBD///fehIRKpd7/1hCEYzk5vVL3//8ptVLW77UxtVLn" + + "SlLW1tZCvVp7vef/1gj/3invSkL//+fWtbXvpaX/3kr/97XvnJznWmMxjM5zvefOxsbWnKXWjIzG" + + "3u/ea3Pn997O5/fnQkqExuf3Whit1u/nUlrnxs7v5+d7zmuU1pT3exDOSjFjrVL/987/pUoQe8b/" + + "75T/3jFKxnO158bWKSl7zoRSxmtajEK1e0pzxlqcUjH/1iHOMSnOvb33cxDWnJx7td6EzmP/74xz" + + "azlrcznec3Pe771jxlpzczne78YpvVqEvWPn99YxvWOtSjHee3vG787OOTE5lEK1QjHv9+drzmve" + + "tbXO772q+r8wAAAFbUlEQVR4Xo2X84PzTBDHN3Zqu2fbemzbNl7atm3btvGHvTNJ2myuyd3NL2mT" + + "zmdnvjM76RImyGQlH5dCHBeSmscNmQkyfwBrZMLEY2aRF5cMSDYPEx+LZpUlAYRQbVEpnuc1je/M" + + "SbVwYoVFAbpE0IaLmiwqiVymmE3H84YuGs2mheCEhQH5qPUrje2ONxHKVIkXR2x2MxsMkDnLvftk" + + "2fSTQNCzSAgngwCCipkXxHiU+BsnCDFE8f6AQgnwaTGhkmDLymW8jPsBeIsth8iCpha618El1wgo" + + "4FOhWyWLWY+O8pbnAwTI29S1ElncJBmF4L0AGeJSdR4dUpt5w+DL0nAgoUuGGKKCBxDCOxrykaDb" + + "+yFQjhUylLlXpAB5jGnIqV6uvvWUcAAhLmDBXIAMrkXRdHQ+cerUiWefq1hRrAgg8LikUgdkQUAx" + + "6+2Ze0WLEO/1BQzrHCFNrAPAeDSD4q/Ln6R3p68MSYzDAUiwIEutJM0bHXE/gpEhJMxaAB3T6aT8" + + "mfkm+QBiMlwKFqAHvrHu9tvTOLrEdX4hFAkJWQB42qbVyam75ruv3zvF+wBCKJ0MAAV6SAy5+raA" + + "y+lb9tYBUw9sffKRJh+CDl2SAEAPquaC76swU1c+zlxbA9if/EIY78AcCBODDKjnVzDM0+sb57zq" + + "N14gdpbg4nraBaxm3NWpIDKNgJIIDTxEAKMyVM9/VrFcpijK52PbNhmk0RQORCA8dhGhIkDA+qPV" + + "Y/U8No2NHZsUfQCdzYTECSiRSRJKgxYAnK6+tnVrPYL7q2P7GNNnT0L3SQSS61AowK4BAExWq9XJ" + + "OmDT5D4GtUab7p92W1aD6AFBOjUKcONNKMG2o9vmScmhd+v5SCTS91StDLBwmHR5q0iiM4yv3X5g" + + "sD1i24tUHc0GQOrOihdw+ZV7drx+8I1IzfpaCQ1oSIGsbqEBdxy8KkLb8dYt7m7AFBpEJI8OUIAd" + + "Hve+wX509IqYgzLqxKMi5X+r6737wgHfMrZBKGwpQMWP0PN8/8qLn15cSRosEQeI3coxGrzRVfE2" + + "BEyTAMNpmbA3k2erPOyq+CUCPGvv3OmGykYBQhiYFbynDLu2uyW826qb7bSlv/VCe2R3vQqhIYQQ" + + "nLmSGKUAT1AqXn7V6p72iUsTThsNuhKUAeKMNFaiW2nG08H90IF1m6DywVdsHgA4bPgRGgAqUgBr" + + "DwxOtPcdv9RK6yklnaGKOXBMmN7RVCtJJMiUdG2s78dv9HbY7KrI9AQBOHwjaxaA6cKhRLXCHkpF" + + "PrAJYBz1su7LtSBQIjzozgI5AJDWsQ7gTJxETTHuEh5yW8kR5+1fvQBT5PDdWgPokE6GSuK3Aaby" + + "2KwNyGFIZ8/NfexVMAGXEfe8MA5QTVdrgGe2M9evev6FMwiAYr308nVzcx/SgHwSlswyLgDLHU0K" + + "tX5UZwCwZsM1b7516J1333v/g2UAuJoCNMsmZkEDZBXujCoOIfVJxQKsvXnDshvWfrEcAV9RAoqY" + + "rfdvHjY06R3tVmtjzQYsQ8ByC/C1O0dEzqkAGqELbiZ1W/RvBr51Ad9ZgO8dQCkh4/q5xvMC6hot" + + "sBl7rP1QT+HHQz9RGoSHhkyMgqEBdNPFWSWMY+1nBPxy+MjvZ2aZxB9n/zz3FwKiOTZfotb3AhhF" + + "xSUUNmGSjX+vWvPPYacVWJOkUilUT05ymEVb0JFHj9l/AVn+35b/jsx6YzNz8mja+iAEH7rYDntY" + + "Gaz3dizW080KWaeICx77kiG7lTKG6EEoPb0Wu0lZ9OA5whFH8GxHQjOMQls5HSs5t/glHX2FYtT/" + + "mGAs/fCtFU0vQJUSQYfvIBvVyukuLhbjuood/H6WCbD/AQSFvIO3JDxgAAAAAElFTkSuQmCC" + } +}; + +// This global tracks if the page has been set up before, to prevent double inits +var gInitialized = false; +var gObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "searchEngineURL") { + setupSearchEngine(); + if (!gInitialized) { + gInitialized = true; + } + return; + } + } +}); + +window.addEventListener("pageshow", function () { + window.gObserver.observe(document.documentElement, { attributes: true }); +}); + +window.addEventListener("pagehide", function() { + window.gObserver.disconnect(); +}); + +function onSearchSubmit(aEvent) { + let searchTerms = document.getElementById("searchText").value; + let searchURL = document.documentElement.getAttribute("searchEngineURL"); + + if (searchURL && searchTerms.length > 0) { + const SEARCH_TOKEN = "_searchTerms_"; + let searchPostData = document.documentElement.getAttribute("searchEnginePostData"); + if (searchPostData) { + // Check if a post form already exists. If so, remove it. + const POST_FORM_NAME = "searchFormPost"; + let form = document.forms[POST_FORM_NAME]; + if (form) { + form.parentNode.removeChild(form); + } + + // Create a new post form. + form = document.body.appendChild(document.createElement("form")); + form.setAttribute("name", POST_FORM_NAME); + // Set the URL to submit the form to. + form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms)); + form.setAttribute("method", "post"); + + // Create new <input type=hidden> elements for search param. + searchPostData = searchPostData.split("&"); + for (let postVar of searchPostData) { + let [name, value] = postVar.split("="); + if (value == SEARCH_TOKEN) { + value = searchTerms; + } + let input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", name); + input.setAttribute("value", value); + form.appendChild(input); + } + // Submit the form. + form.submit(); + } else { + searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms)); + window.location.href = searchURL; + } + } + + aEvent.preventDefault(); +} + + +function setupSearchEngine() { + let searchText = document.getElementById("searchText"); + let searchEngineName = document.documentElement.getAttribute("searchEngineName"); + let searchEngineInfo = SEARCH_ENGINES[searchEngineName]; + let logoElt = document.getElementById("searchEngineLogo"); + + // Add search engine logo. + if (searchEngineInfo && searchEngineInfo.image) { + logoElt.parentNode.hidden = false; + logoElt.src = searchEngineInfo.image; + logoElt.alt = searchEngineName; + searchText.placeholder = ""; + } else { + logoElt.parentNode.hidden = true; + searchText.placeholder = searchEngineName; + } +} diff --git a/application/palemoon/base/content/newtab/sites.js b/application/palemoon/base/content/newtab/sites.js index 873ef201c..a368146bb 100644 --- a/application/palemoon/base/content/newtab/sites.js +++ b/application/palemoon/base/content/newtab/sites.js @@ -4,6 +4,9 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ #endif +const THUMBNAIL_PLACEHOLDER_ENABLED = + Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder"); + /** * This class represents a site that is contained in a cell and can be pinned, * moved around or deleted. @@ -37,7 +40,7 @@ Site.prototype = { /** * The title of the site's link. */ - get title() { return this.link.title; }, + get title() { return this.link.title || this.link.url; }, /** * The site's parent cell. @@ -50,13 +53,19 @@ Site.prototype = { /** * Pins the site on its current or a given index. * @param aIndex The pinned index (optional). + * @return true if link changed type after pin */ pin: function Site_pin(aIndex) { if (typeof aIndex == "undefined") aIndex = this.cell.index; this._updateAttributes(true); - gPinnedLinks.pin(this._link, aIndex); + let changed = gPinnedLinks.pin(this._link, aIndex); + if (changed) { + // render site again + this._render(); + } + return changed; }, /** @@ -108,33 +117,145 @@ Site.prototype = { let control = this._querySelector(".newtab-control-pin"); if (aPinned) { - control.setAttribute("pinned", true); + this.node.setAttribute("pinned", true); control.setAttribute("title", newTabString("unpin")); } else { - control.removeAttribute("pinned"); + this.node.removeAttribute("pinned"); control.setAttribute("title", newTabString("pin")); } }, + _newTabString: function(str, substrArr) { + let regExp = /%[0-9]\$S/g; + let matches; + while ((matches = regExp.exec(str))) { + let match = matches[0]; + let index = match.charAt(1); // Get the digit in the regExp. + str = str.replace(match, substrArr[index - 1]); + } + return str; + }, + + _getSuggestedTileExplanation: function() { + let targetedName = `<strong> ${this.link.targetedName} </strong>`; + let targetedSite = `<strong> ${this.link.targetedSite} </strong>`; + if (this.link.explanation) { + return this._newTabString(this.link.explanation, [targetedName, targetedSite]); + } + return newTabString("suggested.button", [targetedName]); + }, + + /** + * Checks for and modifies link at campaign end time + */ + _checkLinkEndTime: function Site_checkLinkEndTime() { + if (this.link.endTime && this.link.endTime < Date.now()) { + let oldUrl = this.url; + // chop off the path part from url + this.link.url = Services.io.newURI(this.url, null, null).resolve("/"); + // clear supplied images - this triggers thumbnail download for new url + delete this.link.imageURI; + delete this.link.enhancedImageURI; + // remove endTime to avoid further time checks + delete this.link.endTime; + // clear enhanced-content image that may still exist in preloaded page + this._querySelector(".enhanced-content").style.backgroundImage = ""; + gPinnedLinks.replace(oldUrl, this.link); + } + }, + /** * Renders the site's data (fills the HTML fragment). */ _render: function Site_render() { + // first check for end time, as it may modify the link + this._checkLinkEndTime(); + // setup display variables let url = this.url; - let title = this.title || url; - let tooltip = (title == url ? title : title + "\n" + url); + let title = this.link.type == "history" ? this.link.baseDomain : + this.title; + let tooltip = (this.title == url ? this.title : this.title + "\n" + url); let link = this._querySelector(".newtab-link"); link.setAttribute("title", tooltip); link.setAttribute("href", url); - this._querySelector(".newtab-title").textContent = title; + this.node.setAttribute("type", this.link.type); + + let titleNode = this._querySelector(".newtab-title"); + titleNode.textContent = title; + if (this.link.titleBgColor) { + titleNode.style.backgroundColor = this.link.titleBgColor; + } if (this.isPinned()) this._updateAttributes(true); + // Capture the page if the thumbnail is missing, which will cause page.js + // to be notified and call our refreshThumbnail() method. + this.captureIfMissing(); + // but still display whatever thumbnail might be available now. + this.refreshThumbnail(); + }, + + /** + * Called when the site's tab becomes visible for the first time. + * Since the newtab may be preloaded long before it's displayed, + * check for changed conditions and re-render if needed + */ + onFirstVisible: function Site_onFirstVisible() { + if (this.link.endTime && this.link.endTime < Date.now()) { + // site needs to change landing url and background image + this._render(); + } + else { + this.captureIfMissing(); + } + }, + + /** + * Captures the site's thumbnail in the background, but only if there's no + * existing thumbnail and the page allows background captures. + */ + captureIfMissing: function Site_captureIfMissing() { + if (!document.hidden && !this.link.imageURI) { + BackgroundPageThumbs.captureIfMissing(this.url); + } + }, - let thumbnailURL = PageThumbs.getThumbnailURL(this.url); - let thumbnail = this._querySelector(".newtab-thumbnail"); - thumbnail.style.backgroundImage = "url(" + thumbnailURL + ")"; + /** + * Refreshes the thumbnail for the site. + */ + refreshThumbnail: function Site_refreshThumbnail() { + let link = this.link; + + let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail"); + if (link.bgColor) { + thumbnail.style.backgroundColor = link.bgColor; + } + let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url); + thumbnail.style.backgroundImage = 'url("' + uri + '")'; + + if (THUMBNAIL_PLACEHOLDER_ENABLED && + link.type == "history" && + link.baseDomain) { + let placeholder = this._querySelector(".newtab-thumbnail.placeholder"); + let charCodeSum = 0; + for (let c of link.baseDomain) { + charCodeSum += c.charCodeAt(0); + } + const COLORS = 16; + let hue = Math.round((charCodeSum % COLORS) / COLORS * 360); + placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)"; + placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase(); + } + }, + + _ignoreHoverEvents: function(element) { + element.addEventListener("mouseover", () => { + this.cell.node.setAttribute("ignorehover", "true"); + }); + element.addEventListener("mouseout", () => { + this.cell.node.removeAttribute("ignorehover"); + }); }, /** @@ -145,10 +266,6 @@ Site.prototype = { this._node.addEventListener("dragstart", this, false); this._node.addEventListener("dragend", this, false); this._node.addEventListener("mouseover", this, false); - - let controls = this.node.querySelectorAll(".newtab-control"); - for (let i = 0; i < controls.length; i++) - controls[i].addEventListener("click", this, false); }, /** @@ -157,7 +274,75 @@ Site.prototype = { _speculativeConnect: function Site_speculativeConnect() { let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); let uri = Services.io.newURI(this.url, null, null); - sc.speculativeConnect(uri, null); + try { + // This can throw for certain internal URLs, when they wind up in + // about:newtab. Be sure not to propagate the error. + sc.speculativeConnect(uri, null); + } catch (e) {} + }, + + /** + * Record interaction with site using telemetry. + */ + _recordSiteClicked: function Site_recordSiteClicked(aIndex) { + if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") || + Services.prefs.prefHasUserValue("browser.newtabpage.columns") || + aIndex > 8) { + // We only want to get indices for the default configuration, everything + // else goes in the same bucket. + aIndex = 9; + } + Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED") + .add(aIndex); + }, + + _toggleLegalText: function(buttonClass, explanationTextClass) { + let button = this._querySelector(buttonClass); + if (button.hasAttribute("active")) { + let explain = this._querySelector(explanationTextClass); + explain.parentNode.removeChild(explain); + + button.removeAttribute("active"); + } + }, + + /** + * Handles site click events. + */ + onClick: function Site_onClick(aEvent) { + let action; + let pinned = this.isPinned(); + let tileIndex = this.cell.index; + let {button, target} = aEvent; + + // Handle tile/thumbnail link click + if (target.classList.contains("newtab-link") || + target.parentElement.classList.contains("newtab-link")) { + // Record for primary and middle clicks + if (button == 0 || button == 1) { + this._recordSiteClicked(tileIndex); + action = "click"; + } + } + // Only handle primary clicks for the remaining targets + else if (button == 0) { + aEvent.preventDefault(); + if (target.classList.contains("newtab-control-block")) { + this.block(); + action = "block"; + } + else if (pinned && target.classList.contains("newtab-control-pin")) { + this.unpin(); + action = "unpin"; + } + else if (!pinned && target.classList.contains("newtab-control-pin")) { + if (this.pin()) { + // suggested link has changed - update rest of the pages + gAllPages.update(gPage); + } + action = "pin"; + } + } }, /** @@ -165,15 +350,6 @@ Site.prototype = { */ handleEvent: function Site_handleEvent(aEvent) { switch (aEvent.type) { - case "click": - aEvent.preventDefault(); - if (aEvent.target.classList.contains("newtab-control-block")) - this.block(); - else if (this.isPinned()) - this.unpin(); - else - this.pin(); - break; case "mouseover": this._node.removeEventListener("mouseover", this, false); this._speculativeConnect(); @@ -181,9 +357,6 @@ Site.prototype = { case "dragstart": gDrag.start(this, aEvent); break; - case "drag": - gDrag.drag(this, aEvent); - break; case "dragend": gDrag.end(this, aEvent); break; diff --git a/application/palemoon/base/content/newtab/transformations.js b/application/palemoon/base/content/newtab/transformations.js index 978116182..f7db0ad84 100644 --- a/application/palemoon/base/content/newtab/transformations.js +++ b/application/palemoon/base/content/newtab/transformations.js @@ -156,7 +156,7 @@ var gTransformation = { finish(); } else { this.setSitePosition(aSite, targetPosition); - this._whenTransitionEnded(aSite.node, finish); + this._whenTransitionEnded(aSite.node, ["left", "top"], finish); } }, @@ -181,13 +181,13 @@ var gTransformation = { batch.push(new Promise(resolve => { if (!cells[aIndex]) { - // The site disappeared from the grid, hide it. + // The site disappeared from the grid, hide it. this.hideSite(aSite, resolve); } else if (this._getNodeOpacity(aSite.node) != 1) { - // The site disappeared before but is now back, show it. + // The site disappeared before but is now back, show it. this.showSite(aSite, resolve); } else { - // The site's position has changed, move it around. + // The site's position has changed, move it around. this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: resolve}); } })); @@ -202,15 +202,19 @@ var gTransformation = { * Listens for the 'transitionend' event on a given node and calls the given * callback. * @param aNode The node that is transitioned. + * @param aProperties The properties we'll wait to be transitioned. * @param aCallback The callback to call when finished. */ _whenTransitionEnded: - function Transformation_whenTransitionEnded(aNode, aCallback) { + function Transformation_whenTransitionEnded(aNode, aProperties, aCallback) { - aNode.addEventListener("transitionend", function onEnd() { - aNode.removeEventListener("transitionend", onEnd, false); - aCallback(); - }, false); + let props = new Set(aProperties); + aNode.addEventListener("transitionend", function onEnd(e) { + if (props.has(e.propertyName)) { + aNode.removeEventListener("transitionend", onEnd); + aCallback(); + } + }); }, /** @@ -236,8 +240,9 @@ var gTransformation = { if (aCallback) aCallback(); } else { - if (aCallback) - this._whenTransitionEnded(aNode, aCallback); + if (aCallback) { + this._whenTransitionEnded(aNode, ["opacity"], aCallback); + } aNode.style.opacity = aOpacity; } |