mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
466 lines
12 KiB
JavaScript
466 lines
12 KiB
JavaScript
/* 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/Task.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,
|
|
|
|
/**
|
|
* A promise that's resolved when building the initial list completes.
|
|
* @type {Promise}
|
|
*/
|
|
listPromise: 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.emptyListInfo = document.getElementById("emptyListInfo");
|
|
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);
|
|
|
|
window.addEventListener("message", event => this.onMessage(event));
|
|
|
|
this.listPromise = this.ensureListItems();
|
|
ReadingList.addListener(this);
|
|
|
|
Services.prefs.setBoolPref("browser.readinglist.sidebarEverOpened", true);
|
|
|
|
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 {ReadinglistItem} item - Item that was added.
|
|
*/
|
|
onItemAdded(item, append = false) {
|
|
log.trace(`onItemAdded: ${item}`);
|
|
|
|
let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
|
|
this.updateItem(item, itemNode);
|
|
// XXX Inserting at the top by default is a temp hack that will stop
|
|
// working once we start including items received from sync.
|
|
if (append)
|
|
this.list.appendChild(itemNode);
|
|
else
|
|
this.list.insertBefore(itemNode, this.list.firstChild);
|
|
this.itemNodesById.set(item.id, itemNode);
|
|
this.itemsById.set(item.id, item);
|
|
|
|
this.emptyListInfo.hidden = true;
|
|
},
|
|
|
|
/**
|
|
* Handle an item being deleted from the ReadingList.
|
|
* @param {ReadingListItem} 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();
|
|
|
|
this.emptyListInfo.hidden = (this.numItems > 0);
|
|
},
|
|
|
|
/**
|
|
* Handle an item in the ReadingList having any of its properties changed.
|
|
* @param {ReadingListItem} 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 {ReadingListItem} 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}`);
|
|
|
|
itemNode.querySelector(".item-title").textContent = item.title;
|
|
|
|
let domain = item.uri.spec;
|
|
try {
|
|
domain = item.uri.host;
|
|
}
|
|
catch (err) {}
|
|
itemNode.querySelector(".item-domain").textContent = domain;
|
|
|
|
let thumb = itemNode.querySelector(".item-thumb-container");
|
|
if (item.preview) {
|
|
thumb.style.backgroundImage = "url(" + item.preview + ")";
|
|
} else {
|
|
thumb.style.removeProperty("background-image");
|
|
}
|
|
thumb.classList.toggle("preview-available", !!item.preview);
|
|
},
|
|
|
|
/**
|
|
* Ensure that the list is populated with the correct items.
|
|
*/
|
|
ensureListItems: Task.async(function* () {
|
|
yield ReadingList.forEachItem(item => {
|
|
// TODO: Should be batch inserting via DocumentFragment
|
|
try {
|
|
this.onItemAdded(item, true);
|
|
} catch (e) {
|
|
log.warn("Error adding item", e);
|
|
}
|
|
}, {sort: "addedOn", descending: true});
|
|
this.emptyListInfo.hidden = (this.numItems > 0);
|
|
}),
|
|
|
|
/**
|
|
* Get the number of items currently displayed in the list.
|
|
* @type {number}
|
|
*/
|
|
get numItems() {
|
|
return this.list.childElementCount;
|
|
},
|
|
|
|
/**
|
|
* The list item displayed in the current tab.
|
|
* @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.trace(`Setting activeItem: ${node ? node.id : null}`);
|
|
|
|
if (node && 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 list item selected with the keyboard.
|
|
* @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.trace(`Setting selectedItem: ${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.trace(`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) {
|
|
log.debug(`Opening page ${url}`);
|
|
|
|
let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
.rootTreeItem
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindow);
|
|
|
|
let currentUrl = mainWindow.gBrowser.currentURI.spec;
|
|
if (currentUrl.startsWith("about:reader"))
|
|
url = "about:reader?url=" + encodeURIComponent(url);
|
|
|
|
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, 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;
|
|
|
|
if (event.target.classList.contains("remove-button")) {
|
|
ReadingList.deleteItem(this.getItemFromNode(itemNode));
|
|
return;
|
|
}
|
|
|
|
this.activeItem = itemNode;
|
|
this.openActiveItem(event);
|
|
},
|
|
|
|
/**
|
|
* Handle a mousemove event over the list box:
|
|
* If the hovered item isn't the selected one, clear the selection.
|
|
* @param {Event} event - Triggering event.
|
|
*/
|
|
onListMouseMove(event) {
|
|
let itemNode = this.findParentItemNode(event.target);
|
|
if (itemNode != this.selectedItem)
|
|
this.selectedItem = null;
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
|
|
if (!this.numItems) {
|
|
return;
|
|
}
|
|
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();
|
|
|
|
if (!this.numItems) {
|
|
return;
|
|
}
|
|
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 = selectedItem;
|
|
this.openActiveItem(event);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle a message, typically sent from browser-readinglist.js
|
|
* @param {Event} event - Triggering event.
|
|
*/
|
|
onMessage(event) {
|
|
let msg = event.data;
|
|
|
|
if (msg.topic != "UpdateActiveItem") {
|
|
return;
|
|
}
|
|
|
|
if (!msg.url) {
|
|
this.activeItem = null;
|
|
} else {
|
|
ReadingList.itemForURL(msg.url).then(item => {
|
|
this.activeItem = this.itemNodesById.get(item.id);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
addEventListener("DOMContentLoaded", () => RLSidebar.init());
|