auto-type hint

This commit is contained in:
antelle 2016-04-26 22:40:18 +03:00
parent 4c7072eeed
commit 46f3d76380
15 changed files with 195 additions and 14 deletions

View File

@ -151,7 +151,7 @@ AutoTypeRunner.prototype.tryParseCommand = function(op) {
op.type = 'cmd';
op.value = op.sep === '=' ? 'setDelay' : 'wait';
if (!op.arg) {
throw 'Delay requires seconds count';
throw 'Delay requires milliseconds count';
}
if (isNaN(+op.arg)) {
throw 'Bad delay: ' + op.arg;
@ -168,7 +168,7 @@ AutoTypeRunner.prototype.tryParseCommand = function(op) {
};
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
if (!field) {
if (!field || !this.entry) {
return '';
}
field = field.toLowerCase();
@ -213,7 +213,7 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
};
AutoTypeRunner.prototype.getEntryGroupName = function() {
return this.entry.group.get('title');
return this.entry && this.entry.group.get('title');
};
AutoTypeRunner.prototype.dt = function(part) {
@ -260,6 +260,9 @@ AutoTypeRunner.prototype.udt = function(part) {
};
AutoTypeRunner.prototype.getOtp = function(op) {
if (!this.entry) {
return '';
}
this.entry.initOtpGenerator();
if (!this.entry.otpGenerator) {
return '';

View File

@ -10,7 +10,8 @@ var Links = {
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting',
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache'
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache',
AutoType: 'https://github.com/antelle/keeweb/wiki/Auto-Type'
};
module.exports = Links;

View File

@ -8,7 +8,8 @@ var Timeouts = {
BeforeAutoLock: 300,
CheckWindowClosed: 300,
OtpFadeDuration: 10000,
AutoTypeAfterHide: 100
AutoTypeAfterHide: 100,
DrobDownClickWait: 500
};
module.exports = Timeouts;

View File

@ -101,6 +101,11 @@ var EntryModel = Backbone.Model.extend({
this.autoTypeEnabled = this.entry.autoType.enabled;
this.autoTypeObfuscation = this.entry.autoType.obfuscation === kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
this.autoTypeSequence = this.entry.autoType.defaultSequence;
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
},
_convertAutoTypeItem: function(item) {
return { window: item.window, sequence: item.keystrokeSequence };
},
_iconFromId: function(id) {

View File

@ -8,7 +8,7 @@ var MenuItemModel = require('./menu/menu-item-model'),
KdbxIcons = kdbxweb.Consts.Icons,
GroupCollection, EntryCollection;
var DefaultAutoTypeSequence = '{username}{tab}{password}{enter}';
var DefaultAutoTypeSequence = '{USERNAME}{TAB}{PASSWORD}{ENTER}';
var GroupModel = MenuItemModel.extend({
defaults: _.extend({}, MenuItemModel.prototype.defaults, {

View File

@ -225,6 +225,8 @@ var Locale = {
detAutoTypeShortcuts: 'Shortcuts',
detAutoTypeShortcutsDesc: '{} or {} while the app is inactive',
detAutoTypeObfuscation: 'Mix real keystrokes with random',
detAutoTypeWindow: 'Window',
detAutoTypeInputWindow: 'Window title',
detSetupOtpAlert: 'Scan the QR code',
detSetupOtpAlertBody: 'Please copy the QR code which is displayed on the authorization page.',
detSetupOtpAlertBody1: '1. go to the authorization page',
@ -243,6 +245,11 @@ var Locale = {
detOtpQrWrong: 'Wrong QR code',
detOtpQrWrongBody: 'Your QR code was successfully scanned but it doesn\'t contain one-time password data.',
autoTypeEntryFields: 'Entry fields',
autoTypeModifiers: 'Modifier keys',
autoTypeKeys: 'Keys',
autoTypeLink: 'more...',
appSecWarn: 'Not Secure!',
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
'Someone may be watching you and stealing your passwords. ' +

View File

@ -0,0 +1,82 @@
'use strict';
var Backbone = require('backbone'),
FeatureDetector = require('../util/feature-detector'),
Links = require('../const/links'),
Timeouts = require('../const/timeouts');
var AutoTypeHintView = Backbone.View.extend({
template: require('templates/auto-type-hint.hbs'),
events: {},
initialize: function(opts) {
this.input = opts.input;
this.bodyClick = this.bodyClick.bind(this);
this.inputBlur = this.inputBlur.bind(this);
$('body').on('click', this.bodyClick);
this.input.addEventListener('blur', this.inputBlur);
},
render: function () {
this.renderTemplate({
cmd: FeatureDetector.isMac() ? 'command' : 'ctrl',
hasCtrl: FeatureDetector.isMac(),
link: Links.AutoType
});
var rect = this.input.getBoundingClientRect();
this.$el.appendTo(document.body).css({
left: rect.left, top: rect.bottom + 1, width: rect.width
});
var selfRect = this.$el[0].getBoundingClientRect();
var bodyRect = document.body.getBoundingClientRect();
if (selfRect.bottom > bodyRect.bottom) {
this.$el.css('height', selfRect.height + bodyRect.bottom - selfRect.bottom - 1);
}
return this;
},
remove: function() {
$('body').off('click', this.bodyClick);
this.input.removeEventListener('blur', this.inputBlur);
Backbone.View.prototype.remove.apply(this, arguments);
},
bodyClick: function(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) {
var text = $(e.target).text();
if (text[0] !== '{') {
text = text.split(' ')[0];
}
this.insertText(text);
}
this.input.focus();
} else {
this.remove();
}
},
inputBlur: function() {
if (!this.removeTimer) {
this.removeTimer = setTimeout(this.remove.bind(this), Timeouts.DrobDownClickWait);
}
},
insertText: function(text) {
var 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;
}
});
module.exports = AutoTypeHintView;

View File

@ -2,6 +2,7 @@
'use strict';
var Backbone = require('backbone'),
AutoTypeHintView = require('../auto-type-hint-view'),
Locale = require('../../util/locale'),
FeatureDetector = require('../../util/feature-detector'),
AutoType = require('../../auto-type');
@ -10,6 +11,7 @@ var DetailsAutoTypeView = Backbone.View.extend({
template: require('templates/details/details-auto-type.hbs'),
events: {
'focus #details__auto-type-sequence': 'seqFocus',
'input #details__auto-type-sequence': 'seqInput',
'keypress #details__auto-type-sequence': 'seqKeyPress',
'keydown #details__auto-type-sequence': 'seqKeyDown',
@ -17,6 +19,10 @@ var DetailsAutoTypeView = Backbone.View.extend({
'change #details__auto-type-obfuscation': 'obfuscationChange'
},
initialize: function() {
this.views = {};
},
render: function() {
var detAutoTypeShortcutsDesc = Locale.detAutoTypeShortcutsDesc
.replace('{}', FeatureDetector.actionShortcutSymbol() + 'T')
@ -25,6 +31,7 @@ var DetailsAutoTypeView = Backbone.View.extend({
enabled: this.model.getEffectiveEnableAutoType(),
obfuscation: this.model.autoTypeObfuscation,
sequence: this.model.autoTypeSequence,
windows: this.model.autoTypeWindows,
defaultSequence: this.model.group.getEffectiveAutoTypeSeq(),
detAutoTypeShortcutsDesc: detAutoTypeShortcutsDesc
});
@ -34,7 +41,7 @@ var DetailsAutoTypeView = Backbone.View.extend({
seqInput: function(e) {
var that = this;
var el = e.target;
var seq = el.value;
var seq = $.trim(el.value);
AutoType.validate(this.model, seq, function(err) {
$(el).toggleClass('input--error', !!err);
if (!err) {
@ -51,6 +58,13 @@ var DetailsAutoTypeView = Backbone.View.extend({
e.stopPropagation();
},
seqFocus: function(e) {
if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render();
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
}
},
enabledChange: function(e) {
this.model.setEnableAutoType(e.target.checked);
},

View File

@ -3,7 +3,8 @@
var Backbone = require('backbone'),
Scrollable = require('../mixins/scrollable'),
IconSelectView = require('./icon-select-view'),
Launcher = require('../comp/launcher');
AutoTypeHintView = require('./auto-type-hint-view'),
AutoType = require('../auto-type');
var GrpView = Backbone.View.extend({
template: require('templates/grp.hbs'),
@ -13,6 +14,7 @@ var GrpView = Backbone.View.extend({
'click .grp__buttons-trash': 'moveToTrash',
'click .grp__back-button': 'returnToApp',
'input #grp__field-title': 'changeTitle',
'focus #grp__field-auto-type-seq': 'focusAutoTypeSeq',
'input #grp__field-auto-type-seq': 'changeAutoTypeSeq',
'change #grp__check-search': 'setEnableSearching',
'change #grp__check-auto-type': 'setEnableAutoType'
@ -31,7 +33,7 @@ var GrpView = Backbone.View.extend({
customIcon: this.model.get('customIcon'),
enableSearching: this.model.getEffectiveEnableSearching(),
readonly: this.model.get('top'),
canAutoType: !!Launcher,
canAutoType: AutoType.enabled,
autoTypeSeq: this.model.get('autoTypeSeq'),
autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
@ -76,8 +78,22 @@ var GrpView = Backbone.View.extend({
},
changeAutoTypeSeq: function(e) {
var seq = $.trim(e.target.value);
this.model.setAutoTypeSeq(seq);
var that = this;
var el = e.target;
var seq = $.trim(el.value);
AutoType.validate(null, seq, function(err) {
$(e.target).toggleClass('input--error', !!err);
if (!err) {
that.model.setAutoTypeSeq(seq);
}
});
},
focusAutoTypeSeq: function(e) {
if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render();
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
}
},
showIconsSelect: function() {

View File

@ -8,6 +8,7 @@ body {
overflow: auto;
-webkit-overflow-scrolling: touch;
position: fixed;
cursor: default;
}
noscript {

View File

@ -5,12 +5,10 @@ body {
color: text-color();
background-color: background-color();
}
overflow: auto;
font-family: $base-font-family;
font-feature-settings: "kern", "liga";
font-size: $base-font-size;
line-height: $base-line-height;
cursor: default;
}
@-moz-document url-prefix() {

View File

@ -7,8 +7,9 @@
@import "base/base";
@import "utils/drag";
@import "utils/common-dropdown";
@import "utils/auto-type-hint";
@import "utils/drag";
@import "utils/selection";
@import "common/dates";

View File

@ -0,0 +1,24 @@
.auto-type-hint {
@include common-dropdown;
position: absolute;
z-index: $z-index-no-modal;
border-radius: $base-border-radius;
padding: $base-padding;
box-sizing: border-box;
overflow: hidden;
&__block {
margin-bottom: $base-padding-v;
>a, >b {
font-weight: normal;
display: inline-block;
margin-right: $base-padding-h;
margin-bottom: $base-padding-v;
}
}
&__link-details {
position: absolute;
right: 0;
top: 0;
margin: $base-padding;
}
}

View File

@ -0,0 +1,17 @@
<div class="auto-type-hint">
<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>

View File

@ -24,4 +24,15 @@
<div class="details__field-label">{{res 'detAutoTypeShortcuts'}}</div>
<div class="details__field-value">{{{detAutoTypeShortcutsDesc}}}</div>
</div>
{{!--{{#each windows as |win|}}
<div class="details__field">
<div class="details__field-label">{{res 'detAutoTypeWindow'}}</div>
<div class="details__field-value details__auto-type-windows">
<input type="text" maxlength="1024" class="details__auto-type-window-title"
value="{{win.window}}" placeholder="{{res 'detAutoTypeInputWindow'}}" />
<input type="text" maxlength="1024" class="details__auto-type-window-sequence"
value="{{win.sequence}}" placeholder="{{../defaultSequence}}" />
</div>
</div>
{{/each}}--}}
</div>