☠☠ backed out by 6f36188ad818 ☠ ☠ | |
author | Blair McBride <bmcbride@mozilla.com> |
Thu, 19 Feb 2015 20:35:10 +1300 | |
changeset 229908 | 9b07a29dcbdd390de5d6f0384cc36648ef81604c |
parent 229907 | 5dfb417f346e034057e3e76547169445d2aa21b8 |
child 229909 | 6f36188ad81825449c619b7fb1cf320b47512724 |
push id | 28301 |
push user | ryanvm@gmail.com |
push date | Thu, 19 Feb 2015 23:57:30 +0000 |
treeherder | mozilla-central@51458a066fda [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | florian |
bugs | 1123517 |
milestone | 38.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1851,8 +1851,11 @@ pref("dom.ipc.processHangMonitor", true) // debugger is attached. pref("dom.ipc.reportProcessHangs", false); #else pref("dom.ipc.reportProcessHangs", true); #endif // Disable reader mode by default. pref("reader.parse-on-load.enabled", false); + +// Disable ReadingList by default. +pref("browser.readinglist.enabled", false);
--- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -218,16 +218,21 @@ <menupopup id="viewSidebarMenu"> <menuitem id="menu_bookmarksSidebar" key="viewBookmarksSidebarKb" observes="viewBookmarksSidebar"/> <menuitem id="menu_historySidebar" key="key_gotoHistory" observes="viewHistorySidebar" label="&historyButton.label;"/> + <menuitem id="menu_readingListSidebar" + key="key_readingListSidebar" + observes="readingListSidebar" + label="&readingList.label;"/> + <!-- Service providers with sidebars are inserted between these two menuseperators --> <menuseparator hidden="true"/> <menuseparator class="social-provider-menu" hidden="true"/> </menupopup> </menu> <menuseparator/> <menu id="viewFullZoomMenu" label="&fullZoom.label;" accesskey="&fullZoom.accesskey;"
new file mode 100644 --- /dev/null +++ b/browser/base/content/browser-readinglist.js @@ -0,0 +1,68 @@ +/* +# 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/. +*/ + +let ReadingListUI = { + /** + * Initialize the ReadingList UI. + */ + init() { + Preferences.observe("browser.readinglist.enabled", this.updateUI, this); + this.updateUI(); + }, + + /** + * Un-initialize the ReadingList UI. + */ + uninit() { + Preferences.ignore("browser.readinglist.enabled", this.updateUI, this); + }, + + /** + * Whether the ReadingList feature is enabled or not. + * @type {boolean} + */ + get enabled() { + return Preferences.get("browser.readinglist.enabled", false); + }, + + /** + * Whether the ReadingList sidebar is currently open or not. + * @type {boolean} + */ + get isSidebarOpen() { + return SidebarUI.isOpen && SidebarUI.currentID == "readingListSidebar"; + }, + + /** + * Update the UI status, ensuring the UI is shown or hidden depending on + * whether the feature is enabled or not. + */ + updateUI() { + let enabled = this.enabled; + if (!enabled) { + this.hideSidebar(); + } + + document.getElementById("readingListSidebar").setAttribute("hidden", !enabled); + }, + + /** + * Show the ReadingList sidebar. + * @return {Promise} + */ + showSidebar() { + return SidebarUI.show("readingListSidebar"); + }, + + /** + * Hide the ReadingList sidebar, if it is currently shown. + */ + hideSidebar() { + if (this.isSidebarOpen) { + SidebarUI.hide(); + } + }, +};
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -143,16 +143,21 @@ <!-- for both places and non-places, the sidebar lives at chrome://browser/content/history/history-panel.xul so there are no problems when switching between versions --> <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/history/history-panel.xul" oncommand="SidebarUI.toggle('viewHistorySidebar');"/> + <broadcaster id="readingListSidebar" hidden="true" autoCheck="false" + sidebartitle="&readingList.label;" type="checkbox" group="sidebar" + sidebarurl="chrome://browser/content/readinglist/sidebar.xhtml" + oncommand="SidebarUI.toggle('readingListSidebar');"/> + <broadcaster id="viewWebPanelsSidebar" autoCheck="false" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul" oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/> <broadcaster id="bookmarkThisPageBroadcaster" label="&bookmarkThisPageCmd.label;" bookmarklabel="&bookmarkThisPageCmd.label;" editlabel="&editThisBookmarkCmd.label;"/> @@ -413,16 +418,21 @@ key="&historySidebarCmd.commandKey;" #ifdef XP_MACOSX modifiers="accel,shift" #else modifiers="accel" #endif command="viewHistorySidebar"/> + <key id="key_readingListSidebar" + key="&readingList.sidebar.commandKey;" + modifiers="accel,alt" + command="readingListSidebar"/> + <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/>
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -8,16 +8,18 @@ let Cu = Components.utils; let Cc = Components.classes; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/NotificationDB.jsm"); Cu.import("resource:///modules/RecentWindow.jsm"); Cu.import("resource://gre/modules/WindowsPrefSync.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", "resource://gre/modules/Deprecated.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", "resource:///modules/E10SUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"); @@ -220,16 +222,17 @@ let gInitialPages = [ #include browser-eme.js #include browser-feeds.js #include browser-fullScreen.js #include browser-fullZoom.js #include browser-gestureSupport.js #include browser-loop.js #include browser-places.js #include browser-plugins.js +#include browser-readinglist.js #include browser-safebrowsing.js #include browser-sidebar.js #include browser-social.js #include browser-tabview.js #include browser-thumbnails.js #ifdef MOZ_DATA_REPORTING #include browser-data-submission-info-bar.js @@ -1366,16 +1369,17 @@ var gBrowserInit = { return; } // Enable the Restore Last Session command if needed RestoreLastSessionObserver.init(); SocialUI.init(); TabView.init(); + ReadingListUI.init(); // Telemetry for master-password - we do this after 5 seconds as it // can cause IO if NSS/PSM has not already initialized. setTimeout(() => { if (window.closed) { return; } let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"] @@ -1474,16 +1478,18 @@ var gBrowserInit = { ToolbarIconColor.uninit(); BrowserOnClick.uninit(); DevEdition.uninit(); gMenuButtonUpdateBadge.uninit(); + ReadingListUI.uninit(); + SidebarUI.uninit(); // Now either cancel delayedStartup, or clean up the services initialized from // it. if (this._boundDelayedStartup) { this._cancelDelayedStartup(); } else { if (Win7Features)
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -1125,17 +1125,17 @@ <vbox id="browser-border-start" hidden="true" layer="true"/> <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome"> <sidebarheader id="sidebar-header" align="center"> <label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/> <image id="sidebar-throbber"/> <toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/> </sidebarheader> <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" - style="min-width: 14em; width: 18em; max-width: 36em;"/> + style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/> </vbox> <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/> <vbox id="appcontent" flex="1"> <notificationbox id="high-priority-global-notificationbox"/> <tabbrowser id="content" flex="1" contenttooltip="aHTMLTooltip" tabcontainer="tabbrowser-tabs"
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/BrowserUITestUtils.jsm @@ -0,0 +1,70 @@ +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "BrowserUITestUtils", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Timer.jsm"); + + +/** + * Default wait period in millseconds, when waiting for the expected event to occur. + * @type {number} + */ +const DEFAULT_WAIT = 2000; + + +/** + * Test utility functions for dealing with the browser UI DOM. + */ +this.BrowserUITestUtils = { + + /** + * Waits a specified number of miliseconds for a specified event to be + * fired on a specified element. + * + * Usage: + * let receivedEvent = BrowserUITestUtils.waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * yield receivedEvent; + * if (receivedEvent && !(receivedEvent instanceof Error)) { + * receivedEvent.msg == "eventName"; + * // ... + * } + * + * @param {Element} subject - The element that should receive the event. + * @param {string} eventName - The event to wait for. + * @param {number} timeoutMs - The number of miliseconds to wait before giving up. + * @param {Element} target - Expected target of the event. + * @returns {Promise} A Promise that resolves to the received event, or + * rejects with an Error. + */ + waitForEvent(subject, eventName, timeoutMs, target) { + return new Promise((resolve, reject) => { + function listener(event) { + if (target && target !== event.target) { + return; + } + + subject.removeEventListener(eventName, listener); + clearTimeout(timerID); + resolve(event); + } + + timeoutMs = timeoutMs || DEFAULT_WAIT; + let stack = new Error().stack; + + let timerID = setTimeout(() => { + subject.removeEventListener(eventName, listener); + reject(new Error(`${eventName} event timeout at ${stack}`)); + }, timeoutMs); + + + subject.addEventListener(eventName, listener); + }); + }, +};
--- a/browser/base/moz.build +++ b/browser/base/moz.build @@ -1,16 +1,20 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # 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/. SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport' +TESTING_JS_MODULES += [ + 'content/test/BrowserUITestUtils.jsm', +] + MOCHITEST_MANIFESTS += [ 'content/test/general/mochitest.ini', ] MOCHITEST_CHROME_MANIFESTS += [ 'content/test/chrome/chrome.ini', ]
--- a/browser/components/customizableui/test/browser_988072_sidebar_events.js +++ b/browser/components/customizableui/test/browser_988072_sidebar_events.js @@ -48,19 +48,23 @@ function addWidget() { function removeWidget() { CustomizableUI.removeWidgetFromArea("sidebar-button"); PanelUI.enableSingleSubviewPanelAnimations(); } // Filters out the trailing menuseparators from the sidebar list function getSidebarList() { - let sidebars = [...gSidebarMenu.children]; - while (sidebars[sidebars.length - 1].localName == "menuseparator") - sidebars.pop(); + let sidebars = [...gSidebarMenu.children].filter(sidebar => { + if (sidebar.localName == "menuseparator") + return false; + if (sidebar.getAttribute("hidden") == "true") + return false; + return true; + }); return sidebars; } function compareElements(original, displayed) { let attrs = ["label", "key", "disabled", "hidden", "origin", "image", "checked"]; for (let attr of attrs) { is(displayed.getAttribute(attr), original.getAttribute(attr), "Should have the same " + attr + " attribute"); }
--- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -10,16 +10,17 @@ DIRS += [ 'dirprovider', 'downloads', 'feeds', 'loop', 'migration', 'places', 'preferences', 'privatebrowsing', + 'readinglist', 'search', 'sessionstore', 'shell', 'selfsupport', 'tabview', 'uitour', 'translation', ]
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/ReadingList.jsm @@ -0,0 +1,356 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ReadingList"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); + + +(function() { + let parentLog = Log.repository.getLogger("readinglist"); + parentLog.level = Preferences.get("browser.readinglist.logLevel", Log.Level.Warn); + Preferences.observe("browser.readinglist.logLevel", value => { + parentLog.level = value; + }); + let formatter = new Log.BasicFormatter(); + parentLog.addAppender(new Log.ConsoleAppender(formatter)); + parentLog.addAppender(new Log.DumpAppender(formatter)); +})(); + +let log = Log.repository.getLogger("readinglist.api"); + +/** + * Represents an item in the Reading List. + * @constructor + * @see https://github.com/mozilla-services/readinglist/wiki/API-Design-proposal#data-model + */ +function Item(data) { + this._data = data; +} + +Item.prototype = { + /** + * UUID + * @type {string} + */ + get id() { + return this._data.id; + }, + + /** + * Server timestamp + * @type {string} + */ + get lastModified() { + return this._data.last_modified; + }, + + /** + * @type {nsIURL} + */ + get originalUrl() { + return Services.io.newURI(this._data.url, null, null); + }, + + /** + * @type {string} + */ + get originalTitle() { + return this._data.title || ""; + }, + + /** + * @type {nsIURL} + */ + get resolvedUrl() { + return Services.io.newURI(this._data.resolved_url || this._data.url, null, null); + }, + + /** + * @type {string} + */ + get resolvedTitle() { + return this._data.resolved_title || this.originalTitle; + }, + + /** + * @type {string} + */ + get excerpt() { + return this._data.excerpt || ""; + }, + + /** + * @type {ItemStates} + */ + get state() { + return ReadingList.ItemStates[this._data.state] || ReadingList.ItemStates.OK; + }, + + /** + * @type {boolean} + */ + get isFavorite() { + return !!this._data.favorite; + }, + + /** + * @type {boolean} + */ + get isArticle() { + return !!this._data.is_article; + }, + + /** + * @type {number} + */ + get wordCount() { + return this._data.word_count || 0; + }, + + /** + * @type {boolean} + */ + get isUnread() { + return !!this._data.unread; + }, + + /** + * Device name + * @type {string} + */ + get addedBy() { + return this._data.added_by; + }, + + /** + * @type {Date} + */ + get addedOn() { + return new Date(this._data.added_on); + }, + + /** + * @type {Date} + */ + get storedOn() { + return new Date(this._data.stored_on); + }, + + /** + * Device name + * @type {string} + */ + get markedReadBy() { + return this._data.marked_read_by; + }, + + /** + * @type {Date} + */ + get markedReadOn() { + return new date(this._data.marked_read_on); + }, + + /** + * @type {number} + */ + get readPosition() { + return this._data.read_position; + }, + + // Data not specified by the current server API + + /** + * Array of scraped or captured summary images for this page. + * TODO: Implement this. + * @type {[nsIURL]} + */ + get images() { + return []; + }, + + /** + * Favicon for this site. + * @type {nsIURL} + * TODO: Generate moz-anno: URI for favicon. + */ + get favicon() { + return null; + }, + + // Helpers + + /** + * Alias for resolvedUrl. + * TODO: This url/resolvedUrl alias makes it feel like the server API hasn't got this right. + */ + get url() { + return this.resolvedUrl; + }, + /** + * Alias for resolvedTitle + */ + get title() { + return this.resolvedTitle; + }, + + /** + * Domain portion of the URL, with prefixes stripped. For display purposes. + * @type {string} + */ + get domain() { + let host = this.resolvedUrl.host; + if (host.startsWith("www.")) { + host = host.slice(4); + } + return host; + }, + + /** + * Convert this Item to a string representation. + */ + toString() { + return `[Item url=${this.url.spec}]`; + }, + + /** + * Get the value that should be used for a JSON representation of this Item. + */ + toJSON() { + return this._data; + }, +}; + + +let ItemStates = { + OK: Symbol("ok"), + ARCHIVED: Symbol("archived"), + DELETED: Symbol("deleted"), +}; + + +this.ReadingList = { + Item: Item, + ItemStates: ItemStates, + + _listeners: new Set(), + _items: [], + + /** + * Initialize the ReadingList component. + */ + _init() { + log.debug("Init"); + + // Initialize mock data + let mockData = JSON.parse(Preferences.get("browser.readinglist.mockData", "[]")); + for (let itemData of mockData) { + this._items.push(new Item(itemData)); + } + }, + + /** + * Add an event listener. + * @param {object} listener - Listener object to start notifying. + */ + addListener(listener) { + this._listeners.add(listener); + }, + + /** + * Remove a specified event listener. + * @param {object} listener - Listener object to stop notifying. + */ + removeListener(listener) { + this._listeners.delete(listener); + }, + + /** + * Notify all registered event listeners of an event. + * @param {string} eventName - Event name, which will be used as a method name + * on listeners to call. + */ + _notifyListeners(eventName, ...args) { + for (let listener of this._listeners) { + if (typeof listener[eventName] != "function") { + continue; + } + + try { + listener[eventName](...args); + } catch (e) { + log.error(`Error calling listener.${eventName}`, e); + } + } + }, + + /** + * Fetch the number of items that match a set of given conditions. + * TODO: Implement filtering, sorting, etc. Needs backend storage work. + * + * @param {Object} conditions Object specifying a set of conditions for + * filtering items. + * @return {Promise} + * @resolves {number} + */ + getNumItems(conditions = {unread: false}) { + return new Promise((resolve, reject) => { + resolve(this._items.length); + }); + }, + + /** + * Fetch items matching a set of conditions, in a sorted list. + * TODO: Implement filtering, sorting, etc. Needs backend storage work. + * + * @return {Promise} + * @resolves {[Item]} + */ + getItems(options = {sort: "addedOn", conditions: {unread: false}}) { + return new Promise((resolve, reject) => { + resolve([...this._items]); + }); + }, + + /** + * Find an item based on its ID. + * TODO: Implement. Needs backend storage work. + * + * @return {Promise} + * @resolves {Item} + */ + getItemByID(url) { + return new Promise((resolve, reject) => { + resolve(null); + }); + }, + + /** + * Find an item based on its URL. + * + * TODO: Implement. Needs backend storage work. + * TODO: Does this match original or resolved URL, or both? + * TODO: Should this just be a generic findItem API? + * + * @return {Promise} + * @resolves {Item} + */ + getItemByURL(url) { + return new Promise((resolve, reject) => { + resolve(null); + }); + }, +}; + + +ReadingList._init();
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/jar.mn @@ -0,0 +1,7 @@ +# 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/. + +browser.jar: + content/browser/readinglist/sidebar.xhtml + content/browser/readinglist/sidebar.js
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/moz.build @@ -0,0 +1,15 @@ +# 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/. + +JAR_MANIFESTS += ['jar.mn'] + +EXTRA_JS_MODULES.readinglist += [ + 'ReadingList.jsm', +] + +TESTING_JS_MODULES += [ + 'test/ReadingListTestUtils.jsm', +] + +BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/sidebar.js @@ -0,0 +1,403 @@ +/* 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/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/readinglist/ReadingList.jsm"); + +let log = Cu.import("resource://gre/modules/Log.jsm", {}) + .Log.repository.getLogger("readinglist.sidebar"); + + +let RLSidebar = { + /** + * Container element for all list item elements. + * @type {Element} + */ + list: null, + + /** + * <template> element used for constructing list item elements. + * @type {Element} + */ + itemTemplate: null, + + /** + * Map of ReadingList Item objects, keyed by their ID. + * @type {Map} + */ + itemsById: new Map(), + /** + * Map of list item elements, keyed by their corresponding Item's ID. + * @type {Map} + */ + itemNodesById: new Map(), + + /** + * Initialize the sidebar UI. + */ + init() { + log.debug("Initializing"); + + addEventListener("unload", () => this.uninit()); + + this.list = document.getElementById("list"); + this.itemTemplate = document.getElementById("item-template"); + + this.list.addEventListener("click", event => this.onListClick(event)); + this.list.addEventListener("mousemove", event => this.onListMouseMove(event)); + this.list.addEventListener("keydown", event => this.onListKeyDown(event), true); + + this.ensureListItems(); + ReadingList.addListener(this); + + let initEvent = new CustomEvent("Initialized", {bubbles: true}); + document.documentElement.dispatchEvent(initEvent); + }, + + /** + * Un-initialize the sidebar UI. + */ + uninit() { + log.debug("Shutting down"); + + ReadingList.removeListener(this); + }, + + /** + * Handle an item being added to the ReadingList. + * TODO: We may not want to show this new item right now. + * TODO: We should guard against the list growing here. + * + * @param {Readinglist.Item} item - Item that was added. + */ + onItemAdded(item) { + log.trace(`onItemAdded: ${item}`); + + let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild; + this.updateItem(item, itemNode); + this.list.appendChild(itemNode); + this.itemNodesById.set(item.id, itemNode); + this.itemsById.set(item.id, item); + }, + + /** + * Handle an item being deleted from the ReadingList. + * @param {ReadingList.Item} item - Item that was deleted. + */ + onItemDeleted(item) { + log.trace(`onItemDeleted: ${item}`); + + let itemNode = this.itemNodesById.get(item.id); + itemNode.remove(); + this.itemNodesById.delete(item.id); + this.itemsById.delete(item.id); + // TODO: ensureListItems doesn't yet cope with needing to add one item. + //this.ensureListItems(); + }, + + /** + * Handle an item in the ReadingList having any of its properties changed. + * @param {ReadingList.Item} item - Item that was updated. + */ + onItemUpdated(item) { + log.trace(`onItemUpdated: ${item}`); + + let itemNode = this.itemNodesById.get(item.id); + if (!itemNode) + return; + + this.updateItem(item, itemNode); + }, + + /** + * Update the element representing an item, ensuring it's in sync with the + * underlying data. + * @param {ReadingList.Item} item - Item to use as a source. + * @param {Element} itemNode - Element to update. + */ + updateItem(item, itemNode) { + itemNode.setAttribute("id", "item-" + item.id); + itemNode.setAttribute("title", `${item.title}\n${item.url.spec}`); + + itemNode.querySelector(".item-title").textContent = item.title; + itemNode.querySelector(".item-domain").textContent = item.domain; + }, + + /** + * Ensure that the list is populated with the correct items. + */ + ensureListItems() { + ReadingList.getItems().then(items => { + for (let item of items) { + // TODO: Should be batch inserting via DocumentFragment + try { + this.onItemAdded(item); + } catch (e) { + log.warn("Error adding item", e); + } + } + }); + }, + + /** + * Get the number of items currently displayed in the list. + * @type {number} + */ + get numItems() { + return this.list.childElementCount; + }, + + /** + * The currently active element in the list. + * @type {Element} + */ + get activeItem() { + return document.querySelector("#list > .item.active"); + }, + + set activeItem(node) { + if (node && node.parentNode != this.list) { + log.error(`Unable to set activeItem to invalid node ${node}`); + return; + } + + log.debug(`Setting activeItem: ${node ? node.id : null}`); + + if (node) { + if (!node.classList.contains("selected")) { + this.selectedItem = node; + } + + if (node.classList.contains("active")) { + return; + } + } + + let prevItem = document.querySelector("#list > .item.active"); + if (prevItem) { + prevItem.classList.remove("active"); + } + + if (node) { + node.classList.add("active"); + } + + let event = new CustomEvent("ActiveItemChanged", {bubbles: true}); + this.list.dispatchEvent(event); + }, + + /** + * The currently selected item in the list. + * @type {Element} + */ + get selectedItem() { + return document.querySelector("#list > .item.selected"); + }, + + set selectedItem(node) { + if (node && node.parentNode != this.list) { + log.error(`Unable to set selectedItem to invalid node ${node}`); + return; + } + + log.debug(`Setting activeItem: ${node ? node.id : null}`); + + let prevItem = document.querySelector("#list > .item.selected"); + if (prevItem) { + prevItem.classList.remove("selected"); + } + + if (node) { + node.classList.add("selected"); + let itemId = this.getItemIdFromNode(node); + this.list.setAttribute("aria-activedescendant", "item-" + itemId); + } else { + this.list.removeAttribute("aria-activedescendant"); + } + + let event = new CustomEvent("SelectedItemChanged", {bubbles: true}); + this.list.dispatchEvent(event); + }, + + /** + * The index of the currently selected item in the list. + * @type {number} + */ + get selectedIndex() { + for (let i = 0; i < this.numItems; i++) { + let item = this.list.children.item(i); + if (!item) { + break; + } + if (item.classList.contains("selected")) { + return i; + } + } + return -1; + }, + + set selectedIndex(index) { + log.debug(`Setting selectedIndex: ${index}`); + + if (index == -1) { + this.selectedItem = null; + return; + } + + let item = this.list.children.item(index); + if (!item) { + log.warn(`Unable to set selectedIndex to invalid index ${index}`); + return; + } + this.selectedItem = item; + }, + + /** + * Open a given URL. The event is used to determine where it should be opened + * (current tab, new tab, new window). + * @param {string} url - URL to open. + * @param {Event} event - KeyEvent or MouseEvent that triggered this action. + */ + openURL(url, event) { + // TODO: Disabled while working on the listbox mechanics. + log.debug(`Opening page ${url}`); + return; + + let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + mainWindow.openUILink(url, event); + }, + + /** + * Get the ID of the Item associated with a given list item element. + * @param {element} node - List item element to get an ID for. + * @return {string} Assocated Item ID. + */ + getItemIdFromNode(node) { + let id = node.getAttribute("id"); + if (id && id.startsWith("item-")) { + return id.slice(5); + } + + return null; + }, + + /** + * Get the Item associated with a given list item element. + * @param {element} node - List item element to get an Item for. + * @return {string} Associated Item. + */ + getItemFromNode(node) { + let itemId = this.getItemIdFromNode(node); + if (!itemId) { + return null; + } + + return this.itemsById.get(itemId); + }, + + /** + * Open the active item in the list. + * @param {Event} event - Event triggering this. + */ + openActiveItem(event) { + let itemNode = this.activeItem; + if (!itemNode) { + return; + } + + let item = this.getItemFromNode(itemNode); + this.openURL(item.url.spec, event); + }, + + /** + * Find the parent item element, from a given child element. + * @param {Element} node - Child element. + * @return {Element} Element for the item, or null if not found. + */ + findParentItemNode(node) { + while (node && node != this.list && node != document.documentElement && + !node.classList.contains("item")) { + node = node.parentNode; + } + + if (node != this.list && node != document.documentElement) { + return node; + } + + return null; + }, + + /** + * Handle a click event on the list box. + * @param {Event} event - Triggering event. + */ + onListClick(event) { + let itemNode = this.findParentItemNode(event.target); + if (!itemNode) + return; + + this.activeItem = itemNode; + this.openActiveItem(event); + }, + + /** + * Handle a mousemove event over the list box. + * @param {Event} event - Triggering event. + */ + onListMouseMove(event) { + let itemNode = this.findParentItemNode(event.target); + if (!itemNode) + return; + + this.selectedItem = itemNode; + }, + + /** + * Handle a keydown event on the list box. + * @param {Event} event - Triggering event. + */ + onListKeyDown(event) { + if (event.keyCode == KeyEvent.DOM_VK_DOWN) { + // TODO: Refactor this so we pass a direction to a generic method. + // See autocomplete.xml's getNextIndex + event.preventDefault(); + let index = this.selectedIndex + 1; + if (index >= this.numItems) { + index = 0; + } + + this.selectedIndex = index; + this.selectedItem.focus(); + } else if (event.keyCode == KeyEvent.DOM_VK_UP) { + event.preventDefault(); + + let index = this.selectedIndex - 1; + if (index < 0) { + index = this.numItems - 1; + } + + this.selectedIndex = index; + this.selectedItem.focus(); + } else if (event.keyCode == KeyEvent.DOM_VK_RETURN) { + let selectedItem = this.selectedItem; + if (selectedItem) { + this.activeItem = this.selectedItem; + this.openActiveItem(event); + } + } + }, +}; + + +addEventListener("DOMContentLoaded", () => RLSidebar.init());
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/sidebar.xhtml @@ -0,0 +1,30 @@ +<?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 % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> + %browserDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <script src="chrome://browser/content/readinglist/sidebar.js" type="application/javascript;version=1.8"></script> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/readinglist/sidebar.css"/> + <!-- <title>&readingList.label;</title> --> + </head> + + <body role="application"> + <template id="item-template"> + <div class="item" role="option" tabindex="-1"> + <div class="item-thumb-container"></div> + <div class="item-summary-container"> + <div class="item-title"></div> + <div class="item-domain"></div> + </div> + </div> + </template> + + <div id="list" role="listbox" tabindex="1"></div> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/ReadingListTestUtils.jsm @@ -0,0 +1,159 @@ +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "ReadingListTestUtils", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource:///modules/readinglist/ReadingList.jsm"); + + +/** Preference name controlling whether the ReadingList feature is enabled/disabled. */ +const PREF_RL_ENABLED = "browser.readinglist.enabled"; + + +/** + * Utilities for testing the ReadingList sidebar. + */ +function SidebarUtils(window, assert) { + this.window = window; + this.Assert = assert; +} + +SidebarUtils.prototype = { + /** + * Reference to the RLSidebar object controlling the ReadingList sidebar UI. + * @type {object} + */ + get RLSidebar() { + return this.window.SidebarUI.browser.contentWindow.RLSidebar; + }, + + /** + * Reference to the list container element in the sidebar. + * @type {Element} + */ + get list() { + return this.RLSidebar.list; + }, + + /** + * Check that the number of elements in the list matches the expected count. + * @param {number} count - Expected number of items. + */ + expectNumItems(count) { + this.Assert.equal(this.list.childElementCount, count, + "Should have expected number of items in the sidebar list"); + }, + + /** + * Check all items in the sidebar list, ensuring the DOM matches the data. + */ + checkAllItems() { + for (let itemNode of this.list.children) { + this.checkSidebarItem(itemNode); + } + }, + + /** + * Run a series of sanity checks for an element in the list associated with + * an Item, ensuring the DOM matches the data. + */ + checkItem(node) { + let item = this.RLSidebar.getItemFromNode(node); + + this.Assert.ok(node.classList.contains("item"), + "Node should have .item class"); + this.Assert.equal(node.id, "item-" + item.id, + "Node should have correct ID"); + this.Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url.spec, + "Node should have correct title attribute"); + this.Assert.equal(node.querySelector(".item-title").textContent, item.title, + "Node's title element's text should match item title"); + this.Assert.equal(node.querySelector(".item-domain").textContent, item.domain, + "Node's domain element's text should match item title"); + }, + + expectSelectedId(itemId) { + let selectedItem = this.RLSidebar.selectedItem; + if (itemId == null) { + this.Assert.equal(selectedItem, null, "Should have no selected item"); + } else { + this.Assert.notEqual(selectedItem, null, "selectedItem should not be null"); + let selectedId = this.RLSidebar.getItemIdFromNode(selectedItem); + this.Assert.equal(itemId, selectedId, "Should have currect item selected"); + } + }, + + expectActiveId(itemId) { + let activeItem = this.RLSidebar.activeItem; + if (itemId == null) { + this.Assert.equal(activeItem, null, "Should have no active item"); + } else { + this.Assert.notEqual(activeItem, null, "activeItem should not be null"); + let activeId = this.RLSidebar.getItemIdFromNode(activeItem); + this.Assert.equal(itemId, activeId, "Should have correct item active"); + } + }, +}; + + +/** + * Utilities for testing the ReadingList. + */ +this.ReadingListTestUtils = { + /** + * Whether the ReadingList feature is enabled or not. + * @type {boolean} + */ + get enabled() { + return Preferences.get(PREF_RL_ENABLED, false); + }, + set enabled(value) { + Preferences.set(PREF_RL_ENABLED, !!value); + }, + + /** + * Utilities for testing the ReadingList sidebar. + */ + SidebarUtils: SidebarUtils, + + /** + * Synthetically add an item to the ReadingList. + * @param {object|[object]} data - Object or array of objects to pass to the + * Item constructor. + * @return {Promise} Promise that gets fulfilled with the item or items added. + */ + addItem(data) { + if (Array.isArray(data)) { + let promises = []; + for (let itemData of data) { + promises.push(this.addItem(itemData)); + } + return Promise.all(promises); + } + + return new Promise(resolve => { + let item = new ReadingList.Item(data); + ReadingList._items.push(item); + ReadingList._notifyListeners("onItemAdded", item); + resolve(item); + }); + }, + + /** + * Cleanup all data, resetting to a blank state. + */ + cleanup() { + return new Promise(resolve => { + ReadingList._items = []; + ReadingList._listeners.clear(); + Preferences.reset(PREF_RL_ENABLED); + resolve(); + }); + }, +};
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/browser/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + head.js + +[browser_ui_enable_disable.js] +[browser_sidebar_list.js] +[browser_sidebar_mouse_nav.js]
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/browser/browser_sidebar_list.js @@ -0,0 +1,53 @@ +/** + * This tests the basic functionality of the sidebar to list items. + */ + +add_task(function*() { + registerCleanupFunction(function*() { + ReadingListUI.hideSidebar(); + yield RLUtils.cleanup(); + }); + + RLUtils.enabled = true; + + yield ReadingListUI.showSidebar(); + let RLSidebar = RLSidebarUtils.RLSidebar; + let sidebarDoc = SidebarUI.browser.contentDocument; + Assert.equal(RLSidebar.numItems, 0, "Should start with no items"); + Assert.equal(RLSidebar.activeItem, null, "Should start with no active item"); + Assert.equal(RLSidebar.activeItem, null, "Should start with no selected item"); + + info("Adding first item"); + yield RLUtils.addItem({ + id: "c3502a49-bcef-4a94-b222-d4834463de33", + url: "http://example.com/article1", + title: "Article 1", + }); + RLSidebarUtils.expectNumItems(1); + + info("Adding more items"); + yield RLUtils.addItem([{ + id: "e054f5b7-1f4f-463f-bb96-d64c02448c31", + url: "http://example.com/article2", + title: "Article 2", + }, { + id: "4207230b-2364-4e97-9587-01312b0ce4e6", + url: "http://example.com/article3", + title: "Article 3", + }]); + RLSidebarUtils.expectNumItems(3); + + info("Closing sidebar"); + ReadingListUI.hideSidebar(); + + info("Adding another item"); + yield RLUtils.addItem({ + id: "dae0e855-607e-4df3-b27f-73a5e35c94fe", + url: "http://example.com/article4", + title: "Article 4", + }); + + info("Re-eopning sidebar"); + yield ReadingListUI.showSidebar(); + RLSidebarUtils.expectNumItems(4); +});
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/browser/browser_sidebar_mouse_nav.js @@ -0,0 +1,82 @@ +/** + * Test mouse navigation for selecting items in the sidebar. + */ + + +function mouseInteraction(mouseEvent, responseEvent, itemNode) { + let eventPromise = BrowserUITestUtils.waitForEvent(RLSidebarUtils.list, responseEvent); + let details = {}; + if (mouseEvent != "click") { + details.type = mouseEvent; + } + + EventUtils.synthesizeMouseAtCenter(itemNode, details, itemNode.ownerDocument.defaultView); + return eventPromise; +} + +add_task(function*() { + registerCleanupFunction(function*() { + ReadingListUI.hideSidebar(); + yield RLUtils.cleanup(); + }); + + RLUtils.enabled = true; + + let itemData = [{ + id: "00bd24c7-3629-40b0-acde-37aa81768735", + url: "http://example.com/article1", + title: "Article 1", + }, { + id: "28bf7f19-cf94-4ceb-876a-ac1878342e0d", + url: "http://example.com/article2", + title: "Article 2", + }, { + id: "7e5064ea-f45d-4fc7-8d8c-c067b7781e78", + url: "http://example.com/article3", + title: "Article 3", + }, { + id: "8e72a472-8db8-4904-ba39-9672f029e2d0", + url: "http://example.com/article4", + title: "Article 4", + }, { + id: "8d332744-37bc-4a1a-a26b-e9953b9f7d91", + url: "http://example.com/article5", + title: "Article 5", + }]; + yield RLUtils.addItem(itemData); + + yield ReadingListUI.showSidebar(); + RLSidebarUtils.expectNumItems(5); + RLSidebarUtils.expectSelectedId(null); + RLSidebarUtils.expectActiveId(null); + + info("Mouse move over item 1"); + yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]); + RLSidebarUtils.expectSelectedId(itemData[0].id); + RLSidebarUtils.expectActiveId(null); + + info("Mouse move over item 2"); + yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[1]); + RLSidebarUtils.expectSelectedId(itemData[1].id); + RLSidebarUtils.expectActiveId(null); + + info("Mouse move over item 5"); + yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[4]); + RLSidebarUtils.expectSelectedId(itemData[4].id); + RLSidebarUtils.expectActiveId(null); + + info("Mouse move over item 1 again"); + yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]); + RLSidebarUtils.expectSelectedId(itemData[0].id); + RLSidebarUtils.expectActiveId(null); + + info("Mouse click on item 1"); + yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[0]); + RLSidebarUtils.expectSelectedId(itemData[0].id); + RLSidebarUtils.expectActiveId(itemData[0].id); + + info("Mouse click on item 3"); + yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[2]); + RLSidebarUtils.expectSelectedId(itemData[2].id); + RLSidebarUtils.expectActiveId(itemData[2].id); +});
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/browser/browser_ui_enable_disable.js @@ -0,0 +1,49 @@ +/** + * Test enabling/disabling the entire ReadingList feature via the + * browser.readinglist.enabled preference. + */ + +function checkRLState() { + let enabled = RLUtils.enabled; + info("Checking ReadingList UI is " + (enabled ? "enabled" : "disabled")); + + let sidebarBroadcaster = document.getElementById("readingListSidebar"); + let sidebarMenuitem = document.getElementById("menu_readingListSidebar"); + + if (enabled) { + Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true", + "Sidebar broadcaster should not be hidden"); + Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true", + "Sidebar menuitem should be visible"); + } else { + Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true", + "Sidebar broadcaster should be hidden"); + Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true", + "Sidebar menuitem should be hidden"); + Assert.equal(ReadingListUI.isSidebarOpen, false, + "ReadingListUI should not think sidebar is open"); + } + + if (!enabled) { + Assert.equal(SidebarUI.isOpen, false, "Sidebar should not be open"); + } +} + +add_task(function*() { + info("Start with ReadingList disabled"); + RLUtils.enabled = false; + checkRLState(); + info("Enabling ReadingList"); + RLUtils.enabled = true; + checkRLState(); + + info("Opening ReadingList sidebar"); + yield ReadingListUI.showSidebar(); + Assert.ok(SidebarUI.isOpen, "Sidebar should be open"); + Assert.equal(SidebarUI.currentID, "readingListSidebar", "Sidebar should have ReadingList loaded"); + + info("Disabling ReadingList"); + RLUtils.enabled = false; + Assert.ok(!SidebarUI.isOpen, "Sidebar should be closed"); + checkRLState(); +});
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/browser/head.js @@ -0,0 +1,15 @@ +XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", + "resource:///modules/readinglist/ReadingList.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ReadingListTestUtils", + "resource://testing-common/ReadingListTestUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITestUtils", + "resource://testing-common/BrowserUITestUtils.jsm"); + + +XPCOMUtils.defineLazyGetter(this, "RLUtils", () => { + return ReadingListTestUtils; +}); + +XPCOMUtils.defineLazyGetter(this, "RLSidebarUtils", () => { + return new RLUtils.SidebarUtils(window, Assert); +});
--- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -833,10 +833,12 @@ just addresses the organization to follo <!ENTITY processHang.terminateScript.accessKey "S"> <!ENTITY processHang.debugScript.label "Debug Script"> <!ENTITY processHang.debugScript.accessKey "D"> <!ENTITY processHang.terminatePlugin.label "Kill Plugin"> <!ENTITY processHang.terminatePlugin.accessKey "P"> <!ENTITY processHang.terminateProcess.label "Kill Web Process"> <!ENTITY processHang.terminateProcess.accessKey "K"> +<!ENTITY readingList.label "Reading List"> +<!ENTITY readingList.sidebar.commandKey "R"> <!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…"> <!ENTITY emeLearnMoreContextMenu.accesskey "D">
--- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -86,16 +86,17 @@ browser.jar: skin/classic/browser/Toolbar-inverted.png skin/classic/browser/Toolbar-small.png skin/classic/browser/undoCloseTab.png (../shared/undoCloseTab.png) skin/classic/browser/urlbar-arrow.png skin/classic/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg) skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg) skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg) skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png) + skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css) skin/classic/browser/webRTC-shareDevice-16.png skin/classic/browser/webRTC-shareDevice-64.png skin/classic/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png) skin/classic/browser/webRTC-shareMicrophone-16.png skin/classic/browser/webRTC-shareMicrophone-64.png skin/classic/browser/webRTC-sharingMicrophone-16.png (../shared/webrtc/webRTC-sharingMicrophone-16.png) skin/classic/browser/webRTC-shareScreen-16.png (../shared/webrtc/webRTC-shareScreen-16.png) skin/classic/browser/webRTC-shareScreen-64.png (../shared/webrtc/webRTC-shareScreen-64.png)
--- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -137,16 +137,17 @@ browser.jar: skin/classic/browser/urlbar-arrow@2x.png skin/classic/browser/urlbar-popup-blocked.png skin/classic/browser/urlbar-popup-blocked@2x.png skin/classic/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg) skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg) skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg) skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png) skin/classic/browser/reader-mode-16@2x.png (../shared/reader/reader-mode-16@2x.png) + skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css) skin/classic/browser/webRTC-shareDevice-16.png skin/classic/browser/webRTC-shareDevice-16@2x.png skin/classic/browser/webRTC-shareDevice-64.png skin/classic/browser/webRTC-shareDevice-64@2x.png skin/classic/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png) skin/classic/browser/webRTC-sharingDevice-16@2x.png (../shared/webrtc/webRTC-sharingDevice-16@2x.png) skin/classic/browser/webRTC-shareMicrophone-16.png skin/classic/browser/webRTC-shareMicrophone-16@2x.png
new file mode 100644 --- /dev/null +++ b/browser/themes/shared/readinglist/sidebar.css @@ -0,0 +1,72 @@ +/* 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/. */ + +:root, body { + height: 100%; + overflow-x: hidden; +} + +body { + margin: 0; + font: message-box; + background: #F8F7F8; + color: #333333; + -moz-user-select: none; + overflow: hidden; +} + +#list { + height: 100%; + overflow-x: auto; +} + +.item { + display: flex; + flex-flow: row; + cursor: pointer; + padding: 6px; +} + +.item.active { + background: #FEFEFE; +} + +.item.selected { + background: #FDFDFD; +} + +.item-thumb-container { + min-width: 64px; + max-width: 64px; + min-height: 40px; + max-height: 40px; + background: #EBEBEB; + border: 1px solid white; + box-shadow: 0px 1px 2px rgba(0,0,0,.35); + margin: 5px; +} + +.item-summary-container { + display: flex; + flex-flow: column; + -moz-padding-start: 4px; + overflow: hidden; +} + +.item-title { + overflow: hidden; + height: 2.8em; +} + +.item-domain { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-height: 1.4em; + color: #0095DD; +} + +.item:hover .item-domain { + color: #008ACB; +}
--- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -105,16 +105,17 @@ browser.jar: skin/classic/browser/undoCloseTab@2x.png (../shared/undoCloseTab@2x.png) skin/classic/browser/urlbar-arrow.png skin/classic/browser/urlbar-popup-blocked.png skin/classic/browser/urlbar-history-dropmarker.png skin/classic/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg) skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg) skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg) skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png) + skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css) skin/classic/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png) skin/classic/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png) skin/classic/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png) skin/classic/browser/webRTC-shareDevice-16.png skin/classic/browser/webRTC-shareDevice-64.png skin/classic/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png) skin/classic/browser/webRTC-shareMicrophone-16.png skin/classic/browser/webRTC-shareMicrophone-64.png @@ -570,16 +571,17 @@ browser.jar: skin/classic/aero/browser/undoCloseTab@2x.png (../shared/undoCloseTab@2x.png) skin/classic/aero/browser/urlbar-arrow.png skin/classic/aero/browser/urlbar-popup-blocked.png skin/classic/aero/browser/urlbar-history-dropmarker.png skin/classic/aero/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg) skin/classic/aero/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg) skin/classic/aero/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg) skin/classic/aero/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png) + skin/classic/aero/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css) skin/classic/aero/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png) skin/classic/aero/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png) skin/classic/aero/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png) skin/classic/aero/browser/webRTC-shareDevice-16.png skin/classic/aero/browser/webRTC-shareDevice-64.png skin/classic/aero/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png) skin/classic/aero/browser/webRTC-shareMicrophone-16.png skin/classic/aero/browser/webRTC-shareMicrophone-64.png