1
0
mirror of https://github.com/keeweb/keeweb.git synced 2024-06-28 07:50:55 +02:00

auto-type hint

This commit is contained in:
antelle 2021-06-14 21:08:55 +02:00
parent 763ee2e1f5
commit 518b34873b
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
7 changed files with 201 additions and 106 deletions

View File

@ -1,84 +0,0 @@
import { View } from 'framework/views/view';
import { Links } from 'const/links';
import { Timeouts } from 'const/timeouts';
import { Features } from 'util/features';
import template from 'templates/auto-type-hint.hbs';
class AutoTypeHintView extends View {
parent = 'body';
template = template;
events = {};
constructor(model) {
super(model);
this.input = model.input;
this.bodyClick = this.bodyClick.bind(this);
this.inputBlur = this.inputBlur.bind(this);
$('body').on('click', this.bodyClick);
this.input.addEventListener('blur', this.inputBlur);
this.once('remove', () => {
$('body').off('click', this.bodyClick);
this.input.removeEventListener('blur', this.inputBlur);
});
}
render() {
super.render({
cmd: Features.isMac ? 'command' : 'ctrl',
hasCtrl: Features.isMac,
link: Links.AutoType
});
const rect = this.input.getBoundingClientRect();
this.$el.appendTo(document.body).css({
left: rect.left,
top: rect.bottom + 1,
width: rect.width
});
const selfRect = this.$el[0].getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
if (selfRect.bottom > bodyRect.bottom) {
this.$el.css('height', selfRect.height + bodyRect.bottom - selfRect.bottom - 1);
}
}
bodyClick(e) {
if (this.removeTimer) {
clearTimeout(this.removeTimer);
this.removeTimer = null;
}
if (e.target === this.input) {
e.stopPropagation();
return;
}
if ($.contains(this.$el[0], e.target) || e.target === this.$el[0]) {
e.stopPropagation();
if (e.target.tagName.toLowerCase() === 'a' && !e.target.href) {
let text = $(e.target).text();
if (text[0] !== '{') {
text = text.split(' ')[0];
}
this.insertText(text);
}
this.input.focus();
} else {
this.remove();
}
}
inputBlur() {
if (!this.removeTimer) {
this.removeTimer = setTimeout(this.remove.bind(this), Timeouts.DropDownClickWait);
}
}
insertText(text) {
const pos = this.input.selectionEnd || this.input.value.length;
this.input.value = this.input.value.substr(0, pos) + text + this.input.value.substr(pos);
this.input.selectionStart = this.input.selectionEnd = pos + text.length;
this.input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
export { AutoTypeHintView };

View File

@ -0,0 +1,95 @@
import { FunctionComponent, h } from 'preact';
import { AutoTypeHintView } from 'views/auto-type/auto-type-hint-view';
import { Links } from 'const/links';
import { Features } from 'util/features';
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
import { elementBorderSize } from 'util/ui/html-elements';
export const AutoTypeHint: FunctionComponent<{ for: string }> = ({ for: targetId }) => {
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
const isMouseDownOnPopup = useRef(false);
const inputRef = useRef<HTMLInputElement>();
useLayoutEffect(() => {
const input = document.getElementById(targetId);
if (!(input instanceof HTMLInputElement)) {
return;
}
inputRef.current = input;
const inputFocus = () => {
if (!input.offsetParent) {
return;
}
const rect = input.getBoundingClientRect();
const offsetRect = input.offsetParent.getBoundingClientRect();
const borderWidth = elementBorderSize(input);
setPos({
top: rect.bottom - offsetRect.top + borderWidth,
left: rect.left - offsetRect.left,
width: rect.width
});
setVisible(true);
};
const inputBlur = () => {
if (isMouseDownOnPopup.current) {
return;
}
setVisible(false);
};
input.addEventListener('focus', inputFocus);
input.addEventListener('blur', inputBlur);
return () => {
input.removeEventListener('focus', inputFocus);
input.removeEventListener('blur', inputBlur);
};
}, []);
const itemClicked = (text: string) => {
if (text[0] !== '{') {
text = text.split(' ')[0];
}
const input = inputRef.current;
if (!input) {
return;
}
const pos = input.selectionEnd || input.value.length;
input.value = input.value.substr(0, pos) + text + input.value.substr(pos);
input.selectionStart = input.selectionEnd = pos + text.length;
input.dispatchEvent(new Event('input', { bubbles: true }));
};
const onMouseDown = () => {
isMouseDownOnPopup.current = true;
};
const onMouseUp = () => {
isMouseDownOnPopup.current = false;
inputRef.current?.focus();
};
if (!visible) {
return null;
}
return h(AutoTypeHintView, {
...pos,
link: Links.AutoType,
cmd: Features.isMac ? 'command' : 'ctrl',
hasCtrl: Features.isMac,
itemClicked,
onMouseDown,
onMouseUp
});
};

View File

@ -12,7 +12,7 @@ export const GroupPanel: FunctionComponent = () => {
useModelWatcher(group);
const [title, setTitle] = useState(group.title ?? '');
const [autoTypeSeq, setAutoTypeSeq] = useState(group.getEffectiveAutoTypeSeq());
const [autoTypeSeq, setAutoTypeSeq] = useState(group.autoTypeSeq ?? '');
const [autoTypeSeqInvalid, setAutoTypeSeqInvalid] = useState(false);
const backClicked = () => Workspace.showList();
@ -54,7 +54,7 @@ export const GroupPanel: FunctionComponent = () => {
enableSearching: group.getEffectiveEnableSearching(),
icon: group.icon ?? 'folder',
customIcon: group.customIcon,
canAutoType: !!Launcher,
canAutoType: !Launcher,
autoTypeEnabled: group.getEffectiveEnableAutoType(),
autoTypeSeq,
autoTypeSeqInvalid,

View File

@ -0,0 +1,13 @@
export function elementBorderSize(el: HTMLElement): number {
const style = window.getComputedStyle(el);
let borderSize = parseInt(style.borderWidth, 10);
if (style.boxShadow) {
const sizes = style.boxShadow
.split(' ')
.filter((item) => item.endsWith('px'))
.map((item) => parseInt(item, 10))
.filter((item) => item > 0);
borderSize += Math.max(...sizes);
}
return borderSize;
}

View File

@ -0,0 +1,87 @@
import { FunctionComponent } from 'preact';
import { Locale } from 'util/locale';
export const AutoTypeHintView: FunctionComponent<{
top: number;
left: number;
width: number;
link: string;
cmd: string;
hasCtrl: boolean;
itemClicked: (text: string) => void;
onMouseDown: () => void;
onMouseUp: () => void;
}> = ({ top, left, width, link, cmd, hasCtrl, itemClicked, onMouseDown, onMouseUp }) => {
const onClick = (e: Event) => {
if (!(e.target instanceof HTMLElement)) {
return;
}
const link = e.target.closest('a');
if (!link || link.href) {
return;
}
itemClicked(link.innerText);
};
return (
<div
class="auto-type-hint"
style={{ top, left, width }}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onClick={onClick}
>
<div class="auto-type-hint__body">
<a
href={link}
class="auto-type-hint__link-details"
target="_blank"
rel="noreferrer"
>
{Locale.autoTypeLink}
</a>
<div class="auto-type-hint__block">
<div>{Locale.autoTypeEntryFields}:</div>
<a>{'{TITLE}'}</a>
<a>{'{USERNAME}'}</a>
<a>{'{URL}'}</a>
<a>{'{PASSWORD}'}</a>
<a>{'{NOTES}'}</a>
<a>{'{GROUP}'}</a>
<a>{'{TOTP}'}</a>
<a>{'{S:Custom Field Name}'}</a>
</div>
<div class="auto-type-hint__block">
<div>{Locale.autoTypeModifiers}:</div>
<a>+ (shift)</a>
<a>% (alt)</a>
<a>^ ({cmd})</a>
{hasCtrl ? <a>^^ (ctrl)</a> : null}
</div>
<div class="auto-type-hint__block">
<div>{Locale.autoTypeKeys}:</div>
<a>{'{TAB}'}</a>
<a>{'{ENTER}'}</a>
<a>{'{SPACE}'}</a>
<a>{'{UP}'}</a>
<a>{'{DOWN}'}</a>
<a>{'{LEFT}'}</a>
<a>{'{RIGHT}'}</a>
<a>{'{HOME}'}</a>
<a>{'{END}'}</a>
<a>{'{+}'}</a>
<a>{'{%}'}</a>
<a>{'{^}'}</a>
<a>{'{~}'}</a>
<a>{'{(}'}</a>
<a>{'{)}'}</a>
<a>{'{[}'}</a>
<a>{'{]}'}</a>
<a>{'{{}'}</a>
<a>{'{}}'}</a>
</div>
</div>
</div>
);
};

View File

@ -4,7 +4,9 @@ import { Scrollable } from 'views/components/scrollable';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { StringFormat } from 'util/formatting/string-format';
import { AutoTypeHint } from 'ui/auto-type/auto-type-hint';
import { classes } from 'util/ui/classes';
import { useRef } from 'preact/hooks';
export const GroupPanelView: FunctionComponent<{
title: string;
@ -111,13 +113,14 @@ export const GroupPanelView: FunctionComponent<{
})}
id="grp__field-auto-type-seq"
value={autoTypeSeq}
onClick={(e) =>
onInput={(e) =>
autoTypeSeqChanged((e.target as HTMLInputElement).value)
}
size={50}
maxLength={1024}
placeholder={`${Locale.grpAutoTypeSeqDefault}: ${defaultAutoTypeSeq}`}
/>
<AutoTypeHint for="grp__field-auto-type-seq" />
</div>
</>
) : null}

View File

@ -1,19 +0,0 @@
<div class="auto-type-hint">
<div class="auto-type-hint__body">
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeEntryFields'}}:</div>
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeModifiers'}}:</div>
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeKeys'}}:</div>
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
</div>
</div>
</div>