mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1123517 - [ReadingList] Implement basic sidebar that lists unread ReadingList items. r=florian
This commit is contained in:
parent
89f2f315e7
commit
204a0f5703
@ -1856,3 +1856,6 @@ pref("dom.ipc.reportProcessHangs", true);
|
||||
|
||||
// Disable reader mode by default.
|
||||
pref("reader.parse-on-load.enabled", false);
|
||||
|
||||
// Disable ReadingList by default.
|
||||
pref("browser.readinglist.enabled", false);
|
||||
|
@ -223,6 +223,11 @@
|
||||
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"/>
|
||||
|
61
browser/base/content/browser-readinglist.js
Normal file
61
browser/base/content/browser-readinglist.js
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
# 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.updateUI();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
document.getElementById("readingListSidebar").setAttribute("hidden", !enabled);
|
||||
|
||||
if (!enabled) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
},
|
||||
};
|
@ -148,6 +148,11 @@
|
||||
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');"/>
|
||||
@ -418,6 +423,11 @@
|
||||
#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"/>
|
||||
|
@ -13,6 +13,8 @@ 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",
|
||||
@ -225,6 +227,7 @@ let gInitialPages = [
|
||||
#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
|
||||
@ -1371,6 +1374,7 @@ var gBrowserInit = {
|
||||
|
||||
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.
|
||||
|
@ -1130,7 +1130,7 @@
|
||||
<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"/>
|
||||
|
70
browser/base/content/test/BrowserUITestUtils.jsm
Normal file
70
browser/base/content/test/BrowserUITestUtils.jsm
Normal file
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
@ -6,6 +6,10 @@
|
||||
|
||||
SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport'
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'content/test/BrowserUITestUtils.jsm',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'content/test/general/mochitest.ini',
|
||||
]
|
||||
|
@ -53,9 +53,13 @@ function removeWidget() {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ DIRS += [
|
||||
'places',
|
||||
'preferences',
|
||||
'privatebrowsing',
|
||||
'readinglist',
|
||||
'search',
|
||||
'sessionstore',
|
||||
'shell',
|
||||
|
356
browser/components/readinglist/ReadingList.jsm
Normal file
356
browser/components/readinglist/ReadingList.jsm
Normal file
@ -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();
|
7
browser/components/readinglist/jar.mn
Normal file
7
browser/components/readinglist/jar.mn
Normal file
@ -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
|
15
browser/components/readinglist/moz.build
Normal file
15
browser/components/readinglist/moz.build
Normal file
@ -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']
|
392
browser/components/readinglist/sidebar.js
Normal file
392
browser/components/readinglist/sidebar.js
Normal file
@ -0,0 +1,392 @@
|
||||
/* 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");
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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());
|
30
browser/components/readinglist/sidebar.xhtml
Normal file
30
browser/components/readinglist/sidebar.xhtml
Normal file
@ -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>
|
159
browser/components/readinglist/test/ReadingListTestUtils.jsm
Normal file
159
browser/components/readinglist/test/ReadingListTestUtils.jsm
Normal file
@ -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();
|
||||
});
|
||||
},
|
||||
};
|
7
browser/components/readinglist/test/browser/browser.ini
Normal file
7
browser/components/readinglist/test/browser/browser.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[browser_ui_enable_disable.js]
|
||||
[browser_sidebar_list.js]
|
||||
[browser_sidebar_mouse_nav.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);
|
||||
});
|
@ -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);
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
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;
|
||||
checkRLState();
|
||||
});
|
15
browser/components/readinglist/test/browser/head.js
Normal file
15
browser/components/readinglist/test/browser/head.js
Normal file
@ -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);
|
||||
});
|
@ -838,5 +838,7 @@ just addresses the organization to follow, e.g. "This site is run by " -->
|
||||
<!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">
|
||||
|
@ -91,6 +91,7 @@ browser.jar:
|
||||
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)
|
||||
|
@ -142,6 +142,7 @@ browser.jar:
|
||||
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
|
||||
|
72
browser/themes/shared/readinglist/sidebar.css
Normal file
72
browser/themes/shared/readinglist/sidebar.css
Normal file
@ -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;
|
||||
}
|
@ -110,6 +110,7 @@ browser.jar:
|
||||
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)
|
||||
@ -576,6 +577,7 @@ browser.jar:
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user