Merge pull request #468 from padloc/feature/unmask-field-on-hover
Various UX improvements for Item View
This commit is contained in:
commit
609edd22c7
|
@ -15,6 +15,7 @@ import { generatePassphrase } from "@padloc/core/src/diceware";
|
|||
import { randomString, charSets } from "@padloc/core/src/util";
|
||||
import { app } from "../globals";
|
||||
import { descriptionForAudit, iconForAudit, titleTextForAudit } from "../lib/audit";
|
||||
import "./popover";
|
||||
|
||||
@customElement("pl-field")
|
||||
export class FieldElement extends LitElement {
|
||||
|
@ -57,10 +58,19 @@ export class FieldElement extends LitElement {
|
|||
|
||||
private get _fieldActions() {
|
||||
const actions = [
|
||||
...(this._fieldDef.actions || []),
|
||||
{ icon: "copy", label: $l("Copy"), action: () => this.dispatchEvent(new CustomEvent("copy-clipboard")) },
|
||||
{
|
||||
icon: "edit",
|
||||
label: $l("Edit"),
|
||||
action: () => {
|
||||
this.dispatchEvent(new CustomEvent("edit"));
|
||||
this._drawer.collapsed = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (this._fieldDef.mask) {
|
||||
if (this._fieldDef.mask && !app.settings.unmaskFieldsOnHover) {
|
||||
actions.unshift({
|
||||
icon: this._masked ? "show" : "hide",
|
||||
label: this._masked ? "show" : "hide",
|
||||
|
@ -121,10 +131,16 @@ export class FieldElement extends LitElement {
|
|||
}
|
||||
|
||||
protected _mouseenter() {
|
||||
if (app.settings.unmaskFieldsOnHover) {
|
||||
this._masked = false;
|
||||
}
|
||||
this._drawer.collapsed = this.editing;
|
||||
}
|
||||
|
||||
protected _mouseleave() {
|
||||
if (app.settings.unmaskFieldsOnHover) {
|
||||
this._masked = true;
|
||||
}
|
||||
this._drawer.collapsed = true;
|
||||
}
|
||||
|
||||
|
@ -261,6 +277,8 @@ export class FieldElement extends LitElement {
|
|||
.placeholder=${$l("Enter Secret")}
|
||||
type="text"
|
||||
@input=${() => (this.field.value = this._valueInput.value)}
|
||||
@focus=${this._expandSuggestions}
|
||||
@blur=${this._collapseSuggestions}
|
||||
.value=${this.field.value}
|
||||
>
|
||||
<pl-button
|
||||
|
@ -270,6 +288,21 @@ export class FieldElement extends LitElement {
|
|||
>
|
||||
<pl-icon icon="qrcode"></pl-icon>
|
||||
</pl-button>
|
||||
${!this.field.value
|
||||
? html`
|
||||
<pl-drawer slot="below" collapsed>
|
||||
<div class="horizontal layout">
|
||||
<pl-button
|
||||
class="tiny skinny transparent"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent("get-totp-qr"))}
|
||||
>
|
||||
<pl-icon icon="qrcode" class="right-margined"></pl-icon>
|
||||
${$l("Scan QR Code")}
|
||||
</pl-button>
|
||||
</div>
|
||||
</pl-drawer>
|
||||
`
|
||||
: ""}
|
||||
</pl-input>
|
||||
`;
|
||||
case "password":
|
||||
|
@ -457,12 +490,18 @@ export class FieldElement extends LitElement {
|
|||
</div>
|
||||
|
||||
<pl-drawer class="drawer" collapsed>
|
||||
<div class="end-justifying spacing horizontal layout">
|
||||
<div class="end-justifying horizontal wrapping layout">
|
||||
${this._fieldActions.map(
|
||||
({ icon, action, label }) => html`
|
||||
<pl-button class="ghost small slim" @click=${action} style="min-width: 7em">
|
||||
<pl-icon icon=${icon} class="right-margined"></pl-icon>
|
||||
<div>${label}</div>
|
||||
<pl-button
|
||||
class="transparent small slim"
|
||||
@click=${() => action(this.field.value)}
|
||||
style="min-width: 7em"
|
||||
>
|
||||
<div class="half-spacing center-alinging horizontal layout">
|
||||
<pl-icon icon=${icon}></pl-icon>
|
||||
<div>${label}</div>
|
||||
</div>
|
||||
</pl-button>
|
||||
`
|
||||
)}
|
||||
|
|
|
@ -123,7 +123,11 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
return;
|
||||
}
|
||||
this._editing = true;
|
||||
setTimeout(() => this._nameInput && this._nameInput.focus(), 500);
|
||||
setTimeout(() => {
|
||||
if (!this.shadowRoot?.activeElement) {
|
||||
this._nameInput?.focus();
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
this._editing = false;
|
||||
}
|
||||
|
@ -183,6 +187,12 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
setClipboard(await field.transform(), `${item.name} / ${field.name}`);
|
||||
}
|
||||
|
||||
private async _editField(index: number) {
|
||||
this.edit();
|
||||
await this.updateComplete;
|
||||
setTimeout(() => this._fieldInputs[index]?.focus(), 100);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
shared,
|
||||
css`
|
||||
|
@ -412,6 +422,7 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@drop=${(e: DragEvent) => this._drop(e)}
|
||||
@moveup=${() => this._moveField(index, "up")}
|
||||
@movedown=${() => this._moveField(index, "down")}
|
||||
@edit=${() => this._editField(index)}
|
||||
>
|
||||
</pl-field>
|
||||
`
|
||||
|
|
|
@ -14,7 +14,7 @@ import "./rich-content";
|
|||
|
||||
@customElement("pl-report")
|
||||
export class Audit extends StateMixin(Routing(View)) {
|
||||
readonly routePattern = /^audit/;
|
||||
readonly routePattern = /^report/;
|
||||
|
||||
@state()
|
||||
private _selected: Vault | Org | null = null;
|
||||
|
@ -224,7 +224,7 @@ export class Audit extends StateMixin(Routing(View)) {
|
|||
`}
|
||||
<pl-button
|
||||
class="slim margined transparent"
|
||||
@click=${() => this.go("items", { audit: type })}
|
||||
@click=${() => this.go("items", { report: type })}
|
||||
?hidden=${listItems.length < 6}
|
||||
>
|
||||
<div>${$l("Show All")}</div>
|
||||
|
|
|
@ -22,6 +22,9 @@ export class SettingsDisplay extends StateMixin(LitElement) {
|
|||
@query("#faviconsButton")
|
||||
private _faviconsButton: ToggleButton;
|
||||
|
||||
@query("#unmaskFieldsOnHoverButton")
|
||||
private _unmaskFieldsOnHoverButton: ToggleButton;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("change", () => this._updateSettings());
|
||||
|
@ -31,6 +34,7 @@ export class SettingsDisplay extends StateMixin(LitElement) {
|
|||
this.app.setSettings({
|
||||
theme: this._themeSelect.value || undefined,
|
||||
favicons: this._faviconsButton.active,
|
||||
unmaskFieldsOnHover: this._unmaskFieldsOnHoverButton.active,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -80,6 +84,31 @@ export class SettingsDisplay extends StateMixin(LitElement) {
|
|||
</pl-toggle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="double-margined box">
|
||||
<h2 class="padded bg-dark border-bottom semibold horizontal center-aligning layout">
|
||||
<div class="uppercase stretch">${$l("Masked Fields")}</div>
|
||||
<pl-icon icon="info-round" class="subtle"></pl-icon>
|
||||
<pl-popover trigger="hover" class="small double-padded regular" style="max-width: 20em">
|
||||
${$l(
|
||||
"If this option is enabled, masked fields such as passwords or credit card " +
|
||||
"numbers will be unmasked when you move your mouse over them." +
|
||||
"Disable this option if you would rather use an explicit button."
|
||||
)}
|
||||
</pl-popover>
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<pl-toggle-button
|
||||
class="transparent"
|
||||
id="unmaskFieldsOnHoverButton"
|
||||
.active=${this.state.settings.unmaskFieldsOnHover}
|
||||
.label=${$l("Reveal On Hover")}
|
||||
reverse
|
||||
>
|
||||
</pl-toggle-button>
|
||||
</div>
|
||||
</div>
|
||||
</pl-scroller>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -167,9 +167,8 @@ export const misc = css`
|
|||
}
|
||||
|
||||
.list-item:focus:not([aria-selected="true"]) {
|
||||
background: var(--list-item-focus-background);
|
||||
color: var(--list-item-focus-color);
|
||||
/* box-shadow: inset 0.2em 0 0 0 var(--color-highlight); */
|
||||
box-shadow: inset 0.2em 0 0 0 var(--color-highlight);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
|
|
|
@ -63,6 +63,8 @@ export class Settings extends Serializable {
|
|||
securityReportReused = true;
|
||||
/** Enable checking for compromised passwords */
|
||||
securityReportCompromised = true;
|
||||
/** Unmask Fields on hover */
|
||||
unmaskFieldsOnHover = true;
|
||||
}
|
||||
|
||||
export interface HashedItem {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { totp } from "./otp";
|
|||
import { uuid } from "./util";
|
||||
import { AccountID } from "./account";
|
||||
import { AttachmentInfo } from "./attachment";
|
||||
import { openExternalUrl } from "./platform";
|
||||
|
||||
/** A tag that can be assigned to a [[VaultItem]] */
|
||||
export type Tag = string;
|
||||
|
@ -48,6 +49,7 @@ export interface FieldDef {
|
|||
format?: (value: string, masked: boolean) => string;
|
||||
/** for values that need to be prepared before being copied / filled */
|
||||
transform?: (value: string) => Promise<string>;
|
||||
actions?: { icon: string; label: string; action: (value: string) => void }[];
|
||||
}
|
||||
|
||||
/** Available field types and respective meta data (order matters for pattern matching) */
|
||||
|
@ -98,6 +100,13 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
get name() {
|
||||
return $l("URL");
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
icon: "web",
|
||||
label: $l("Open"),
|
||||
action: (value: string) => openExternalUrl(value.startsWith("http") ? value : `https://${value}`),
|
||||
},
|
||||
],
|
||||
},
|
||||
[FieldType.Date]: {
|
||||
type: FieldType.Date,
|
||||
|
|
Loading…
Reference in New Issue