summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/megalist/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/megalist/content')
-rw-r--r--toolkit/components/satchel/megalist/content/Dialog.mjs116
-rw-r--r--toolkit/components/satchel/megalist/content/MegalistView.mjs98
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.css93
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.ftl28
-rw-r--r--toolkit/components/satchel/megalist/content/megalist.html175
-rw-r--r--toolkit/components/satchel/megalist/content/search-input.mjs36
6 files changed, 472 insertions, 74 deletions
diff --git a/toolkit/components/satchel/megalist/content/Dialog.mjs b/toolkit/components/satchel/megalist/content/Dialog.mjs
new file mode 100644
index 0000000000..f2eca6376c
--- /dev/null
+++ b/toolkit/components/satchel/megalist/content/Dialog.mjs
@@ -0,0 +1,116 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+const GENERIC_DIALOG_TEMPLATE = document.querySelector("#dialog-template");
+
+const DIALOGS = {
+ "remove-login": {
+ template: "#remove-login-dialog-template",
+ },
+ "export-logins": {
+ template: "#export-logins-dialog-template",
+ },
+ "remove-logins": {
+ template: "#remove-logins-dialog-template",
+ callback: dialog => {
+ const primaryButton = dialog.querySelector("button.primary");
+ const checkbox = dialog.querySelector(".confirm-checkbox");
+ const toggleButton = () => (primaryButton.disabled = !checkbox.checked);
+ checkbox.addEventListener("change", toggleButton);
+ toggleButton();
+ },
+ },
+ "import-logins": {
+ template: "#import-logins-dialog-template",
+ },
+ "import-error": {
+ template: "#import-error-dialog-template",
+ },
+};
+
+/**
+ * Setup dismiss and command handling logic for the dialog overlay.
+ *
+ * @param {Element} overlay - The overlay element containing the dialog
+ * @param {Function} messageHandler - Function to send message back to view model.
+ */
+const setupControls = (overlay, messageHandler) => {
+ const dialog = overlay.querySelector(".dialog-container");
+ const commandButtons = dialog.querySelectorAll("[data-command]");
+ for (const commandButton of commandButtons) {
+ const commandId = commandButton.dataset.command;
+ commandButton.addEventListener("click", () => messageHandler(commandId));
+ }
+
+ dialog.querySelectorAll("[close-dialog]").forEach(element => {
+ element.addEventListener("click", cancelDialog, { once: true });
+ });
+
+ document.addEventListener("keyup", function handleKeyUp(ev) {
+ if (ev.key === "Escape") {
+ cancelDialog();
+ document.removeEventListener("keyup", handleKeyUp);
+ }
+ });
+
+ document.addEventListener("click", function handleClickOutside(ev) {
+ if (!dialog.contains(ev.target)) {
+ cancelDialog();
+ document.removeEventListener("click", handleClickOutside);
+ }
+ });
+ dialog.querySelector("[autofocus]")?.focus();
+};
+
+/**
+ * Add data-l10n-args to elements with localizable attribute
+ *
+ * @param {Element} dialog - The dialog element.
+ * @param {Array<object>} l10nArgs - List of localization arguments.
+ */
+const populateL10nArgs = (dialog, l10nArgs) => {
+ const localizableElements = dialog.querySelectorAll("[localizable]");
+ for (const [index, localizableElement] of localizableElements.entries()) {
+ localizableElement.dataset.l10nArgs = JSON.stringify(l10nArgs[index]) ?? "";
+ }
+};
+
+/**
+ * Remove the currently displayed dialog overlay from the DOM.
+ */
+export const cancelDialog = () =>
+ document.querySelector(".dialog-overlay")?.remove();
+
+/**
+ * Create a new dialog overlay and populate it using the specified template and data.
+ *
+ * @param {object} dialogData - Data required to populate the dialog, includes template and localization args.
+ * @param {Function} messageHandler - Function to send message back to view model.
+ */
+export const createDialog = (dialogData, messageHandler) => {
+ const templateData = DIALOGS[dialogData?.id];
+
+ const genericTemplateClone = document.importNode(
+ GENERIC_DIALOG_TEMPLATE.content,
+ true
+ );
+
+ const overlay = genericTemplateClone.querySelector(".dialog-overlay");
+ const dialog = genericTemplateClone.querySelector(".dialog-container");
+
+ const overrideTemplate = document.querySelector(templateData.template);
+ const overrideTemplateClone = document.importNode(
+ overrideTemplate.content,
+ true
+ );
+
+ genericTemplateClone
+ .querySelector(".dialog-wrapper")
+ .appendChild(overrideTemplateClone);
+
+ populateL10nArgs(genericTemplateClone, dialogData.l10nArgs);
+ setupControls(overlay, messageHandler);
+ document.body.appendChild(genericTemplateClone);
+ templateData?.callback?.(dialog, messageHandler);
+};
diff --git a/toolkit/components/satchel/megalist/content/MegalistView.mjs b/toolkit/components/satchel/megalist/content/MegalistView.mjs
index 44a0198692..feec2409f8 100644
--- a/toolkit/components/satchel/megalist/content/MegalistView.mjs
+++ b/toolkit/components/satchel/megalist/content/MegalistView.mjs
@@ -4,13 +4,14 @@
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import {
+ createDialog,
+ cancelDialog,
+} from "chrome://global/content/megalist/Dialog.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/megalist/VirtualizedList.mjs";
-// eslint-disable-next-line import/no-unassigned-import
-import "chrome://global/content/megalist/search-input.mjs";
-
/**
* Map with limit on how many entries it can have.
* When over limit entries are added, oldest one are removed.
@@ -77,6 +78,7 @@ export class MegalistView extends MozLitElement {
super();
this.selectedIndex = 0;
this.searchText = "";
+ this.layout = null;
window.addEventListener("MessageFromViewModel", ev =>
this.#onMessageFromViewModel(ev)
@@ -88,6 +90,7 @@ export class MegalistView extends MozLitElement {
listLength: { type: Number },
selectedIndex: { type: Number },
searchText: { type: String },
+ layout: { type: Object },
};
}
@@ -112,6 +115,10 @@ export class MegalistView extends MozLitElement {
#templates = {};
+ static queries = {
+ searchInput: ".search",
+ };
+
connectedCallback() {
super.connectedCallback();
this.ownerDocument.addEventListener("keydown", e => this.#handleKeydown(e));
@@ -296,6 +303,16 @@ export class MegalistView extends MozLitElement {
this.requestUpdate();
}
+ receiveSetLayout({ layout }) {
+ if (layout) {
+ createDialog(layout, commandId =>
+ this.#messageToViewModel("Command", { commandId })
+ );
+ } else {
+ cancelDialog();
+ }
+ }
+
#handleInputChange(e) {
const searchText = e.target.value;
this.#messageToViewModel("UpdateFilter", { searchText });
@@ -333,6 +350,15 @@ export class MegalistView extends MozLitElement {
}
#handleClick(e) {
+ const elementWithCommand = e.composedTarget.closest("[data-command]");
+ if (elementWithCommand) {
+ const commandId = elementWithCommand.dataset.command;
+ if (commandId) {
+ this.#messageToViewModel("Command", { commandId });
+ return;
+ }
+ }
+
const lineElement = e.composedTarget.closest(".line");
if (!lineElement) {
return;
@@ -360,6 +386,12 @@ export class MegalistView extends MozLitElement {
const popup = this.ownerDocument.createElement("div");
popup.className = "menuPopup";
+
+ let closeMenu = () => {
+ popup.remove();
+ this.searchInput.focus();
+ };
+
popup.addEventListener(
"keydown",
e => {
@@ -385,7 +417,7 @@ export class MegalistView extends MozLitElement {
switch (e.code) {
case "Escape":
- popup.remove();
+ closeMenu();
break;
case "Tab":
if (e.shiftKey) {
@@ -416,9 +448,7 @@ export class MegalistView extends MozLitElement {
e.composedTarget?.closest(".menuPopup") !=
e.relatedTarget?.closest(".menuPopup")
) {
- // TODO: this triggers on macOS before "click" event. Due to this,
- // we are not receiving the command.
- popup.remove();
+ closeMenu();
}
},
{ capture: true }
@@ -433,7 +463,7 @@ export class MegalistView extends MozLitElement {
}
const menuItem = this.ownerDocument.createElement("button");
- menuItem.textContent = command.label;
+ menuItem.setAttribute("data-l10n-id", command.label);
menuItem.addEventListener("click", e => {
this.#messageToViewModel("Command", {
snapshotId,
@@ -449,26 +479,50 @@ export class MegalistView extends MozLitElement {
popup.querySelector("button")?.focus();
}
+ /**
+ * Renders data-source specific UI that should be displayed before the
+ * virtualized list. This is determined by the "SetLayout" message provided
+ * by the View Model. Defaults to displaying the search input.
+ */
+ renderBeforeList() {
+ return html`
+ <input
+ class="search"
+ type="search"
+ data-l10n-id="filter-placeholder"
+ .value=${this.searchText}
+ @input=${e => this.#handleInputChange(e)}
+ />
+ `;
+ }
+
+ renderList() {
+ if (this.layout) {
+ return null;
+ }
+
+ return html` <virtualized-list
+ .lineCount=${this.listLength}
+ .lineHeight=${MegalistView.LINE_HEIGHT}
+ .selectedIndex=${this.selectedIndex}
+ .createLineElement=${index => this.createLineElement(index)}
+ @click=${e => this.#handleClick(e)}
+ >
+ </virtualized-list>`;
+ }
+
+ renderAfterList() {}
+
render() {
return html`
<link
rel="stylesheet"
href="chrome://global/content/megalist/megalist.css"
/>
- <div class="container">
- <search-input
- .value=${this.searchText}
- .change=${e => this.#handleInputChange(e)}
- >
- </search-input>
- <virtualized-list
- .lineCount=${this.listLength}
- .lineHeight=${MegalistView.LINE_HEIGHT}
- .selectedIndex=${this.selectedIndex}
- .createLineElement=${index => this.createLineElement(index)}
- @click=${e => this.#handleClick(e)}
- >
- </virtualized-list>
+ <div @click=${this.#handleClick} class="container">
+ <div class="beforeList">${this.renderBeforeList()}</div>
+ ${this.renderList()}
+ <div class="afterList">${this.renderAfterList()}</div>
</div>
`;
}
diff --git a/toolkit/components/satchel/megalist/content/megalist.css b/toolkit/components/satchel/megalist/content/megalist.css
index b442a7b60d..3f8bb9de2c 100644
--- a/toolkit/components/satchel/megalist/content/megalist.css
+++ b/toolkit/components/satchel/megalist/content/megalist.css
@@ -8,18 +8,27 @@
display: flex;
flex-direction: column;
justify-content: center;
- max-height: 100vh;
+ height: 100vh;
- > search-input {
+ > .beforeList {
margin: 20px;
+
+ .search {
+ padding: 8px;
+ border-radius: 4px;
+ border: 1px solid var(--in-content-border-color);
+ box-sizing: border-box;
+ width: 100%;
+ }
}
}
virtualized-list {
position: relative;
overflow: auto;
- margin: 20px;
-
+ margin-block: 20px;
+ padding-inline: 20px;
+ flex-grow: 1;
.lines-container {
padding-inline-start: unset;
}
@@ -29,7 +38,7 @@ virtualized-list {
display: flex;
align-items: stretch;
position: absolute;
- width: 100%;
+ width: calc(100% - 40px);
user-select: none;
box-sizing: border-box;
height: 64px;
@@ -93,11 +102,19 @@ virtualized-list {
> .content {
flex-grow: 1;
+ &:not(.section) {
+ display: grid;
+ grid-template-rows: max-content 1fr;
+ grid-template-columns: max-content;
+ grid-column-gap: 8px;
+ padding-inline-start: 8px;
+ padding-block-start: 4px;
+ }
+
> div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- padding-inline-start: 10px;
&:last-child {
padding-block-end: 10px;
@@ -115,6 +132,8 @@ virtualized-list {
> .label {
color: var(--text-color-deemphasized);
padding-block: 2px 4px;
+ grid-row: 1;
+ align-content: end;
}
> .value {
@@ -125,7 +144,7 @@ virtualized-list {
fill: currentColor;
width: auto;
height: 16px;
- margin-inline: 4px;
+ margin-inline-end: 4px;
vertical-align: text-bottom;
}
@@ -139,12 +158,14 @@ virtualized-list {
}
> .stickers {
- text-align: end;
- margin-block-start: 2px;
+ grid-row: 1;
+ align-content: start;
> span {
- padding: 2px;
+ padding: 4px;
margin-inline-end: 2px;
+ border-radius: 24px;
+ font-size: xx-small;
}
/* Hard-coded colors will be addressed in FXCM-1013 */
@@ -159,6 +180,12 @@ virtualized-list {
border: 1px solid maroon;
color: whitesmoke;
}
+
+ > span.error {
+ background-color: orange;
+ border: 1px solid orangered;
+ color: black;
+ }
}
&.section {
@@ -199,10 +226,46 @@ virtualized-list {
}
}
-.search {
- padding: 8px;
- border-radius: 4px;
- border: 1px solid var(--in-content-border-color);
- box-sizing: border-box;
+/* Dialog styles */
+.dialog-overlay {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 16px;
+ position: fixed;
+ top: 0;
+ left: 0;
width: 100%;
+ height: 100%;
+ z-index: 1;
+ background-color: rgba(0, 0, 0, 0.5);
+ box-sizing: border-box;
+ /* TODO: probably want to remove this later ? */
+ backdrop-filter: blur(6px);
+}
+
+.dialog-container {
+ display: grid;
+ padding: 16px 32px;
+ color: var(--in-content-text-color);
+ background-color: var(--in-content-box-background);
+ border: 1px solid var(--in-content-border-color);
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
+}
+
+.dialog-title {
+ margin: 0;
+}
+
+.dismiss-button {
+ justify-self: end;
+}
+
+.dialog-content {
+ margin-block-start: 16px;
+ margin-block-end: 16px;
+
+ .checkbox-text {
+ margin-block-start: 8px;
+ }
}
diff --git a/toolkit/components/satchel/megalist/content/megalist.ftl b/toolkit/components/satchel/megalist/content/megalist.ftl
index 69d085a7c5..4089477add 100644
--- a/toolkit/components/satchel/megalist/content/megalist.ftl
+++ b/toolkit/components/satchel/megalist/content/megalist.ftl
@@ -23,6 +23,7 @@ command-cancel = Cancel
passwords-section-label = Passwords
passwords-disabled = Passwords are disabled
+passwords-dismiss-breach-alert-command = Dismiss breach alert
passwords-command-create = Add Password
passwords-command-import = Import from a File…
passwords-command-export = Export Passwords…
@@ -65,6 +66,33 @@ passwords-filtered-count =
*[other] { $count } of { $total } passwords
}
+# Confirm the removal of all saved passwords
+# $total (number) - Total number of passwords
+passwords-remove-all-title =
+ { $total ->
+ [one] Remove { $total } password?
+ *[other] Remove all { $total } passwords?
+ }
+
+# Checkbox label to confirm the removal of saved passwords
+# $total (number) - Total number of passwords
+passwords-remove-all-confirm =
+ { $total ->
+ [1] Yes, remove password
+ *[other] Yes, remove passwords
+ }
+
+# Button label to confirm removal of saved passwords
+passwords-remove-all-confirm-button = Confirm
+
+# Message to confirm the removal of saved passwords
+# $total (number) - Total number of passwords
+passwords-remove-all-message =
+ { $total ->
+ [1] This will remove your saved password and any breach alerts. You cannot undo this action.
+ *[other] This will remove your saved passwords and any breach alerts. You cannot undo this action.
+ }
+
passwords-origin-label = Website address
passwords-username-label = Username
passwords-password-label = Password
diff --git a/toolkit/components/satchel/megalist/content/megalist.html b/toolkit/components/satchel/megalist/content/megalist.html
index 6ff3f089fc..9d15587033 100644
--- a/toolkit/components/satchel/megalist/content/megalist.html
+++ b/toolkit/components/satchel/megalist/content/megalist.html
@@ -15,7 +15,12 @@
src="chrome://global/content/megalist/MegalistView.mjs"
></script>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/megalist/megalist.css"
+ />
<link rel="localization" href="preview/megalist.ftl" />
+ <link rel="localization" href="browser/aboutLogins.ftl" />
</head>
<body>
@@ -56,11 +61,11 @@
<template id="lineTemplate">
<div class="content">
<div class="label"></div>
+ <div class="stickers"></div>
<div class="value">
<img class="icon" />
<span></span>
</div>
- <div class="stickers"></div>
</div>
</template>
@@ -74,5 +79,173 @@
<div class="stickers"></div>
</div>
</template>
+
+ <template id="dialog-template">
+ <div class="dialog-overlay">
+ <div class="dialog-container">
+ <moz-button
+ data-l10n-id="confirmation-dialog-dismiss-button"
+ iconSrc="chrome://global/skin/icons/close.svg"
+ size="small"
+ type="icon ghost"
+ class="dismiss-button"
+ close-dialog
+ >
+ </moz-button>
+ <div class="dialog-wrapper"></div>
+ </div>
+ </div>
+ </template>
+
+ <template id="remove-logins-dialog-template">
+ <h2
+ class="dialog-title"
+ data-l10n-id="about-logins-confirm-remove-all-sync-dialog-title2"
+ localizable
+ ></h2>
+ <div class="dialog-content" slot="dialog-content">
+ <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p>
+ <label>
+ <input type="checkbox" class="confirm-checkbox checkbox" autofocus />
+ <span
+ class="checkbox-text"
+ data-l10n-id="about-logins-confirm-remove-all-dialog-checkbox-label2"
+ ></span>
+ </label>
+ </div>
+ <moz-button-group>
+ <button
+ class="primary danger-button"
+ data-l10n-id="about-logins-confirm-remove-all-dialog-confirm-button-label"
+ data-command="LoginDataSource.confirmRemoveAll"
+ ></button>
+ <button
+ close-dialog
+ data-l10n-id="confirmation-dialog-cancel-button"
+ ></button>
+ </moz-button-group>
+ </template>
+
+ <template id="remove-login-dialog-template">
+ <h2
+ class="dialog-title"
+ data-l10n-id="about-logins-confirm-delete-dialog-title"
+ ></h2>
+ <div class="dialog-content" slot="dialog-content">
+ <p data-l10n-id="about-logins-confirm-delete-dialog-message"></p>
+ </div>
+ <moz-button-group>
+ <button
+ class="primary danger-button"
+ data-l10n-id="about-logins-confirm-remove-dialog-confirm-button"
+ data-command="LoginDataSource.confirmRemoveLogin"
+ ></button>
+ <button
+ close-dialog
+ data-l10n-id="confirmation-dialog-cancel-button"
+ ></button>
+ </moz-button-group>
+ </template>
+ <template id="export-logins-dialog-template">
+ <h2
+ class="dialog-title"
+ data-l10n-id="about-logins-confirm-export-dialog-title2"
+ ></h2>
+ <div class="dialog-content" slot="dialog-content">
+ <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p>
+ </div>
+ <moz-button-group>
+ <button
+ class="primary danger-button"
+ data-l10n-id="about-logins-confirm-export-dialog-confirm-button2"
+ data-command="LoginDataSource.confirmExportLogins"
+ ></button>
+ <button
+ close-dialog
+ data-l10n-id="confirmation-dialog-cancel-button"
+ ></button>
+ </moz-button-group>
+ </template>
+
+ <template id="import-logins-dialog-template">
+ <h2
+ class="dialog-title"
+ data-l10n-id="about-logins-import-dialog-title"
+ ></h2>
+ <div class="dialog-content">
+ <div data-l10n-id="about-logins-import-dialog-items-added2" localizable>
+ <span></span>
+ <span data-l10n-name="count"></span>
+ </div>
+ <div
+ data-l10n-id="about-logins-import-dialog-items-modified2"
+ localizable
+ >
+ <span></span>
+ <span data-l10n-name="count"></span>
+ </div>
+ <div
+ data-l10n-id="about-logins-import-dialog-items-no-change2"
+ data-l10n-name="no-change"
+ localizable
+ >
+ <span></span>
+ <span data-l10n-name="count"></span>
+ <span data-l10n-name="meta"></span>
+ </div>
+ <div data-l10n-id="about-logins-import-dialog-items-error" localizable>
+ <span></span>
+ <span data-l10n-name="count"></span>
+ <span data-l10n-name="meta"></span>
+ </div>
+ <a
+ class="open-detailed-report"
+ href="about:loginsimportreport"
+ target="_blank"
+ data-l10n-id="about-logins-alert-import-message"
+ ></a>
+ </div>
+ <button
+ class="primary"
+ data-l10n-id="about-logins-import-dialog-done"
+ close-dialog
+ ></button>
+ </template>
+
+ <template id="import-error-dialog-template">
+ <h2
+ class="dialog-title"
+ data-l10n-id="about-logins-import-dialog-error-title"
+ ></h2>
+ <div class="dialog-content">
+ <p
+ data-l10n-id="about-logins-import-dialog-error-file-format-title"
+ ></p>
+ <p
+ data-l10n-id="about-logins-import-dialog-error-file-format-description"
+ ></p>
+ <p
+ data-l10n-id="about-logins-import-dialog-error-no-logins-imported"
+ ></p>
+ <a
+ class="error-learn-more-link"
+ href="https://support.mozilla.org/kb/import-login-data-file"
+ data-l10n-id="about-logins-import-dialog-error-learn-more"
+ target="_blank"
+ rel="noreferrer"
+ ></a>
+ </div>
+ <moz-button-group>
+ <button
+ class="primary"
+ data-l10n-id="about-logins-import-dialog-error-try-import-again"
+ data-command="LoginDataSource.confirmRetryImport"
+ ></button>
+ <button
+ close-dialog
+ data-l10n-id="confirmation-dialog-cancel-button"
+ ></button>
+ </moz-button-group>
+ </template>
</body>
</html>
diff --git a/toolkit/components/satchel/megalist/content/search-input.mjs b/toolkit/components/satchel/megalist/content/search-input.mjs
deleted file mode 100644
index e30d13ef2a..0000000000
--- a/toolkit/components/satchel/megalist/content/search-input.mjs
+++ /dev/null
@@ -1,36 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-import { html } from "chrome://global/content/vendor/lit.all.mjs";
-import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
-
-export default class SearchInput extends MozLitElement {
- static get properties() {
- return {
- items: { type: Array },
- change: { type: Function },
- value: { type: String },
- };
- }
-
- render() {
- return html` <link
- rel="stylesheet"
- href="chrome://global/content/megalist/megalist.css"
- />
- <link
- rel="stylesheet"
- href="chrome://global/skin/in-content/common.css"
- />
- <input
- class="search"
- type="search"
- data-l10n-id="filter-placeholder"
- @input=${this.change}
- .value=${this.value}
- />`;
- }
-}
-
-customElements.define("search-input", SearchInput);