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:
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);
|
||||
|
||||
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,
|
||||
|
|
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 { 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}
|
||||
|
|
|
@ -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