mirror of
https://github.com/keeweb/keeweb.git
synced 2024-06-30 08:10:57 +02:00
auto-type hint
This commit is contained in:
parent
763ee2e1f5
commit
518b34873b
|
@ -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 };
|
|
95
app/scripts/ui/auto-type/auto-type-hint.ts
Normal file
95
app/scripts/ui/auto-type/auto-type-hint.ts
Normal 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
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,7 +12,7 @@ export const GroupPanel: FunctionComponent = () => {
|
||||||
useModelWatcher(group);
|
useModelWatcher(group);
|
||||||
|
|
||||||
const [title, setTitle] = useState(group.title ?? '');
|
const [title, setTitle] = useState(group.title ?? '');
|
||||||
const [autoTypeSeq, setAutoTypeSeq] = useState(group.getEffectiveAutoTypeSeq());
|
const [autoTypeSeq, setAutoTypeSeq] = useState(group.autoTypeSeq ?? '');
|
||||||
const [autoTypeSeqInvalid, setAutoTypeSeqInvalid] = useState(false);
|
const [autoTypeSeqInvalid, setAutoTypeSeqInvalid] = useState(false);
|
||||||
|
|
||||||
const backClicked = () => Workspace.showList();
|
const backClicked = () => Workspace.showList();
|
||||||
|
@ -54,7 +54,7 @@ export const GroupPanel: FunctionComponent = () => {
|
||||||
enableSearching: group.getEffectiveEnableSearching(),
|
enableSearching: group.getEffectiveEnableSearching(),
|
||||||
icon: group.icon ?? 'folder',
|
icon: group.icon ?? 'folder',
|
||||||
customIcon: group.customIcon,
|
customIcon: group.customIcon,
|
||||||
canAutoType: !!Launcher,
|
canAutoType: !Launcher,
|
||||||
autoTypeEnabled: group.getEffectiveEnableAutoType(),
|
autoTypeEnabled: group.getEffectiveEnableAutoType(),
|
||||||
autoTypeSeq,
|
autoTypeSeq,
|
||||||
autoTypeSeqInvalid,
|
autoTypeSeqInvalid,
|
||||||
|
|
13
app/scripts/util/ui/html-elements.ts
Normal file
13
app/scripts/util/ui/html-elements.ts
Normal 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;
|
||||||
|
}
|
87
app/scripts/views/auto-type/auto-type-hint-view.tsx
Normal file
87
app/scripts/views/auto-type/auto-type-hint-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,7 +4,9 @@ import { Scrollable } from 'views/components/scrollable';
|
||||||
import { Locale } from 'util/locale';
|
import { Locale } from 'util/locale';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { StringFormat } from 'util/formatting/string-format';
|
import { StringFormat } from 'util/formatting/string-format';
|
||||||
|
import { AutoTypeHint } from 'ui/auto-type/auto-type-hint';
|
||||||
import { classes } from 'util/ui/classes';
|
import { classes } from 'util/ui/classes';
|
||||||
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
export const GroupPanelView: FunctionComponent<{
|
export const GroupPanelView: FunctionComponent<{
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -111,13 +113,14 @@ export const GroupPanelView: FunctionComponent<{
|
||||||
})}
|
})}
|
||||||
id="grp__field-auto-type-seq"
|
id="grp__field-auto-type-seq"
|
||||||
value={autoTypeSeq}
|
value={autoTypeSeq}
|
||||||
onClick={(e) =>
|
onInput={(e) =>
|
||||||
autoTypeSeqChanged((e.target as HTMLInputElement).value)
|
autoTypeSeqChanged((e.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
size={50}
|
size={50}
|
||||||
maxLength={1024}
|
maxLength={1024}
|
||||||
placeholder={`${Locale.grpAutoTypeSeqDefault}: ${defaultAutoTypeSeq}`}
|
placeholder={`${Locale.grpAutoTypeSeqDefault}: ${defaultAutoTypeSeq}`}
|
||||||
/>
|
/>
|
||||||
|
<AutoTypeHint for="grp__field-auto-type-seq" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -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>
|
|
Loading…
Reference in New Issue
Block a user