mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-22 17:55:50 +00:00
Bug 1525173 - HTML about:addons detail view r=jaws,flod
Differential Revision: https://phabricator.services.mozilla.com/D22766 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
d7be8a0205
commit
0978afd594
@ -111,6 +111,10 @@ detail-last-updated =
|
||||
|
||||
detail-contributions-description = The developer of this add-on asks that you help support its continued development by making a small contribution.
|
||||
|
||||
detail-contributions-button = Contribute
|
||||
.title = Contribute to the development of this add-on
|
||||
.accesskey = C
|
||||
|
||||
detail-update-type =
|
||||
.value = Automatic Updates
|
||||
|
||||
@ -347,3 +351,23 @@ expand-addon-button = More Options
|
||||
|
||||
addons-enabled-heading = Enabled
|
||||
addons-disabled-heading = Disabled
|
||||
|
||||
addon-detail-author-label = Author
|
||||
addon-detail-version-label = Version
|
||||
addon-detail-last-updated-label = Last Updated
|
||||
addon-detail-homepage-label = Homepage
|
||||
addon-detail-rating-label = Rating
|
||||
|
||||
# This string is used to show that an add-on is disabled.
|
||||
# Variables:
|
||||
# $name (string) - The name of the add-on
|
||||
addon-name-disabled = { $name } (disabled)
|
||||
|
||||
# The number of reviews that an add-on has received on AMO.
|
||||
# Variables:
|
||||
# $numberOfReviews (number) - The number of reviews received
|
||||
addon-detail-reviews-link =
|
||||
{ $numberOfReviews ->
|
||||
[one] { $numberOfReviews } review
|
||||
*[other] { $numberOfReviews } reviews
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
:root {
|
||||
--section-width: 664px;
|
||||
--addon-icon-size: 32px;
|
||||
}
|
||||
|
||||
#main {
|
||||
@ -25,17 +26,24 @@
|
||||
|
||||
.addon.card {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.addon.card:hover {
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.addon-card-collapsed {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
addon-list .addon.card {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.card-heading-icon {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: var(--addon-icon-size);
|
||||
height: var(--addon-icon-size);
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
@ -53,7 +61,9 @@
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--grey-90);
|
||||
line-height: 1;
|
||||
line-height: 22px;
|
||||
/* Subtract the top line-height so the text and icon align at the top. */
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.addon-description {
|
||||
@ -63,6 +73,21 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Prevent the content from wrapping unless expanded. */
|
||||
addon-card:not([expanded]) .card-contents {
|
||||
/* Card padding times 4: 2 times for card, 1 between icon and content,
|
||||
* 1 for the ... button. */
|
||||
width: calc(var(--section-width) - var(--card-padding) * 4 - var(--addon-icon-size));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Ellipsize if the content is too long. */
|
||||
addon-card:not([expanded]) .addon-name,
|
||||
addon-card:not([expanded]) .addon-description {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.more-options-menu {
|
||||
position: relative;
|
||||
/* Add some negative margin to account for the button's padding */
|
||||
@ -70,6 +95,91 @@
|
||||
margin-inline-end: -8px;
|
||||
}
|
||||
|
||||
addon-details {
|
||||
color: var(--grey-60);
|
||||
}
|
||||
|
||||
.addon-detail-description {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.addon-detail-contribute {
|
||||
padding: var(--card-padding);
|
||||
border: 1px solid var(--grey-90-a20);
|
||||
border-radius: var(--panel-border-radius);
|
||||
margin-bottom: var(--card-padding);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.addon-detail-contribute > label {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.addon-detail-contribute-button {
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
background-image: url("chrome://global/skin/icons/heart.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px;
|
||||
padding-inline-start: 28px;
|
||||
margin-top: var(--card-padding);
|
||||
margin-bottom: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.addon-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--grey-90-a20);
|
||||
margin: 0 calc(var(--card-padding) * -1);
|
||||
padding: var(--card-padding);
|
||||
}
|
||||
|
||||
.addon-detail-row:last-of-type {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.addon-detail-row {
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.addon-detail-rating {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.addon-detail-rating-star {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url("chrome://browser/skin/bookmark-hollow.svg");
|
||||
}
|
||||
|
||||
.addon-detail-rating-star[fill="full"] {
|
||||
background: url("chrome://browser/skin/bookmark.svg");
|
||||
}
|
||||
|
||||
.addon-detail-rating-star[fill="half"] {
|
||||
background: url("chrome://browser/skin/bookmark.svg");
|
||||
width: 8px;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.addon-detail-rating-star[fill="half"]::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
background: url("chrome://browser/skin/bookmark-hollow.svg");
|
||||
background-position: 8px;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.addon-detail-rating > a {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
button[action="more-options"] {
|
||||
min-width: auto;
|
||||
min-height: auto;
|
||||
|
@ -7,6 +7,7 @@
|
||||
<link rel="localization" href="branding/brand.ftl"/>
|
||||
<link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
|
||||
|
||||
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"></script>
|
||||
<script type="application/javascript" src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@ -15,19 +16,58 @@
|
||||
|
||||
<template name="card">
|
||||
<div class="card addon">
|
||||
<img class="card-heading-icon addon-icon"/>
|
||||
<div class="card-contents">
|
||||
<span class="addon-name"></span>
|
||||
<span class="addon-description"></span>
|
||||
<div class="addon-card-collapsed">
|
||||
<img class="card-heading-icon addon-icon"/>
|
||||
<div class="card-contents">
|
||||
<span class="addon-name"></span>
|
||||
<span class="addon-description"></span>
|
||||
</div>
|
||||
<div class="more-options-menu">
|
||||
<button class="ghost-button" action="more-options"></button>
|
||||
<panel-list>
|
||||
<panel-item action="toggle-disabled"></panel-item>
|
||||
<panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
|
||||
<panel-item-separator></panel-item-separator>
|
||||
<panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
|
||||
</panel-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="more-options-menu">
|
||||
<button class="ghost-button" action="more-options"></button>
|
||||
<panel-list>
|
||||
<panel-item action="toggle-disabled"></panel-item>
|
||||
<panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
|
||||
<panel-item-separator></panel-item-separator>
|
||||
<panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
|
||||
</panel-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="addon-details">
|
||||
<div class="addon-detail-description"></div>
|
||||
<div class="addon-detail-contribute">
|
||||
<label data-l10n-id="detail-contributions-description"></label>
|
||||
<button
|
||||
class="addon-detail-contribute-button"
|
||||
action="contribute"
|
||||
data-l10n-id="detail-contributions-button"
|
||||
data-l10n-attrs="accesskey">
|
||||
</button>
|
||||
</div>
|
||||
<div class="addon-detail-row addon-detail-row-author">
|
||||
<label data-l10n-id="addon-detail-author-label"></label>
|
||||
</div>
|
||||
<div class="addon-detail-row addon-detail-row-version">
|
||||
<label data-l10n-id="addon-detail-version-label"></label>
|
||||
</div>
|
||||
<div class="addon-detail-row addon-detail-row-lastUpdated">
|
||||
<label data-l10n-id="addon-detail-last-updated-label"></label>
|
||||
</div>
|
||||
<div class="addon-detail-row addon-detail-row-homepage">
|
||||
<label data-l10n-id="addon-detail-homepage-label"></label>
|
||||
<a target="_blank"></a>
|
||||
</div>
|
||||
<div class="addon-detail-row addon-detail-row-rating">
|
||||
<label data-l10n-id="addon-detail-rating-label"></label>
|
||||
<div class="addon-detail-rating">
|
||||
<span class="addon-detail-rating-star"></span>
|
||||
<span class="addon-detail-rating-star"></span>
|
||||
<span class="addon-detail-rating-star"></span>
|
||||
<span class="addon-detail-rating-star"></span>
|
||||
<span class="addon-detail-rating-star"></span>
|
||||
<a target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -3,14 +3,11 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* eslint max-len: ["error", 80] */
|
||||
/* exported initialize, hide, show */
|
||||
/* import-globals-from ../../../content/contentAreaUtils.js */
|
||||
/* global windowRoot */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {XPCOMUtils} = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
||||
});
|
||||
@ -26,6 +23,15 @@ function hasPermission(addon, permission) {
|
||||
return !!(addon.permissions & PERMISSION_MASKS[permission]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is set in initialize() by the parent about:addons window. It
|
||||
* is a helper for gViewController.loadView().
|
||||
*
|
||||
* @param {string} type The view type to load.
|
||||
* @param {string} param The (optional) param for the view.
|
||||
*/
|
||||
let loadViewFn;
|
||||
|
||||
let _templates = {};
|
||||
|
||||
/**
|
||||
@ -42,6 +48,19 @@ function importTemplate(name) {
|
||||
throw new Error(`Unknown template: ${name}`);
|
||||
}
|
||||
|
||||
function nl2br(text) {
|
||||
let frag = document.createDocumentFragment();
|
||||
let hasAppended = false;
|
||||
for (let part of text.split("\n")) {
|
||||
if (hasAppended) {
|
||||
frag.appendChild(document.createElement("br"));
|
||||
}
|
||||
frag.appendChild(new Text(part));
|
||||
hasAppended = true;
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
class PanelList extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["open"];
|
||||
@ -213,6 +232,122 @@ class PanelItem extends HTMLElement {
|
||||
}
|
||||
customElements.define("panel-item", PanelItem);
|
||||
|
||||
class AddonDetails extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.hasConnected = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.hasConnected) {
|
||||
this.hasConnected = true;
|
||||
this.render();
|
||||
this.addEventListener("click", this);
|
||||
}
|
||||
}
|
||||
|
||||
setAddon(addon) {
|
||||
this.addon = addon;
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (e.type == "click" && e.target.getAttribute("action") == "contribute") {
|
||||
openURL(this.addon.contributionURL);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {addon} = this;
|
||||
if (!addon) {
|
||||
throw new Error("addon-details must be initialized by setAddon");
|
||||
}
|
||||
|
||||
this.appendChild(importTemplate("addon-details"));
|
||||
|
||||
// Full description.
|
||||
let description = this.querySelector(".addon-detail-description");
|
||||
if (addon.getFullDescription) {
|
||||
description.appendChild(addon.getFullDescription(document));
|
||||
} else if (addon.fullDescription) {
|
||||
description.appendChild(nl2br(addon.fullDescription));
|
||||
}
|
||||
|
||||
// Contribute.
|
||||
if (!addon.contributionURL) {
|
||||
this.querySelector(".addon-detail-contribute").remove();
|
||||
}
|
||||
|
||||
// Author.
|
||||
let creatorRow = this.querySelector(".addon-detail-row-author");
|
||||
if (addon.creator) {
|
||||
let creator;
|
||||
if (addon.creator.url) {
|
||||
creator = document.createElement("a");
|
||||
creator.href = addon.creator.url;
|
||||
creator.target = "_blank";
|
||||
creator.textContent = addon.creator.name;
|
||||
} else {
|
||||
creator = new Text(addon.creator.name);
|
||||
}
|
||||
creatorRow.appendChild(creator);
|
||||
} else {
|
||||
creatorRow.remove();
|
||||
}
|
||||
|
||||
// Version. Don't show a version for LWTs.
|
||||
let version = this.querySelector(".addon-detail-row-version");
|
||||
if (addon.version && !/@personas\.mozilla\.org/.test(addon.id)) {
|
||||
version.appendChild(new Text(addon.version));
|
||||
} else {
|
||||
version.remove();
|
||||
}
|
||||
|
||||
// Last updated.
|
||||
let updateDate = this.querySelector(".addon-detail-row-lastUpdated");
|
||||
if (addon.updateDate) {
|
||||
let lastUpdated = addon.updateDate.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
updateDate.appendChild(new Text(lastUpdated));
|
||||
} else {
|
||||
updateDate.remove();
|
||||
}
|
||||
|
||||
// Homepage.
|
||||
let homepageRow = this.querySelector(".addon-detail-row-homepage");
|
||||
if (addon.homepageURL) {
|
||||
let homepageURL = homepageRow.querySelector("a");
|
||||
homepageURL.href = addon.homepageURL;
|
||||
homepageURL.textContent = addon.homepageURL;
|
||||
} else {
|
||||
homepageRow.remove();
|
||||
}
|
||||
|
||||
// Rating.
|
||||
let ratingRow = this.querySelector(".addon-detail-row-rating");
|
||||
if (addon.averageRating) {
|
||||
let stars = ratingRow.querySelectorAll(".addon-detail-rating-star");
|
||||
for (let i = 0; i < stars.length; i++) {
|
||||
let fill = "";
|
||||
if (addon.averageRating > i) {
|
||||
fill = addon.averageRating > i + 0.5 ? "full" : "half";
|
||||
}
|
||||
stars[i].setAttribute("fill", fill);
|
||||
}
|
||||
let reviews = ratingRow.querySelector("a");
|
||||
reviews.href = addon.reviewURL;
|
||||
document.l10n.setAttributes(reviews, "addon-detail-reviews-link", {
|
||||
numberOfReviews: addon.reviewCount,
|
||||
});
|
||||
} else {
|
||||
ratingRow.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("addon-details", AddonDetails);
|
||||
|
||||
/**
|
||||
* A card component for managing an add-on. It should be initialized by setting
|
||||
* the add-on with `setAddon()` before being connected to the document.
|
||||
@ -224,15 +359,33 @@ customElements.define("panel-item", PanelItem);
|
||||
class AddonCard extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.connected = false;
|
||||
this.hasRendered = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.connected) {
|
||||
return;
|
||||
// If we've already rendered we can just update, otherwise render.
|
||||
if (this.hasRendered) {
|
||||
this.update();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
this.registerListener();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeListener();
|
||||
}
|
||||
|
||||
get expanded() {
|
||||
return this.hasAttribute("expanded");
|
||||
}
|
||||
|
||||
set expanded(val) {
|
||||
if (val) {
|
||||
this.setAttribute("expanded", "true");
|
||||
} else {
|
||||
this.removeAttribute("expanded");
|
||||
}
|
||||
this.connected = true;
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,12 +398,34 @@ class AddonCard extends HTMLElement {
|
||||
this.addon = addon;
|
||||
}
|
||||
|
||||
registerListener() {
|
||||
AddonManager.addAddonListener(this);
|
||||
}
|
||||
|
||||
removeListener() {
|
||||
AddonManager.removeAddonListener(this);
|
||||
}
|
||||
|
||||
onDisabled(addon) {
|
||||
if (addon.id == this.addon.id) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
onEnabled(addon) {
|
||||
if (addon.id == this.addon.id) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the card's contents based on the previously set add-on. This should
|
||||
* be called if there has been a change to the add-on.
|
||||
*/
|
||||
update() {
|
||||
let {addon, card} = this;
|
||||
|
||||
// Update the icon.
|
||||
let icon;
|
||||
if (addon.type == "plugin") {
|
||||
icon = PLUGIN_ICON_URL;
|
||||
@ -258,20 +433,50 @@ class AddonCard extends HTMLElement {
|
||||
icon = AddonManager.getPreferredIconURL(addon, 32, window);
|
||||
}
|
||||
card.querySelector(".addon-icon").src = icon;
|
||||
card.querySelector(".addon-name").textContent = addon.name;
|
||||
|
||||
// Update the name.
|
||||
let name = card.querySelector(".addon-name");
|
||||
if (addon.isActive) {
|
||||
name.textContent = addon.name;
|
||||
name.removeAttribute("data-l10n-id");
|
||||
} else {
|
||||
document.l10n.setAttributes(name, "addon-name-disabled", {
|
||||
name: addon.name,
|
||||
});
|
||||
}
|
||||
|
||||
// Update description.
|
||||
card.querySelector(".addon-description").textContent = addon.description;
|
||||
|
||||
// Hide remove button if not allowed.
|
||||
let removeButton = card.querySelector('[action="remove"]');
|
||||
removeButton.hidden = !hasPermission(addon, "uninstall");
|
||||
|
||||
// Set disable label and hide if not allowed.
|
||||
let disableButton = card.querySelector('[action="toggle-disabled"]');
|
||||
let disableAction = addon.userDisabled ? "enable" : "disable";
|
||||
document.l10n.setAttributes(
|
||||
disableButton, `${disableAction}-addon-button`);
|
||||
disableButton.hidden = !hasPermission(addon, disableAction);
|
||||
|
||||
// If disable and remove are hidden, we don't need the separator.
|
||||
// The separator isn't needed when expanded (nothing under it) or when the
|
||||
// remove and disable buttons are hidden (nothing above it).
|
||||
let separator = card.querySelector("panel-item-separator");
|
||||
separator.hidden = removeButton.hidden && disableButton.hidden;
|
||||
separator.hidden = this.expanded ||
|
||||
removeButton.hidden && disableButton.hidden;
|
||||
|
||||
// Hide the expand button if we're expanded.
|
||||
card.querySelector('[action="expand"]').hidden = this.expanded;
|
||||
|
||||
this.sendEvent("update");
|
||||
}
|
||||
|
||||
expand() {
|
||||
if (!this.hasRendered) {
|
||||
this.expanded = true;
|
||||
} else {
|
||||
throw new Error("expand() is only supported before render()");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -289,13 +494,14 @@ class AddonCard extends HTMLElement {
|
||||
this.update();
|
||||
|
||||
let panel = this.card.querySelector("panel-list");
|
||||
|
||||
let moreOptionsButton = this.card.querySelector('[action="more-options"]');
|
||||
// Open on mousedown when the mouse is used.
|
||||
|
||||
// Open panel on mousedown when the mouse is used.
|
||||
moreOptionsButton.addEventListener("mousedown", (e) => {
|
||||
panel.toggle(e);
|
||||
});
|
||||
// Open when there's a click from the keyboard.
|
||||
|
||||
// Open panel on click from the keyboard.
|
||||
moreOptionsButton.addEventListener("click", (e) => {
|
||||
if (e.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
|
||||
panel.toggle(e);
|
||||
@ -328,10 +534,28 @@ class AddonCard extends HTMLElement {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "expand":
|
||||
loadViewFn("detail", this.addon.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.expanded) {
|
||||
let details = document.createElement("addon-details");
|
||||
details.setAddon(this.addon);
|
||||
this.card.appendChild(details);
|
||||
} else {
|
||||
// Expand on double click.
|
||||
this.addEventListener("dblclick", (e) => {
|
||||
// Don't expand if a button is double clicked.
|
||||
if (e.target.tagName != "BUTTON") {
|
||||
loadViewFn("detail", this.addon.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.appendChild(this.card);
|
||||
this.hasRendered = true;
|
||||
}
|
||||
|
||||
sendEvent(name, detail) {
|
||||
@ -356,15 +580,10 @@ customElements.define("addon-card", AddonCard);
|
||||
class AddonList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.connected = false;
|
||||
this.sections = [];
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.connected) {
|
||||
return;
|
||||
}
|
||||
this.connected = true;
|
||||
// Register the listener and get the add-ons, these operations should
|
||||
// happpen as close to each other as possible.
|
||||
this.registerListener();
|
||||
@ -374,7 +593,6 @@ class AddonList extends HTMLElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
// Remove content and stop listening until this is connected again.
|
||||
this.connected = false;
|
||||
this.textContent = "";
|
||||
this.removeListener();
|
||||
}
|
||||
@ -519,7 +737,6 @@ class AddonList extends HTMLElement {
|
||||
if (card) {
|
||||
let sectionIndex = this.sections.findIndex(s => s.filterFn(addon));
|
||||
if (sectionIndex != -1) {
|
||||
card.update();
|
||||
// Move the card, if needed. This will allow an animation between
|
||||
// page sections and provides clearer events for testing.
|
||||
if (card.parentNode.getAttribute("section") != sectionIndex) {
|
||||
@ -527,8 +744,6 @@ class AddonList extends HTMLElement {
|
||||
this.insertCardInto(card, sectionIndex);
|
||||
this.updateSectionIfEmpty(oldSection);
|
||||
this.sendEvent("move", {id: addon.id});
|
||||
} else {
|
||||
this.sendEvent("update", {id: addon.id});
|
||||
}
|
||||
} else {
|
||||
this.removeAddon(addon);
|
||||
@ -624,14 +839,34 @@ class ListView {
|
||||
}
|
||||
}
|
||||
|
||||
class DetailView {
|
||||
constructor({param, root}) {
|
||||
this.id = param;
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
async render() {
|
||||
let addon = await AddonManager.getAddonByID(this.id);
|
||||
let card = document.createElement("addon-card");
|
||||
card.setAddon(addon);
|
||||
card.expand();
|
||||
|
||||
// Go back to the list view when the add-on is removed.
|
||||
card.addEventListener("remove", () => loadViewFn("list", addon.type));
|
||||
|
||||
this.root.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic view management.
|
||||
let root = null;
|
||||
|
||||
/**
|
||||
* Called from extensions.js once, when about:addons is loading.
|
||||
*/
|
||||
function initialize() {
|
||||
function initialize(opts) {
|
||||
root = document.getElementById("main");
|
||||
loadViewFn = opts.loadViewFn;
|
||||
window.addEventListener("unload", () => {
|
||||
// Clear out the root node so the disconnectedCallback will trigger
|
||||
// properly and all of the custom elements can cleanup.
|
||||
@ -647,6 +882,8 @@ function initialize() {
|
||||
async function show(type, param) {
|
||||
if (type == "list") {
|
||||
await new ListView({param, root}).render();
|
||||
} else if (type == "detail") {
|
||||
await new DetailView({param, root}).render();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -783,14 +783,15 @@ var gViewController = {
|
||||
|
||||
this.viewObjects.discover = gDiscoverView;
|
||||
this.viewObjects.legacy = gLegacyView;
|
||||
this.viewObjects.detail = gDetailView;
|
||||
this.viewObjects.updates = gUpdatesView;
|
||||
this.viewObjects.shortcuts = gShortcutsView;
|
||||
|
||||
if (useHtmlViews) {
|
||||
this.viewObjects.list = htmlView("list");
|
||||
this.viewObjects.detail = htmlView("detail");
|
||||
} else {
|
||||
this.viewObjects.list = gListView;
|
||||
this.viewObjects.detail = gDetailView;
|
||||
}
|
||||
|
||||
for (let type in this.viewObjects) {
|
||||
@ -3884,6 +3885,16 @@ var gBrowser = {
|
||||
}, true);
|
||||
}
|
||||
|
||||
const htmlViewOpts = {
|
||||
loadViewFn(type, param) {
|
||||
let viewId = `addons://${type}`;
|
||||
if (param) {
|
||||
viewId += "/" + encodeURIComponent(param);
|
||||
}
|
||||
gViewController.loadView(viewId);
|
||||
},
|
||||
};
|
||||
|
||||
// View wrappers for the HTML version of about:addons. These delegate to an
|
||||
// HTML browser that renders the actual views.
|
||||
let htmlBrowser;
|
||||
@ -3896,7 +3907,7 @@ function getHtmlBrowser() {
|
||||
});
|
||||
htmlBrowserLoaded = new Promise(
|
||||
resolve => htmlBrowser.addEventListener("load", resolve, {once: true})
|
||||
).then(() => htmlBrowser.contentWindow.initialize());
|
||||
).then(() => htmlBrowser.contentWindow.initialize(htmlViewOpts));
|
||||
}
|
||||
return htmlBrowser;
|
||||
}
|
||||
@ -3904,7 +3915,7 @@ function getHtmlBrowser() {
|
||||
function htmlView(type) {
|
||||
return {
|
||||
node: null,
|
||||
isRoot: true,
|
||||
isRoot: type != "detail",
|
||||
|
||||
initialize() {
|
||||
this.node = getHtmlBrowser();
|
||||
|
@ -117,3 +117,4 @@ skip-if = os == 'linux' || (os == 'mac' && debug) # bug 1483347
|
||||
[browser_webext_options_addon_reload.js]
|
||||
tags = webextensions
|
||||
[browser_html_list_view.js]
|
||||
[browser_html_detail_view.js]
|
||||
|
@ -0,0 +1,322 @@
|
||||
/* eslint max-len: ["error", 80] */
|
||||
|
||||
let gProvider;
|
||||
let promptService;
|
||||
|
||||
function getAddonCard(doc, addonId) {
|
||||
return doc.querySelector(`addon-card[addon-id="${addonId}"]`);
|
||||
}
|
||||
|
||||
function checkLabel(row, name) {
|
||||
is(row.ownerDocument.l10n.getAttributes(row.querySelector("label")).id,
|
||||
`addon-detail-${name}-label`, `The ${name} label is set`);
|
||||
}
|
||||
|
||||
function checkLink(link, url, text = url) {
|
||||
ok(link, "There is a link");
|
||||
is(link.href, url, "The link goes to the URL");
|
||||
if (text instanceof Object) {
|
||||
// Check the fluent data.
|
||||
Assert.deepEqual(
|
||||
link.ownerDocument.l10n.getAttributes(link),
|
||||
text, "The fluent data is set correctly");
|
||||
} else {
|
||||
// Just check text.
|
||||
is(link.textContent, text, "The text is set");
|
||||
}
|
||||
is(link.getAttribute("target"), "_blank", "The link opens in a new tab");
|
||||
}
|
||||
|
||||
add_task(async function enableHtmlViews() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["extensions.htmlaboutaddons.enabled", true]],
|
||||
});
|
||||
|
||||
gProvider = new MockProvider();
|
||||
gProvider.createAddons([{
|
||||
id: "addon1@mochi.test",
|
||||
name: "Test add-on 1",
|
||||
creator: {name: "The creator", url: "http://example.com/me"},
|
||||
version: "3.1",
|
||||
description: "Short description",
|
||||
fullDescription: "Longer description\nWith brs!",
|
||||
type: "extension",
|
||||
contributionURL: "http://foo.com",
|
||||
averageRating: 4.3,
|
||||
reviewCount: 5,
|
||||
reviewURL: "http://example.com/reviews",
|
||||
homepageURL: "http://example.com/addon1",
|
||||
updateDate: new Date("2019-03-07T01:00:00"),
|
||||
}, {
|
||||
id: "addon2@mochi.test",
|
||||
name: "Test add-on 2",
|
||||
creator: {name: "I made it"},
|
||||
description: "Short description",
|
||||
type: "extension",
|
||||
}]);
|
||||
|
||||
promptService = mockPromptService();
|
||||
});
|
||||
|
||||
add_task(async function testOpenDetailView() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
name: "Test",
|
||||
applications: {gecko: {id: "test@mochi.test"}},
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
|
||||
let win = await loadInitialView("extension");
|
||||
let doc = win.document;
|
||||
|
||||
// Test double click on card to open details.
|
||||
let card = getAddonCard(doc, "test@mochi.test");
|
||||
ok(!card.querySelector("addon-details"), "The card doesn't have details");
|
||||
let loaded = waitForViewLoad(win);
|
||||
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
|
||||
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 2}, win);
|
||||
await loaded;
|
||||
|
||||
card = getAddonCard(doc, "test@mochi.test");
|
||||
ok(card.querySelector("addon-details"), "The card now has details");
|
||||
|
||||
loaded = waitForViewLoad(win);
|
||||
win.managerWindow.document.getElementById("go-back").click();
|
||||
await loaded;
|
||||
|
||||
// Test using more options menu.
|
||||
card = getAddonCard(doc, "test@mochi.test");
|
||||
loaded = waitForViewLoad(win);
|
||||
card.querySelector('[action="expand"]').click();
|
||||
await loaded;
|
||||
|
||||
card = getAddonCard(doc, "test@mochi.test");
|
||||
ok(card.querySelector("addon-details"), "The card now has details");
|
||||
|
||||
await closeView(win);
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function testDetailOperations() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
name: "Test",
|
||||
applications: {gecko: {id: "test@mochi.test"}},
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
|
||||
let win = await loadInitialView("extension");
|
||||
let doc = win.document;
|
||||
|
||||
let card = getAddonCard(doc, "test@mochi.test");
|
||||
ok(!card.querySelector("addon-details"), "The card doesn't have details");
|
||||
let loaded = waitForViewLoad(win);
|
||||
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
|
||||
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 2}, win);
|
||||
await loaded;
|
||||
|
||||
card = getAddonCard(doc, "test@mochi.test");
|
||||
let panel = card.querySelector("panel-list");
|
||||
|
||||
// Check button visibility.
|
||||
let disableButton = panel.querySelector('[action="toggle-disabled"]');
|
||||
ok(!disableButton.hidden, "The disable button is visible");
|
||||
|
||||
let removeButton = panel.querySelector('[action="remove"]');
|
||||
ok(!removeButton.hidden, "The remove button is visible");
|
||||
|
||||
let separator = panel.querySelector("panel-item-separator");
|
||||
ok(separator.hidden, "The separator is hidden");
|
||||
|
||||
let expandButton = panel.querySelector('[action="expand"]');
|
||||
ok(expandButton.hidden, "The expand button is hidden");
|
||||
|
||||
// Check toggling disabled.
|
||||
let name = card.querySelector(".addon-name");
|
||||
is(name.textContent, "Test", "The name is set when enabled");
|
||||
is(doc.l10n.getAttributes(name).id, "", "There is no l10n name");
|
||||
|
||||
// Disable the extension.
|
||||
let disableToggled = BrowserTestUtils.waitForEvent(card, "update");
|
||||
disableButton.click();
|
||||
await disableToggled;
|
||||
|
||||
// The (disabled) text should be shown now.
|
||||
Assert.deepEqual(
|
||||
doc.l10n.getAttributes(name),
|
||||
{id: "addon-name-disabled", args: {name: "Test"}},
|
||||
"The name is updated to the disabled text");
|
||||
|
||||
// Enable the add-on.
|
||||
disableToggled = BrowserTestUtils.waitForEvent(card, "update");
|
||||
disableButton.click();
|
||||
await disableToggled;
|
||||
|
||||
// Name is just the add-on name again.
|
||||
is(name.textContent, "Test", "The name is reset when enabled");
|
||||
is(doc.l10n.getAttributes(name).id, "", "There is no l10n name");
|
||||
|
||||
// Remove but cancel.
|
||||
let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled");
|
||||
removeButton.click();
|
||||
await cancelled;
|
||||
|
||||
|
||||
// Remove the extension.
|
||||
let viewChanged = waitForViewLoad(win);
|
||||
// Tell the mock prompt service that the prompt was accepted.
|
||||
promptService._response = 0;
|
||||
removeButton.click();
|
||||
await viewChanged;
|
||||
|
||||
// We're on the list view now and there's no card for this extension.
|
||||
ok(doc.querySelector("addon-list"), "There's an addon-list now");
|
||||
ok(!getAddonCard(doc, "test@mochi.test"),
|
||||
"The extension no longer has a card");
|
||||
let addon = await AddonManager.getAddonByID("test@mochi.test");
|
||||
ok(!addon, "The extension can't be found now");
|
||||
|
||||
await closeView(win);
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function testFullDetails() {
|
||||
let win = await loadInitialView("extension");
|
||||
let doc = win.document;
|
||||
|
||||
// The list card.
|
||||
let card = getAddonCard(doc, "addon1@mochi.test");
|
||||
ok(!card.hasAttribute("expanded"), "The list card is not expanded");
|
||||
let loaded = waitForViewLoad(win);
|
||||
card.querySelector('[action="expand"]').click();
|
||||
await loaded;
|
||||
|
||||
// This is now the detail card.
|
||||
card = getAddonCard(doc, "addon1@mochi.test");
|
||||
ok(card.hasAttribute("expanded"), "The detail card is expanded");
|
||||
|
||||
let details = card.querySelector("addon-details");
|
||||
let desc = details.querySelector(".addon-detail-description");
|
||||
is(desc.innerHTML, "Longer description<br>With brs!",
|
||||
"The full description replaces newlines with <br>");
|
||||
|
||||
let contrib = details.querySelector(".addon-detail-contribute");
|
||||
ok(contrib, "The contribution section is visible");
|
||||
|
||||
let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
|
||||
|
||||
// The first row is the author.
|
||||
let row = rows.shift();
|
||||
checkLabel(row, "author");
|
||||
let link = row.querySelector("a");
|
||||
checkLink(link, "http://example.com/me", "The creator");
|
||||
|
||||
// The version is next.
|
||||
row = rows.shift();
|
||||
checkLabel(row, "version");
|
||||
let text = row.lastChild;
|
||||
is(text.textContent, "3.1", "The version is set");
|
||||
|
||||
// Last updated is next.
|
||||
row = rows.shift();
|
||||
checkLabel(row, "last-updated");
|
||||
text = row.lastChild;
|
||||
is(text.textContent, "March 7, 2019", "The last updated date is set");
|
||||
|
||||
// Homepage.
|
||||
row = rows.shift();
|
||||
checkLabel(row, "homepage");
|
||||
link = row.querySelector("a");
|
||||
checkLink(link, "http://example.com/addon1");
|
||||
|
||||
// Reviews.
|
||||
row = rows.shift();
|
||||
checkLabel(row, "rating");
|
||||
let rating = row.lastElementChild;
|
||||
ok(rating.classList.contains("addon-detail-rating"), "Found the rating el");
|
||||
let stars = Array.from(rating.querySelectorAll(".addon-detail-rating-star"));
|
||||
let fullAttrs = stars.map(star => star.getAttribute("fill")).join(",");
|
||||
is(fullAttrs, "full,full,full,full,half", "Four and a half stars are full");
|
||||
link = rating.querySelector("a");
|
||||
checkLink(link, "http://example.com/reviews", {
|
||||
id: "addon-detail-reviews-link",
|
||||
args: {numberOfReviews: 5},
|
||||
});
|
||||
|
||||
// That should've been all the rows.
|
||||
is(rows.length, 0, "There are no more rows left");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
add_task(async function testMinimalExtension() {
|
||||
let win = await loadInitialView("extension");
|
||||
let doc = win.document;
|
||||
|
||||
let card = getAddonCard(doc, "addon2@mochi.test");
|
||||
ok(!card.hasAttribute("expanded"), "The list card is not expanded");
|
||||
let loaded = waitForViewLoad(win);
|
||||
card.querySelector('[action="expand"]').click();
|
||||
await loaded;
|
||||
|
||||
card = getAddonCard(doc, "addon2@mochi.test");
|
||||
let details = card.querySelector("addon-details");
|
||||
|
||||
let desc = details.querySelector(".addon-detail-description");
|
||||
is(desc.textContent, "", "There is no full description");
|
||||
|
||||
let contrib = details.querySelector(".addon-detail-contribute");
|
||||
ok(!contrib, "There is no contribution element");
|
||||
|
||||
let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
|
||||
let row = rows.shift();
|
||||
checkLabel(row, "author");
|
||||
let text = row.lastChild;
|
||||
is(text.textContent, "I made it", "The author is set");
|
||||
ok(text instanceof Text, "The author is a text node");
|
||||
|
||||
is(rows.length, 0, "There was only 1 row");
|
||||
|
||||
await closeView(win);
|
||||
});
|
||||
|
||||
add_task(async function testDefaultTheme() {
|
||||
let win = await loadInitialView("theme");
|
||||
let doc = win.document;
|
||||
|
||||
// The list card.
|
||||
let card = getAddonCard(doc, "default-theme@mozilla.org");
|
||||
ok(!card.hasAttribute("expanded"), "The list card is not expanded");
|
||||
let loaded = waitForViewLoad(win);
|
||||
card.querySelector('[action="expand"]').click();
|
||||
await loaded;
|
||||
|
||||
card = getAddonCard(doc, "default-theme@mozilla.org");
|
||||
let rows = Array.from(card.querySelectorAll(".addon-detail-row"));
|
||||
|
||||
// Author.
|
||||
let author = rows.shift();
|
||||
checkLabel(author, "author");
|
||||
let text = author.lastChild;
|
||||
is(text.textContent, "Mozilla", "The author is set");
|
||||
|
||||
// Version.
|
||||
let version = rows.shift();
|
||||
checkLabel(version, "version");
|
||||
is(version.lastChild.textContent, "1.0", "It's always version 1.0");
|
||||
|
||||
// Last updated.
|
||||
let lastUpdated = rows.shift();
|
||||
checkLabel(lastUpdated, "last-updated");
|
||||
ok(lastUpdated.lastChild.textContent, "There is a date set");
|
||||
|
||||
is(rows.length, 0, "There are no more rows");
|
||||
|
||||
await closeView(win);
|
||||
});
|
@ -2,22 +2,6 @@
|
||||
|
||||
let promptService;
|
||||
|
||||
let gManagerWindow;
|
||||
let gCategoryUtilities;
|
||||
|
||||
async function loadInitialView(type) {
|
||||
gManagerWindow = await open_manager(null);
|
||||
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
|
||||
await gCategoryUtilities.openType(type);
|
||||
|
||||
let browser = gManagerWindow.document.getElementById("html-view-browser");
|
||||
return browser.contentWindow;
|
||||
}
|
||||
|
||||
function closeView() {
|
||||
return close_manager(gManagerWindow);
|
||||
}
|
||||
|
||||
const SECTION_INDEXES = {
|
||||
enabled: 0,
|
||||
disabled: 1,
|
||||
|
@ -1468,6 +1468,25 @@ function assertTelemetryMatches(events, {filterMethods} = {}) {
|
||||
}
|
||||
|
||||
/* HTML view helpers */
|
||||
async function loadInitialView(type) {
|
||||
let managerWindow = await open_manager(null);
|
||||
let categoryUtilities = new CategoryUtilities(managerWindow);
|
||||
await categoryUtilities.openType(type);
|
||||
|
||||
let browser = managerWindow.document.getElementById("html-view-browser");
|
||||
let win = browser.contentWindow;
|
||||
win.managerWindow = managerWindow;
|
||||
return win;
|
||||
}
|
||||
|
||||
function waitForViewLoad(win) {
|
||||
return wait_for_view_load(win.managerWindow, undefined, true);
|
||||
}
|
||||
|
||||
function closeView(win) {
|
||||
return close_manager(win.managerWindow);
|
||||
}
|
||||
|
||||
function mockPromptService() {
|
||||
let {prompt} = Services;
|
||||
let promptService = {
|
||||
|
Loading…
Reference in New Issue
Block a user