2021-05-08 11:38:23 +02:00
|
|
|
import * as kdbxweb from 'kdbxweb';
|
2019-09-16 20:42:33 +02:00
|
|
|
import { View } from 'framework/views/view';
|
2019-10-26 20:50:52 +02:00
|
|
|
import { Events } from 'framework/events';
|
2019-09-15 14:16:32 +02:00
|
|
|
import { CopyPaste } from 'comp/browser/copy-paste';
|
|
|
|
import { Tip } from 'util/ui/tip';
|
2019-09-18 20:42:17 +02:00
|
|
|
import { isEqual } from 'util/fn';
|
2019-09-26 23:43:07 +02:00
|
|
|
import { Features } from 'util/features';
|
|
|
|
import { Locale } from 'util/locale';
|
|
|
|
import { AutoType } from 'auto-type';
|
2019-09-27 07:37:26 +02:00
|
|
|
import { PasswordPresenter } from 'util/formatting/password-presenter';
|
2019-09-26 23:43:07 +02:00
|
|
|
import { DropdownView } from 'views/dropdown-view';
|
2020-06-01 10:25:16 +02:00
|
|
|
import { AppSettingsModel } from 'models/app-settings-model';
|
|
|
|
import { Timeouts } from 'const/timeouts';
|
2020-05-09 17:00:39 +02:00
|
|
|
import template from 'templates/details/fields/field.hbs';
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-09-16 19:55:06 +02:00
|
|
|
class FieldView extends View {
|
|
|
|
template = template;
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-09-16 19:55:06 +02:00
|
|
|
events = {
|
2015-10-17 23:49:24 +02:00
|
|
|
'click .details__field-label': 'fieldLabelClick',
|
2020-06-01 10:25:16 +02:00
|
|
|
'dblclick .details__field-label': 'fieldLabelDblClick',
|
2018-08-22 10:04:48 +02:00
|
|
|
'click .details__field-value': 'fieldValueClick',
|
2019-09-26 23:43:07 +02:00
|
|
|
'dragstart .details__field-label': 'fieldLabelDrag',
|
|
|
|
'click .details__field-options': 'fieldOptionsClick'
|
2019-09-16 19:55:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
constructor(model, options) {
|
|
|
|
super(model, options);
|
|
|
|
this.once('remove', () => {
|
|
|
|
if (this.tip) {
|
|
|
|
Tip.hideTip(this.valueEl[0]);
|
|
|
|
}
|
|
|
|
});
|
2019-10-26 20:50:52 +02:00
|
|
|
if (Features.isMobile) {
|
|
|
|
this.listenTo(Events, 'click', this.bodyClick);
|
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
render() {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.value = typeof this.model.value === 'function' ? this.model.value() : this.model.value;
|
2019-09-28 19:30:13 +02:00
|
|
|
const renderedValue = this.renderValue(this.value);
|
2019-09-16 19:55:06 +02:00
|
|
|
super.render({
|
2019-09-08 08:25:15 +02:00
|
|
|
cls: this.cssClass,
|
2019-08-16 23:05:39 +02:00
|
|
|
editable: !this.readonly,
|
|
|
|
multiline: this.model.multiline,
|
|
|
|
title: this.model.title,
|
|
|
|
canEditTitle: this.model.newField,
|
2021-03-21 17:35:06 +01:00
|
|
|
canGen: this.model.canGen,
|
2019-09-26 23:43:07 +02:00
|
|
|
protect: this.value && this.value.isProtected,
|
2019-09-28 19:30:13 +02:00
|
|
|
hasOptions: !Features.isMobile && renderedValue && this.hasOptions
|
2019-08-16 23:05:39 +02:00
|
|
|
});
|
2015-10-17 23:49:24 +02:00
|
|
|
this.valueEl = this.$el.find('.details__field-value');
|
2019-09-28 19:30:13 +02:00
|
|
|
this.valueEl.html(renderedValue);
|
2015-10-17 23:49:24 +02:00
|
|
|
this.labelEl = this.$el.find('.details__field-label');
|
2016-06-04 14:47:56 +02:00
|
|
|
if (this.model.tip) {
|
|
|
|
this.tip = typeof this.model.tip === 'function' ? this.model.tip() : this.model.tip;
|
|
|
|
if (this.tip) {
|
|
|
|
this.valueEl.attr('title', this.tip);
|
2020-05-22 22:44:50 +02:00
|
|
|
Tip.createTip(this.valueEl[0]);
|
2016-06-04 14:47:56 +02:00
|
|
|
}
|
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2016-06-04 14:47:56 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
update() {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (typeof this.model.value === 'function') {
|
2017-01-31 07:50:28 +01:00
|
|
|
const newVal = this.model.value();
|
2019-08-16 23:05:39 +02:00
|
|
|
if (
|
2019-09-18 20:42:17 +02:00
|
|
|
!isEqual(newVal, this.value) ||
|
2019-08-16 23:05:39 +02:00
|
|
|
(this.value && newVal && this.value.toString() !== newVal.toString())
|
|
|
|
) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.render();
|
|
|
|
}
|
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
fieldLabelClick(e) {
|
2015-10-17 23:49:24 +02:00
|
|
|
e.stopImmediatePropagation();
|
2019-09-26 23:43:07 +02:00
|
|
|
this.hideOptionsDropdown();
|
2017-04-16 12:15:53 +02:00
|
|
|
if (this.preventCopy) {
|
|
|
|
return;
|
|
|
|
}
|
2020-06-01 10:25:16 +02:00
|
|
|
if (AutoType.enabled && AppSettingsModel.fieldLabelDblClickAutoType) {
|
|
|
|
if (this.fieldLabelClickTimer) {
|
|
|
|
clearTimeout(this.fieldLabelClickTimer);
|
|
|
|
this.fieldLabelClickTimer = null;
|
|
|
|
this.emit('autotype', { source: this });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.fieldLabelClickTimer = setTimeout(() => {
|
|
|
|
this.copyValue();
|
|
|
|
this.fieldLabelClickTimer = null;
|
|
|
|
}, Timeouts.FieldLabelDoubleClick);
|
|
|
|
} else {
|
|
|
|
this.copyValue();
|
|
|
|
}
|
2019-09-26 23:43:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
copyValue() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const field = this.model.name;
|
|
|
|
let copyRes;
|
2015-10-17 23:49:24 +02:00
|
|
|
if (field) {
|
2020-03-14 21:12:13 +01:00
|
|
|
const text = this.getTextValue();
|
|
|
|
if (!text) {
|
2015-10-17 23:49:24 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-03-14 21:12:13 +01:00
|
|
|
if (!CopyPaste.simpleCopy) {
|
|
|
|
CopyPaste.createHiddenInput(text);
|
|
|
|
}
|
|
|
|
copyRes = CopyPaste.copy(text);
|
|
|
|
this.emit('copy', { source: this, copyRes });
|
|
|
|
return;
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2016-02-14 13:05:31 +01:00
|
|
|
if (!this.value) {
|
|
|
|
return;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const selection = window.getSelection();
|
|
|
|
const range = document.createRange();
|
2015-10-17 23:49:24 +02:00
|
|
|
range.selectNodeContents(this.valueEl[0]);
|
|
|
|
selection.removeAllRanges();
|
|
|
|
selection.addRange(range);
|
2016-06-11 03:36:58 +02:00
|
|
|
copyRes = CopyPaste.copy(this.valueEl[0].innerText || this.valueEl.text());
|
2016-02-14 13:05:31 +01:00
|
|
|
if (copyRes) {
|
2015-10-17 23:49:24 +02:00
|
|
|
selection.removeAllRanges();
|
2019-09-16 19:55:06 +02:00
|
|
|
this.emit('copy', { source: this, copyRes });
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
fieldValueClick(e) {
|
2019-09-26 23:43:07 +02:00
|
|
|
this.hideOptionsDropdown();
|
2015-10-17 23:49:24 +02:00
|
|
|
if (['a', 'input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) >= 0) {
|
|
|
|
return;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const sel = window.getSelection().toString();
|
2015-10-17 23:49:24 +02:00
|
|
|
if (!sel) {
|
2019-10-26 20:50:52 +02:00
|
|
|
if (Features.isMobile) {
|
|
|
|
e.stopPropagation();
|
|
|
|
this.showMobileActions();
|
|
|
|
} else {
|
|
|
|
this.edit();
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
fieldLabelDrag(e) {
|
2018-08-22 10:04:48 +02:00
|
|
|
e.stopPropagation();
|
2019-09-26 23:43:07 +02:00
|
|
|
this.hideOptionsDropdown();
|
2018-08-22 10:04:48 +02:00
|
|
|
if (!this.value) {
|
|
|
|
return;
|
|
|
|
}
|
2019-09-16 21:49:21 +02:00
|
|
|
const dt = e.dataTransfer;
|
2020-03-14 21:01:41 +01:00
|
|
|
const txtval = this.getTextValue();
|
2018-08-22 10:04:48 +02:00
|
|
|
if (this.valueEl[0].tagName.toLowerCase() === 'a') {
|
|
|
|
dt.setData('text/uri-list', txtval);
|
|
|
|
}
|
|
|
|
dt.setData('text/plain', txtval);
|
|
|
|
dt.effectAllowed = 'copy';
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2018-08-22 10:04:48 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
edit() {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (this.readonly || this.editing) {
|
|
|
|
return;
|
|
|
|
}
|
2019-09-27 07:37:26 +02:00
|
|
|
this.valueEl.removeClass('details__field-value--revealed');
|
2015-10-17 23:49:24 +02:00
|
|
|
this.$el.addClass('details__field--edit');
|
|
|
|
this.startEdit();
|
|
|
|
this.editing = true;
|
2017-04-16 12:15:53 +02:00
|
|
|
this.preventCopy = true;
|
2018-08-22 10:04:48 +02:00
|
|
|
this.labelEl[0].setAttribute('draggable', 'false');
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
endEdit(newVal, extra) {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (!this.editing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.editing = false;
|
2019-08-16 23:05:39 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
this.preventCopy = false;
|
|
|
|
}, 300);
|
2017-01-31 07:50:28 +01:00
|
|
|
let textEqual;
|
2016-01-17 14:12:35 +01:00
|
|
|
if (this.value && this.value.isProtected) {
|
|
|
|
textEqual = this.value.equals(newVal);
|
|
|
|
} else if (newVal && newVal.isProtected) {
|
|
|
|
textEqual = newVal.equals(this.value);
|
2020-11-29 11:10:34 +01:00
|
|
|
} else if (newVal instanceof Date && this.value instanceof Date) {
|
|
|
|
textEqual = newVal.toDateString() === this.value.toDateString();
|
2016-01-17 14:12:35 +01:00
|
|
|
} else {
|
2019-09-18 20:42:17 +02:00
|
|
|
textEqual = isEqual(this.value, newVal);
|
2016-01-17 14:12:35 +01:00
|
|
|
}
|
2019-08-18 08:05:38 +02:00
|
|
|
const protectedEqual =
|
|
|
|
(newVal && newVal.isProtected) === (this.value && this.value.isProtected);
|
2021-04-26 13:26:27 +02:00
|
|
|
if (!extra?.newField && this.model.newField) {
|
|
|
|
extra ??= {};
|
|
|
|
extra.newField = this.model.newField;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const nameChanged = extra && extra.newField;
|
|
|
|
let arg;
|
2016-01-16 13:35:34 +01:00
|
|
|
if (newVal !== undefined && (!textEqual || !protectedEqual || nameChanged)) {
|
2015-10-17 23:49:24 +02:00
|
|
|
arg = { val: newVal, field: this.model.name };
|
|
|
|
if (extra) {
|
2019-09-17 22:17:40 +02:00
|
|
|
Object.assign(arg, extra);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
} else if (extra) {
|
|
|
|
arg = extra;
|
|
|
|
}
|
|
|
|
if (arg) {
|
2016-06-05 16:49:00 +02:00
|
|
|
this.triggerChange(arg);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2019-09-27 07:37:26 +02:00
|
|
|
this.valueEl
|
|
|
|
.removeClass('details__field-value--revealed')
|
|
|
|
.html(this.renderValue(this.value));
|
2015-10-17 23:49:24 +02:00
|
|
|
this.$el.removeClass('details__field--edit');
|
2018-08-22 10:04:48 +02:00
|
|
|
this.labelEl[0].setAttribute('draggable', 'true');
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2016-06-05 16:49:00 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
triggerChange(arg) {
|
2016-06-05 16:49:00 +02:00
|
|
|
arg.sender = this;
|
2019-09-16 19:55:06 +02:00
|
|
|
this.emit('change', arg);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2019-09-26 23:43:07 +02:00
|
|
|
|
|
|
|
fieldOptionsClick(e) {
|
|
|
|
if (this.views.optionsDropdown) {
|
|
|
|
this.hideOptionsDropdown();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
const dropdownView = new DropdownView();
|
|
|
|
|
|
|
|
this.listenTo(dropdownView, 'cancel', this.hideOptionsDropdown);
|
|
|
|
this.listenTo(dropdownView, 'select', this.optionsDropdownSelect);
|
|
|
|
|
|
|
|
const options = [];
|
|
|
|
|
|
|
|
options.push({ value: 'copy', icon: 'copy', text: Locale.alertCopy });
|
|
|
|
|
|
|
|
if (this.value instanceof kdbxweb.ProtectedValue) {
|
|
|
|
if (this.valueEl.hasClass('details__field-value--revealed')) {
|
|
|
|
options.push({ value: 'hide', icon: 'eye-slash', text: Locale.detHideField });
|
|
|
|
} else {
|
|
|
|
options.push({ value: 'reveal', icon: 'eye', text: Locale.detRevealField });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (AutoType.enabled && this.model.sequence) {
|
2020-11-25 20:10:37 +01:00
|
|
|
options.push({ value: 'autotype', icon: 'keyboard', text: Locale.detAutoTypeField });
|
2019-09-26 23:43:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const rect = this.$el[0].getBoundingClientRect();
|
|
|
|
const position = {
|
|
|
|
top: rect.bottom,
|
|
|
|
right: rect.right
|
|
|
|
};
|
|
|
|
|
|
|
|
dropdownView.render({
|
|
|
|
position,
|
|
|
|
options
|
|
|
|
});
|
|
|
|
|
|
|
|
this.views.optionsDropdown = dropdownView;
|
|
|
|
}
|
|
|
|
|
|
|
|
hideOptionsDropdown() {
|
|
|
|
if (this.views.optionsDropdown) {
|
|
|
|
this.views.optionsDropdown.remove();
|
|
|
|
delete this.views.optionsDropdown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
optionsDropdownSelect(e) {
|
|
|
|
this.hideOptionsDropdown();
|
|
|
|
switch (e.item) {
|
|
|
|
case 'copy':
|
|
|
|
this.copyValue();
|
|
|
|
break;
|
|
|
|
case 'reveal':
|
|
|
|
this.revealValue();
|
|
|
|
break;
|
|
|
|
case 'hide':
|
|
|
|
this.hideValue();
|
|
|
|
break;
|
|
|
|
case 'autotype':
|
|
|
|
this.emit('autotype', { source: this });
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
revealValue() {
|
2020-04-02 16:27:10 +02:00
|
|
|
const revealedEl = PasswordPresenter.asDOM(this.value);
|
2020-05-09 20:15:46 +02:00
|
|
|
this.valueEl.addClass('details__field-value--revealed').empty();
|
2020-04-02 16:27:10 +02:00
|
|
|
this.valueEl.append(revealedEl);
|
2019-09-26 23:43:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
hideValue() {
|
|
|
|
this.valueEl
|
|
|
|
.removeClass('details__field-value--revealed')
|
|
|
|
.html(this.renderValue(this.value));
|
|
|
|
}
|
2019-10-26 20:50:52 +02:00
|
|
|
|
|
|
|
bodyClick(e) {
|
|
|
|
if (!this.mobileActionsEl) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.valueEl[0].contains(e.target) || this.mobileActionsEl[0].contains(e.target)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.mobileActionsEl.remove();
|
|
|
|
delete this.mobileActionsEl;
|
|
|
|
}
|
|
|
|
|
|
|
|
showMobileActions() {
|
|
|
|
if (this.readonly) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.mobileActionsEl) {
|
|
|
|
this.mobileActionsEl.remove();
|
|
|
|
delete this.mobileActionsEl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const left = this.valueEl.position().left;
|
|
|
|
const width = this.$el.width() - left;
|
|
|
|
const top = this.valueEl.height();
|
|
|
|
|
|
|
|
const mobileActionsEl = $('<div></div>')
|
|
|
|
.addClass('details__field-mobile-actions')
|
|
|
|
.appendTo(this.$el)
|
|
|
|
.css({ left, top, width });
|
|
|
|
|
|
|
|
const actions = [];
|
|
|
|
if (this.value) {
|
2020-11-25 18:20:53 +01:00
|
|
|
actions.push({ name: 'copy', icon: 'copy' });
|
2019-10-26 20:50:52 +02:00
|
|
|
}
|
2020-11-25 18:20:53 +01:00
|
|
|
actions.push({ name: 'edit', icon: 'pencil-alt' });
|
2019-10-26 20:50:52 +02:00
|
|
|
if (this.value instanceof kdbxweb.ProtectedValue) {
|
|
|
|
actions.push({ name: 'reveal', icon: 'eye' });
|
|
|
|
}
|
|
|
|
if (this.model.canGen) {
|
|
|
|
actions.push({ name: 'generate', icon: 'bolt' });
|
|
|
|
}
|
|
|
|
for (const action of actions) {
|
|
|
|
$('<div></div>')
|
|
|
|
.addClass(`details__field-mobile-action fa fa-${action.icon}`)
|
|
|
|
.appendTo(mobileActionsEl)
|
|
|
|
.click(() => this.doMobileAction(action.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.mobileActionsEl = mobileActionsEl;
|
|
|
|
}
|
|
|
|
|
|
|
|
doMobileAction(name) {
|
|
|
|
this.mobileActionsEl.remove();
|
|
|
|
delete this.mobileActionsEl;
|
|
|
|
switch (name) {
|
|
|
|
case 'copy':
|
|
|
|
this.copyValue();
|
|
|
|
break;
|
|
|
|
case 'edit':
|
|
|
|
this.edit();
|
|
|
|
break;
|
|
|
|
case 'reveal':
|
|
|
|
this.revealValue();
|
|
|
|
break;
|
|
|
|
case 'generate':
|
|
|
|
this.edit();
|
|
|
|
setTimeout(() => this.showGenerator(), 0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-03-14 21:01:41 +01:00
|
|
|
|
|
|
|
getTextValue() {
|
2020-03-14 21:12:13 +01:00
|
|
|
if (!this.value) {
|
|
|
|
return '';
|
|
|
|
}
|
2020-03-14 21:01:41 +01:00
|
|
|
return this.value.isProtected ? this.value.getText() : this.value;
|
|
|
|
}
|
2019-09-16 19:55:06 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-09-15 14:16:32 +02:00
|
|
|
export { FieldView };
|