keeweb/app/scripts/views/details/details-view.js

985 lines
32 KiB
JavaScript
Raw Normal View History

2019-09-15 14:16:32 +02:00
import kdbxweb from 'kdbxweb';
2019-09-16 20:42:33 +02:00
import { View } from 'framework/views/view';
2019-09-16 22:57:56 +02:00
import { Events } from 'framework/events';
2019-09-16 19:24:15 +02:00
import { AutoType } from 'auto-type';
2019-09-15 14:16:32 +02:00
import { CopyPaste } from 'comp/browser/copy-paste';
import { KeyHandler } from 'comp/browser/key-handler';
import { OtpQrReader } from 'comp/format/otp-qr-reader';
import { Alerts } from 'comp/ui/alerts';
import { Keys } from 'const/keys';
import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model';
import { GroupModel } from 'models/group-model';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { FileSaver } from 'util/ui/file-saver';
import { Tip } from 'util/ui/tip';
2019-09-16 20:42:33 +02:00
import { Copyable } from 'framework/views/copyable';
import { Scrollable } from 'framework/views/scrollable';
2019-09-15 14:16:32 +02:00
import { DetailsAddFieldView } from 'views/details/details-add-field-view';
import { DetailsAttachmentView } from 'views/details/details-attachment-view';
import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
import { DetailsHistoryView } from 'views/details/details-history-view';
import { DropdownView } from 'views/dropdown-view';
2020-05-05 17:28:20 +02:00
import { createDetailsFields } from 'views/details/details-fields';
2019-09-15 14:16:32 +02:00
import { FieldViewCustom } from 'views/fields/field-view-custom';
import { IconSelectView } from 'views/icon-select-view';
2019-09-18 20:42:17 +02:00
import { isEqual } from 'util/fn';
2019-09-16 19:24:15 +02:00
import template from 'templates/details/details.hbs';
import emptyTemplate from 'templates/details/details-empty.hbs';
import groupTemplate from 'templates/details/details-group.hbs';
2017-01-31 07:50:28 +01:00
2019-09-16 19:24:15 +02:00
class DetailsView extends View {
parent = '.app__details';
2019-09-16 20:54:14 +02:00
fieldViews = [];
2019-09-16 19:24:15 +02:00
fieldCopyTip = null;
2015-10-17 23:49:24 +02:00
2019-09-16 19:24:15 +02:00
events = {
2015-10-17 23:49:24 +02:00
'click .details__colors-popup-item': 'selectColor',
'click .details__header-icon': 'toggleIcons',
'click .details__attachment': 'toggleAttachment',
'click .details__header-title': 'editTitle',
'click .details__history-link': 'showHistory',
'click .details__buttons-trash': 'moveToTrash',
2015-11-09 19:15:39 +01:00
'click .details__buttons-trash-del': 'deleteFromTrash',
2015-10-26 22:07:19 +01:00
'click .details__back-button': 'backClick',
'click .details__attachment-add': 'attachmentBtnClick',
'change .details__attachment-input-file': 'attachmentFileChange',
2015-10-17 23:49:24 +02:00
'dragover .details': 'dragover',
'dragleave .details': 'dragleave',
2016-07-30 11:25:22 +02:00
'drop .details': 'drop',
'contextmenu .details': 'contextMenu'
2019-09-16 19:24:15 +02:00
};
2015-10-17 23:49:24 +02:00
2019-09-16 19:24:15 +02:00
constructor(model, options) {
super(model, options);
2015-10-17 23:49:24 +02:00
this.initScroll();
2019-09-16 22:57:56 +02:00
this.listenTo(Events, 'entry-selected', this.showEntry);
this.listenTo(Events, 'copy-password', this.copyPassword);
this.listenTo(Events, 'copy-user', this.copyUserName);
this.listenTo(Events, 'copy-url', this.copyUrl);
this.listenTo(Events, 'copy-otp', this.copyOtp);
2019-09-16 22:57:56 +02:00
this.listenTo(Events, 'toggle-settings', this.settingsToggled);
this.listenTo(Events, 'context-menu-select', this.contextMenuSelect);
this.listenTo(Events, 'set-locale', this.render);
2019-09-17 19:01:12 +02:00
this.listenTo(Events, 'qr-read', this.otpCodeRead);
this.listenTo(Events, 'qr-enter-manually', this.otpEnterManually);
2019-09-16 19:24:15 +02:00
this.onKey(
2019-08-18 08:05:38 +02:00
Keys.DOM_VK_C,
this.copyPasswordFromShortcut,
KeyHandler.SHORTCUT_ACTION,
false,
true
);
2019-09-16 19:24:15 +02:00
this.onKey(Keys.DOM_VK_B, this.copyUserName, KeyHandler.SHORTCUT_ACTION);
this.onKey(Keys.DOM_VK_U, this.copyUrl, KeyHandler.SHORTCUT_ACTION);
if (AutoType.enabled) {
2020-05-09 10:37:34 +02:00
this.onKey(Keys.DOM_VK_T, () => this.autoType(), KeyHandler.SHORTCUT_ACTION);
}
2019-09-16 19:24:15 +02:00
this.onKey(
2019-08-18 08:05:38 +02:00
Keys.DOM_VK_DELETE,
this.deleteKeyPress,
KeyHandler.SHORTCUT_ACTION,
false,
true
);
2019-09-16 19:24:15 +02:00
this.onKey(
2019-08-18 08:05:38 +02:00
Keys.DOM_VK_BACK_SPACE,
this.deleteKeyPress,
KeyHandler.SHORTCUT_ACTION,
false,
true
);
2019-09-16 19:24:15 +02:00
this.once('remove', () => {
this.removeFieldViews();
});
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
removeFieldViews() {
2016-07-17 13:30:38 +02:00
this.fieldViews.forEach(fieldView => fieldView.remove());
2015-10-17 23:49:24 +02:00
this.fieldViews = [];
2016-07-16 15:30:17 +02:00
this.hideFieldCopyTip();
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
render() {
2019-09-15 20:09:28 +02:00
Tip.destroyTips(this.$el);
this.removeScroll();
2015-10-17 23:49:24 +02:00
this.removeFieldViews();
2016-04-23 16:50:40 +02:00
this.removeInnerViews();
2015-10-17 23:49:24 +02:00
if (!this.model) {
2019-09-16 19:24:15 +02:00
this.template = emptyTemplate;
super.render();
2015-10-17 23:49:24 +02:00
return;
}
2015-11-08 22:25:00 +01:00
if (this.model instanceof GroupModel) {
2019-09-16 19:24:15 +02:00
this.template = groupTemplate;
super.render();
2015-11-08 22:25:00 +01:00
return;
}
2019-09-17 23:44:17 +02:00
const model = { deleted: this.appModel.filter.trash, ...this.model };
2019-09-16 19:24:15 +02:00
this.template = template;
super.render(model);
2015-10-17 23:49:24 +02:00
this.setSelectedColor(this.model.color);
this.addFieldViews();
2016-01-17 21:19:42 +01:00
this.createScroll({
2015-10-17 23:49:24 +02:00
root: this.$el.find('.details__body')[0],
scroller: this.$el.find('.scroller')[0],
2016-01-17 21:19:42 +01:00
bar: this.$el.find('.scroller__bar')[0]
2015-10-17 23:49:24 +02:00
});
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
this.pageResized();
2016-01-13 18:46:43 +01:00
this.showCopyTip();
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2020-05-05 17:28:20 +02:00
getFieldView(name) {
return this.fieldViews.find(fv => fv.model.name === name);
}
2019-08-18 10:17:09 +02:00
addFieldViews() {
2020-05-05 17:28:20 +02:00
const { fieldViews, fieldViewsAside } = createDetailsFields(this);
2016-03-05 09:35:22 +01:00
2019-09-17 19:50:42 +02:00
const hideEmptyFields = AppSettingsModel.hideEmptyFields;
2016-03-05 09:35:22 +01:00
2017-01-31 07:50:28 +01:00
const fieldsMainEl = this.$el.find('.details__body-fields');
const fieldsAsideEl = this.$el.find('.details__body-aside');
2020-04-15 16:50:01 +02:00
for (const views of [fieldViews, fieldViewsAside]) {
for (const fieldView of views) {
fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0];
fieldView.render();
fieldView.on('change', this.fieldChanged.bind(this));
fieldView.on('copy', this.fieldCopied.bind(this));
2020-05-09 10:37:34 +02:00
fieldView.on('autotype', e => this.autoType(e.source.model.sequence));
2020-04-15 16:50:01 +02:00
if (hideEmptyFields) {
const value = fieldView.model.value();
if (!value || value.length === 0 || value.byteLength === 0) {
if (
this.model.isJustCreated &&
['$UserName', '$Password'].indexOf(fieldView.model.name) >= 0
) {
return; // don't hide user for new records
}
fieldView.hide();
2016-03-05 10:00:43 +01:00
}
}
2016-03-05 09:35:22 +01:00
}
2020-04-15 16:50:01 +02:00
}
2020-05-05 17:28:20 +02:00
2020-04-15 16:50:01 +02:00
this.fieldViews = fieldViews.concat(fieldViewsAside);
2016-03-05 09:35:22 +01:00
2020-04-15 16:50:01 +02:00
if (!this.model.external) {
this.moreView = new DetailsAddFieldView();
this.moreView.render();
this.moreView.on('add-field', this.addNewField.bind(this));
this.moreView.on('more-click', this.toggleMoreOptions.bind(this));
}
2019-09-16 19:24:15 +02:00
}
2016-03-05 09:35:22 +01:00
2019-08-18 10:17:09 +02:00
addNewField() {
2016-03-05 09:35:22 +01:00
this.moreView.remove();
this.moreView = null;
2017-01-31 07:50:28 +01:00
let newFieldTitle = Locale.detNetField;
2016-03-05 09:35:22 +01:00
if (this.model.fields[newFieldTitle]) {
2017-01-31 07:50:28 +01:00
for (let i = 1; ; i++) {
const newFieldTitleVariant = newFieldTitle + i;
2016-03-05 09:35:22 +01:00
if (!this.model.fields[newFieldTitleVariant]) {
2015-10-17 23:49:24 +02:00
newFieldTitle = newFieldTitleVariant;
break;
}
}
}
2019-09-16 19:55:06 +02:00
const fieldView = new FieldViewCustom(
{
2019-08-16 23:05:39 +02:00
name: '$' + newFieldTitle,
title: newFieldTitle,
newField: newFieldTitle,
multiline: true,
2019-08-18 10:17:09 +02:00
value() {
2019-08-16 23:05:39 +02:00
return '';
}
2019-09-16 19:55:06 +02:00
},
{
parent: this.$el.find('.details__body-fields')[0]
2019-08-16 23:05:39 +02:00
}
2019-09-16 19:55:06 +02:00
);
2016-03-05 09:35:22 +01:00
fieldView.on('change', this.fieldChanged.bind(this));
2019-09-16 19:55:06 +02:00
fieldView.render();
2016-03-05 09:35:22 +01:00
fieldView.edit();
this.fieldViews.push(fieldView);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
toggleMoreOptions() {
2016-03-05 09:35:22 +01:00
if (this.views.dropdownView) {
this.views.dropdownView.remove();
this.views.dropdownView = null;
} else {
2019-09-15 12:12:00 +02:00
setTimeout(() => {
2017-01-31 07:50:28 +01:00
const dropdownView = new DropdownView();
2016-03-05 09:35:22 +01:00
this.listenTo(dropdownView, 'cancel', this.toggleMoreOptions);
this.listenTo(dropdownView, 'select', this.moreOptionsSelect);
2019-09-17 19:50:42 +02:00
const hideEmptyFields = AppSettingsModel.hideEmptyFields;
2017-01-31 07:50:28 +01:00
const moreOptions = [];
2016-03-05 09:35:22 +01:00
if (hideEmptyFields) {
2016-07-17 13:30:38 +02:00
this.fieldViews.forEach(fieldView => {
2016-03-05 09:40:07 +01:00
if (fieldView.isHidden()) {
2019-08-16 23:05:39 +02:00
moreOptions.push({
value: 'add:' + fieldView.model.name,
icon: 'pencil',
text: Locale.detMenuAddField.replace('{}', fieldView.model.title)
});
2016-03-05 09:35:22 +01:00
}
}, this);
2019-08-18 08:05:38 +02:00
moreOptions.push({
value: 'add-new',
icon: 'plus',
text: Locale.detMenuAddNewField
});
moreOptions.push({
value: 'toggle-empty',
icon: 'eye',
text: Locale.detMenuShowEmpty
});
2016-03-05 09:35:22 +01:00
} else {
2019-08-18 08:05:38 +02:00
moreOptions.push({
value: 'add-new',
icon: 'plus',
text: Locale.detMenuAddNewField
});
moreOptions.push({
value: 'toggle-empty',
icon: 'eye-slash',
text: Locale.detMenuHideEmpty
});
2016-03-05 09:35:22 +01:00
}
2019-08-16 23:05:39 +02:00
moreOptions.push({ value: 'otp', icon: 'clock-o', text: Locale.detSetupOtp });
2016-04-23 16:50:40 +02:00
if (AutoType.enabled) {
2019-08-18 08:05:38 +02:00
moreOptions.push({
value: 'auto-type',
icon: 'keyboard-o',
text: Locale.detAutoTypeSettings
});
2016-04-23 16:50:40 +02:00
}
2019-08-16 23:05:39 +02:00
moreOptions.push({ value: 'clone', icon: 'clone', text: Locale.detClone });
moreOptions.push({
value: 'copy-to-clipboard',
icon: 'copy',
text: Locale.detCopyEntryToClipboard
});
2017-01-31 07:50:28 +01:00
const rect = this.moreView.labelEl[0].getBoundingClientRect();
2016-03-05 09:35:22 +01:00
dropdownView.render({
2019-08-16 23:05:39 +02:00
position: { top: rect.bottom, left: rect.left },
2016-03-05 09:35:22 +01:00
options: moreOptions
});
this.views.dropdownView = dropdownView;
});
}
2019-09-16 19:24:15 +02:00
}
2016-03-05 09:35:22 +01:00
2019-08-18 10:17:09 +02:00
moreOptionsSelect(e) {
2016-03-05 09:35:22 +01:00
this.views.dropdownView.remove();
this.views.dropdownView = null;
switch (e.item) {
case 'add-new':
this.addNewField();
break;
2019-08-18 10:17:09 +02:00
case 'toggle-empty': {
2019-09-17 19:50:42 +02:00
const hideEmptyFields = AppSettingsModel.hideEmptyFields;
AppSettingsModel.hideEmptyFields = !hideEmptyFields;
2016-03-05 09:35:22 +01:00
this.render();
break;
2019-08-18 10:17:09 +02:00
}
2016-03-31 22:52:04 +02:00
case 'otp':
this.setupOtp();
break;
2016-04-23 16:50:40 +02:00
case 'auto-type':
this.toggleAutoType();
break;
2016-08-21 19:39:02 +02:00
case 'clone':
this.clone();
break;
case 'copy-to-clipboard':
this.copyToClipboard();
break;
2016-03-05 09:35:22 +01:00
default:
if (e.item.lastIndexOf('add:', 0) === 0) {
2017-01-31 07:50:28 +01:00
const fieldName = e.item.substr(4);
2019-09-17 23:44:17 +02:00
const fieldView = this.fieldViews.find(f => f.model.name === fieldName);
2016-03-05 09:35:22 +01:00
fieldView.show();
fieldView.edit();
}
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
getUserNameCompletions(part) {
2016-02-28 12:16:05 +01:00
return this.appModel.completeUserNames(part);
2019-09-16 19:24:15 +02:00
}
2016-02-28 12:16:05 +01:00
2019-08-18 10:17:09 +02:00
setSelectedColor(color) {
2019-08-16 23:05:39 +02:00
this.$el
.find('.details__colors-popup > .details__colors-popup-item')
.removeClass('details__colors-popup-item--active');
2017-01-31 07:50:28 +01:00
const colorEl = this.$el.find('.details__header-color')[0];
2019-09-17 22:17:40 +02:00
for (const cls of colorEl.classList) {
2015-10-17 23:49:24 +02:00
if (cls.indexOf('color') > 0 && cls.lastIndexOf('details', 0) !== 0) {
colorEl.classList.remove(cls);
}
2019-09-17 22:17:40 +02:00
}
2015-10-17 23:49:24 +02:00
if (color) {
2019-08-16 23:05:39 +02:00
this.$el
.find('.details__colors-popup > .' + color + '-color')
.addClass('details__colors-popup-item--active');
2015-10-17 23:49:24 +02:00
colorEl.classList.add(color + '-color');
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
selectColor(e) {
2019-08-16 23:05:39 +02:00
let color = $(e.target)
.closest('.details__colors-popup-item')
.data('color');
2015-10-17 23:49:24 +02:00
if (!color) {
return;
}
if (color === this.model.color) {
color = null;
}
this.model.setColor(color);
this.entryUpdated();
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
toggleIcons() {
2020-04-15 16:50:01 +02:00
if (this.model.external) {
return;
}
2015-10-31 20:09:32 +01:00
if (this.views.sub && this.views.sub instanceof IconSelectView) {
2015-10-17 23:49:24 +02:00
this.render();
return;
}
this.removeSubView();
2019-09-15 20:09:28 +02:00
const subView = new IconSelectView(
{
2015-11-21 23:15:51 +01:00
iconId: this.model.customIconId || this.model.iconId,
2019-08-16 23:05:39 +02:00
url: this.model.url,
file: this.model.file
2019-09-15 20:09:28 +02:00
},
{
parent: this.scroller[0],
replace: true
2015-11-21 23:15:51 +01:00
}
2019-09-15 20:09:28 +02:00
);
2015-10-17 23:49:24 +02:00
this.listenTo(subView, 'select', this.iconSelected);
subView.render();
this.pageResized();
this.views.sub = subView;
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
toggleAttachment(e) {
2017-01-31 07:50:28 +01:00
const attBtn = $(e.target).closest('.details__attachment');
const id = attBtn.data('id');
const attachment = this.model.attachments[id];
2015-10-17 23:49:24 +02:00
if (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey) {
this.downloadAttachment(attachment);
return;
}
if (this.views.sub && this.views.sub.attId === id) {
this.render();
return;
}
this.removeSubView();
2019-09-16 18:41:06 +02:00
const subView = new DetailsAttachmentView(attachment, {
parent: this.scroller[0],
replace: true
});
2015-10-17 23:49:24 +02:00
subView.attId = id;
subView.render(this.pageResized.bind(this));
subView.on('download', () => this.downloadAttachment(attachment));
this.listenTo(subView, 'close', this.render.bind(this));
2015-10-17 23:49:24 +02:00
this.views.sub = subView;
attBtn.addClass('details__attachment--active');
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
removeSubView() {
2015-10-17 23:49:24 +02:00
this.$el.find('.details__attachment').removeClass('details__attachment--active');
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
downloadAttachment(attachment) {
2017-01-31 07:50:28 +01:00
const data = attachment.getBinary();
2015-10-17 23:49:24 +02:00
if (!data) {
return;
}
2017-01-31 07:50:28 +01:00
const mimeType = attachment.mimeType || 'application/octet-stream';
2019-08-16 23:05:39 +02:00
const blob = new Blob([data], { type: mimeType });
2015-10-17 23:49:24 +02:00
FileSaver.saveAs(blob, attachment.title);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
iconSelected(sel) {
2015-11-21 23:15:51 +01:00
if (sel.custom) {
if (sel.id !== this.model.customIconId) {
this.model.setCustomIcon(sel.id);
this.entryUpdated();
} else {
this.render();
}
} else if (sel.id !== this.model.iconId) {
2015-11-22 11:59:13 +01:00
this.model.setIcon(+sel.id);
2015-10-17 23:49:24 +02:00
this.entryUpdated();
} else {
this.render();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
showEntry(entry) {
2015-10-17 23:49:24 +02:00
this.model = entry;
this.initOtp();
2015-10-17 23:49:24 +02:00
this.render();
2015-10-27 22:07:48 +01:00
if (entry && !entry.title && entry.isJustCreated) {
2015-10-18 14:18:53 +02:00
this.editTitle();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
initOtp() {
this.matchingOtpEntry = null;
if (!this.model || this.model.external) {
return;
}
this.matchingOtpEntry = this.appModel.getMatchingOtpEntry(this.model);
this.model.initOtpGenerator();
this.matchingOtpEntry?.initOtpGenerator();
}
2019-08-18 10:17:09 +02:00
copyKeyPress(editView) {
2020-05-05 17:28:20 +02:00
if (!editView || this.isHidden()) {
return false;
2019-08-16 23:05:39 +02:00
}
2015-10-17 23:49:24 +02:00
if (!window.getSelection().toString()) {
2020-03-14 21:12:13 +01:00
const fieldText = editView.getTextValue();
if (!fieldText) {
2016-02-22 08:09:23 +01:00
return;
}
2016-01-22 18:51:36 +01:00
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(fieldText);
2016-01-22 18:51:36 +01:00
}
2017-01-31 07:50:28 +01:00
const copyRes = CopyPaste.copy(fieldText);
2019-08-18 10:17:09 +02:00
this.fieldCopied({ source: editView, copyRes });
return true;
}
return false;
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
copyPasswordFromShortcut(e) {
2020-05-08 23:16:13 +02:00
if (this.model.external) {
this.copyOtp();
e.preventDefault();
}
2020-05-05 17:28:20 +02:00
const copied = this.copyKeyPress(this.getFieldView('$Password'));
if (copied) {
e.preventDefault();
2015-10-17 23:49:24 +02:00
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
copyPassword() {
2020-05-05 17:28:20 +02:00
this.copyKeyPress(this.getFieldView('$Password'));
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
copyUserName() {
2020-05-05 17:28:20 +02:00
this.copyKeyPress(this.getFieldView('$UserName'));
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
copyUrl() {
2020-05-05 17:28:20 +02:00
this.copyKeyPress(this.getFieldView('$URL'));
2019-09-16 19:24:15 +02:00
}
2016-02-22 08:09:23 +01:00
copyOtp() {
2020-05-09 10:37:34 +02:00
const otpField = this.getFieldView('$otp');
2020-05-08 23:33:57 +02:00
if (this.model.external) {
2020-05-09 10:37:34 +02:00
if (!otpField) {
2020-05-08 23:33:57 +02:00
return false;
}
2020-05-09 10:37:34 +02:00
otpField.copyValue();
2020-05-08 23:33:57 +02:00
return true;
}
2020-05-09 10:37:34 +02:00
this.copyKeyPress(otpField);
}
2019-08-18 10:17:09 +02:00
showCopyTip() {
2016-01-13 18:46:43 +01:00
if (this.helpTipCopyShown) {
return;
}
2019-09-17 19:50:42 +02:00
this.helpTipCopyShown = AppSettingsModel.helpTipCopyShown;
2016-01-13 18:46:43 +01:00
if (this.helpTipCopyShown) {
return;
}
2019-09-17 19:50:42 +02:00
AppSettingsModel.helpTipCopyShown = true;
2016-01-13 18:46:43 +01:00
this.helpTipCopyShown = true;
2017-01-31 07:50:28 +01:00
const label = this.moreView.labelEl;
const tip = new Tip(label, { title: Locale.detCopyHint, placement: 'right' });
2016-01-13 18:46:43 +01:00
tip.show();
2016-03-27 15:41:13 +02:00
this.fieldCopyTip = tip;
2019-08-16 23:05:39 +02:00
setTimeout(() => {
tip.hide();
}, Timeouts.AutoHideHint);
2019-09-16 19:24:15 +02:00
}
2016-01-13 18:46:43 +01:00
2019-08-18 10:17:09 +02:00
settingsToggled() {
2016-07-16 15:30:17 +02:00
this.hideFieldCopyTip();
2019-09-16 19:24:15 +02:00
}
2016-07-16 15:30:17 +02:00
2019-08-18 10:17:09 +02:00
fieldChanged(e) {
2015-10-17 23:49:24 +02:00
if (e.field) {
if (e.field[0] === '$') {
2017-01-31 07:50:28 +01:00
let fieldName = e.field.substr(1);
2016-04-02 15:20:48 +02:00
if (fieldName === 'otp') {
if (this.otpFieldChanged(e.val)) {
this.entryUpdated();
return;
}
} else if (e.newField) {
2016-01-16 13:35:34 +01:00
if (fieldName) {
this.model.setField(fieldName, undefined);
}
fieldName = e.newField;
2017-01-31 07:50:28 +01:00
let i = 0;
2016-01-16 13:35:34 +01:00
while (this.model.hasField(fieldName)) {
i++;
fieldName = e.newField + i;
}
2017-05-03 21:21:29 +02:00
const allowEmpty = this.model.group.isEntryTemplatesGroup();
this.model.setField(fieldName, e.val, allowEmpty);
2015-10-17 23:49:24 +02:00
this.entryUpdated();
return;
2016-06-05 16:49:00 +02:00
} else if (fieldName === 'File') {
2017-01-31 07:50:28 +01:00
const newFile = this.appModel.files.get(e.val);
2016-06-05 16:49:00 +02:00
this.model.moveToFile(newFile);
this.appModel.activeEntryId = this.model.id;
this.entryUpdated();
2019-09-16 22:57:56 +02:00
Events.emit('entry-selected', this.model);
2016-06-05 16:49:00 +02:00
return;
2016-01-16 13:35:34 +01:00
} else if (fieldName) {
2015-10-17 23:49:24 +02:00
this.model.setField(fieldName, e.val);
}
} else if (e.field === 'Tags') {
this.model.setTags(e.val);
this.appModel.updateTags();
} else if (e.field === 'Expires') {
2017-01-31 07:50:28 +01:00
const dt = e.val || undefined;
2019-09-18 20:42:17 +02:00
if (!isEqual(dt, this.model.expires)) {
2015-10-17 23:49:24 +02:00
this.model.setExpires(dt);
}
}
this.entryUpdated(true);
this.fieldViews.forEach(function(fieldView, ix) {
2019-08-16 23:05:39 +02:00
if (
fieldView instanceof FieldViewCustom &&
!fieldView.model.newField &&
!this.model.hasField(fieldView.model.title)
) {
2015-10-17 23:49:24 +02:00
fieldView.remove();
this.fieldViews.splice(ix, 1);
} else {
fieldView.update();
}
}, this);
2016-03-05 09:35:22 +01:00
} else if (e.newField) {
this.render();
return;
2015-10-17 23:49:24 +02:00
}
if (e.tab) {
this.focusNextField(e.tab);
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
otpFieldChanged(value) {
2017-01-31 07:50:28 +01:00
let oldValue = this.model.fields.otp;
2016-04-02 15:20:48 +02:00
if (oldValue && oldValue.isProtected) {
oldValue = oldValue.getText();
}
2016-04-04 20:45:17 +02:00
if (value && value.isProtected) {
value = value.getText();
}
2016-04-02 15:20:48 +02:00
if (oldValue === value) {
2016-04-04 20:45:17 +02:00
this.render();
2016-04-02 15:20:48 +02:00
return false;
}
this.model.setOtpUrl(value);
return true;
2019-09-16 19:24:15 +02:00
}
2016-04-02 15:20:48 +02:00
2019-08-18 10:17:09 +02:00
dragover(e) {
2015-10-17 23:49:24 +02:00
e.preventDefault();
e.stopPropagation();
2019-09-16 21:49:21 +02:00
const dt = e.dataTransfer;
2019-08-18 08:05:38 +02:00
if (
!dt.types ||
(dt.types.indexOf ? dt.types.indexOf('Files') === -1 : !dt.types.contains('Files'))
) {
dt.dropEffect = 'none';
return;
}
dt.dropEffect = 'copy';
2015-10-17 23:49:24 +02:00
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
if (this.model && !this.dragging) {
this.dragging = true;
this.$el.find('.details').addClass('details--drag');
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
dragleave() {
2015-10-17 23:49:24 +02:00
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
2016-07-17 13:30:38 +02:00
this.dragTimeout = setTimeout(() => {
2015-10-17 23:49:24 +02:00
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
2016-07-17 13:30:38 +02:00
}, 100);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
drop(e) {
2015-10-17 23:49:24 +02:00
e.preventDefault();
if (!this.model) {
return;
}
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
2019-09-16 21:49:21 +02:00
const files = e.target.files || e.dataTransfer.files;
this.addAttachedFiles(files);
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
attachmentBtnClick() {
this.$el.find('.details__attachment-input-file')[0].click();
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
attachmentFileChange(e) {
this.addAttachedFiles(e.target.files);
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
addAttachedFiles(files) {
2019-09-17 22:17:40 +02:00
for (const file of files) {
const reader = new FileReader();
reader.onload = () => {
this.addAttachment(file.name, reader.result);
};
reader.readAsArrayBuffer(file);
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
addAttachment(name, data) {
2017-01-29 23:28:04 +01:00
this.model.addAttachment(name, data).then(() => {
this.entryUpdated();
});
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
deleteKeyPress(e) {
2015-10-17 23:49:24 +02:00
if (this.views.sub && this.views.sub.attId !== undefined) {
2016-09-04 09:26:54 +02:00
e.preventDefault();
2017-01-31 07:50:28 +01:00
const attachment = this.model.attachments[this.views.sub.attId];
2015-10-17 23:49:24 +02:00
this.model.removeAttachment(attachment.title);
this.render();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
editTitle() {
2020-04-15 16:50:01 +02:00
if (this.model.external) {
return;
}
2017-01-31 07:50:28 +01:00
const input = $('<input/>')
2015-10-17 23:49:24 +02:00
.addClass('details__header-title-input')
.attr({ autocomplete: 'off', spellcheck: 'false', placeholder: 'Title' })
.val(this.model.title);
input.bind({
blur: this.titleInputBlur.bind(this),
input: this.titleInputInput.bind(this),
keydown: this.titleInputKeydown.bind(this),
keypress: this.titleInputInput.bind(this)
});
$('.details__header-title').replaceWith(input);
input.focus()[0].setSelectionRange(this.model.title.length, this.model.title.length);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
titleInputBlur(e) {
2015-10-17 23:49:24 +02:00
this.setTitle(e.target.value);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
titleInputInput(e) {
2015-10-17 23:49:24 +02:00
e.stopPropagation();
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
titleInputKeydown(e) {
2015-11-17 22:49:12 +01:00
KeyHandler.reg();
2015-10-17 23:49:24 +02:00
e.stopPropagation();
2017-01-31 07:50:28 +01:00
const code = e.keyCode || e.which;
2015-10-17 23:49:24 +02:00
if (code === Keys.DOM_VK_RETURN) {
$(e.target).unbind('blur');
this.setTitle(e.target.value);
} else if (code === Keys.DOM_VK_ESCAPE) {
$(e.target).unbind('blur');
2015-10-27 22:07:48 +01:00
if (this.model.isJustCreated) {
2015-10-27 20:40:34 +01:00
this.model.removeWithoutHistory();
2019-09-16 22:57:56 +02:00
Events.emit('refresh');
2015-10-27 20:40:34 +01:00
return;
}
2015-10-17 23:49:24 +02:00
this.render();
} else if (code === Keys.DOM_VK_TAB) {
e.preventDefault();
$(e.target).unbind('blur');
this.setTitle(e.target.value);
if (!e.shiftKey) {
this.focusNextField({ field: '$Title' });
}
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
setTitle(title) {
2015-10-17 23:49:24 +02:00
if (this.model.title instanceof kdbxweb.ProtectedValue) {
title = kdbxweb.ProtectedValue.fromString(title);
}
if (title !== this.model.title) {
this.model.setField('Title', title);
this.entryUpdated(true);
}
2017-01-31 07:50:28 +01:00
const newTitle = $('<h1 class="details__header-title"></h1>').text(title || '(no title)');
2015-10-17 23:49:24 +02:00
this.$el.find('.details__header-title-input').replaceWith(newTitle);
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
entryUpdated(skipRender) {
2019-09-16 22:57:56 +02:00
Events.emit('entry-updated', { entry: this.model });
2015-10-17 23:49:24 +02:00
if (!skipRender) {
this.render();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
focusNextField(config) {
2019-08-16 23:05:39 +02:00
let found = false,
nextFieldView;
2015-10-17 23:49:24 +02:00
if (config.field === '$Title' && !config.prev) {
found = true;
}
2017-01-31 07:50:28 +01:00
const start = config.prev ? this.fieldViews.length - 1 : 0;
const end = config.prev ? -1 : this.fieldViews.length;
const inc = config.prev ? -1 : 1;
for (let i = start; i !== end; i += inc) {
const fieldView = this.fieldViews[i];
2015-10-17 23:49:24 +02:00
if (fieldView.model.name === config.field) {
found = true;
2016-03-05 09:35:22 +01:00
} else if (found && !fieldView.readonly && !fieldView.isHidden()) {
2015-10-17 23:49:24 +02:00
nextFieldView = fieldView;
break;
}
}
if (nextFieldView) {
nextFieldView.edit();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
showHistory() {
2015-10-17 23:49:24 +02:00
this.removeSubView();
2019-09-16 19:09:57 +02:00
const subView = new DetailsHistoryView(this.model, {
parent: this.scroller[0],
replace: true
});
2015-10-17 23:49:24 +02:00
this.listenTo(subView, 'close', this.historyClosed.bind(this));
subView.render();
this.pageResized();
this.views.sub = subView;
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
historyClosed(e) {
2015-10-17 23:49:24 +02:00
if (e.updated) {
this.entryUpdated();
} else {
this.render();
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
moveToTrash() {
const doMove = () => {
this.model.moveToTrash();
2019-09-16 22:57:56 +02:00
Events.emit('refresh');
};
2019-09-15 08:11:11 +02:00
if (Features.isMobile) {
Alerts.yesno({
header: Locale.detDelToTrash,
body: Locale.detDelToTrashBody,
icon: 'trash',
success: doMove
});
} else {
doMove();
}
2019-09-16 19:24:15 +02:00
}
2015-10-26 22:07:19 +01:00
2019-08-18 10:17:09 +02:00
clone() {
2017-01-31 07:50:28 +01:00
const newEntry = this.model.cloneEntry(' ' + Locale.detClonedName);
2019-09-16 22:57:56 +02:00
Events.emit('select-entry', newEntry);
2019-09-16 19:24:15 +02:00
}
2016-08-21 19:39:02 +02:00
copyToClipboard() {
CopyPaste.copyHtml(this.model.getHtml());
2019-09-16 19:24:15 +02:00
}
2019-08-18 10:17:09 +02:00
deleteFromTrash() {
2015-11-09 19:15:39 +01:00
Alerts.yesno({
2015-12-17 19:25:25 +01:00
header: Locale.detDelFromTrash,
2020-04-23 19:55:52 +02:00
body: Locale.detDelFromTrashBody,
hint: Locale.detDelFromTrashBodyHint,
2015-11-09 19:15:39 +01:00
icon: 'minus-circle',
2016-07-17 13:30:38 +02:00
success: () => {
2015-11-09 19:15:39 +01:00
this.model.deleteFromTrash();
2019-09-16 22:57:56 +02:00
Events.emit('refresh');
2016-07-17 13:30:38 +02:00
}
2015-11-09 19:15:39 +01:00
});
2019-09-16 19:24:15 +02:00
}
2015-11-09 19:15:39 +01:00
2019-08-18 10:17:09 +02:00
backClick() {
2019-09-16 22:57:56 +02:00
Events.emit('toggle-details', false);
2019-09-16 19:24:15 +02:00
}
2016-03-31 22:52:04 +02:00
2016-07-30 11:25:22 +02:00
contextMenu(e) {
2017-01-31 07:50:28 +01:00
const canCopy = document.queryCommandSupported('copy');
const options = [];
2016-07-30 11:25:22 +02:00
if (canCopy) {
2020-05-08 23:16:13 +02:00
if (this.model.external) {
options.push({
value: 'det-copy-otp',
icon: 'clipboard',
text: Locale.detMenuCopyOtp
});
} else {
options.push({
value: 'det-copy-password',
icon: 'clipboard',
text: Locale.detMenuCopyPassword
});
}
2019-08-18 08:05:38 +02:00
options.push({
value: 'det-copy-user',
icon: 'clipboard',
text: Locale.detMenuCopyUser
});
2016-07-30 11:25:22 +02:00
}
2020-05-08 23:16:13 +02:00
if (!this.model.external) {
options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField });
options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone });
if (canCopy) {
options.push({
value: 'copy-to-clipboard',
icon: 'copy',
text: Locale.detCopyEntryToClipboard
});
}
2019-09-14 22:12:02 +02:00
}
2017-02-05 15:16:40 +01:00
if (AutoType.enabled) {
options.push({ value: 'det-auto-type', icon: 'keyboard-o', text: Locale.detAutoType });
}
2019-09-17 22:17:40 +02:00
Events.emit('show-context-menu', Object.assign(e, { options }));
2019-09-16 19:24:15 +02:00
}
2016-07-30 11:25:22 +02:00
contextMenuSelect(e) {
switch (e.item) {
case 'det-copy-password':
this.copyPassword();
break;
case 'det-copy-user':
this.copyUserName();
break;
2020-05-08 23:16:13 +02:00
case 'det-copy-otp':
this.copyOtp();
break;
2016-07-30 11:25:22 +02:00
case 'det-add-new':
this.addNewField();
break;
2016-08-21 19:39:02 +02:00
case 'det-clone':
this.clone();
break;
2017-02-05 15:16:40 +01:00
case 'det-auto-type':
this.autoType();
break;
2019-09-14 22:12:02 +02:00
case 'copy-to-clipboard':
this.copyToClipboard();
break;
2016-07-30 11:25:22 +02:00
}
2019-09-16 19:24:15 +02:00
}
2016-07-30 11:25:22 +02:00
2019-08-18 10:17:09 +02:00
setupOtp() {
2019-01-01 20:18:33 +01:00
OtpQrReader.read();
2019-09-16 19:24:15 +02:00
}
2016-03-31 22:52:04 +02:00
2019-08-18 10:17:09 +02:00
otpCodeRead(otp) {
2016-04-01 23:09:16 +02:00
this.model.setOtp(otp);
this.entryUpdated();
2019-09-16 19:24:15 +02:00
}
2016-04-04 20:45:17 +02:00
2019-08-18 10:17:09 +02:00
otpEnterManually() {
2016-04-04 20:45:17 +02:00
if (this.model.fields.otp) {
2017-01-31 07:50:28 +01:00
const otpField = this.fieldViews.find(f => f.model.name === '$otp');
2016-04-04 20:45:17 +02:00
if (otpField) {
otpField.edit();
}
} else {
this.moreView.remove();
this.moreView = null;
2019-09-16 19:55:06 +02:00
const fieldView = new FieldViewCustom(
{
2019-08-16 23:05:39 +02:00
name: '$otp',
title: 'otp',
newField: 'otp',
value: kdbxweb.ProtectedValue.fromString('')
2019-09-16 19:55:06 +02:00
},
{
parent: this.$el.find('.details__body-fields')[0]
2019-08-16 23:05:39 +02:00
}
2019-09-16 19:55:06 +02:00
);
2016-04-04 20:45:17 +02:00
fieldView.on('change', this.fieldChanged.bind(this));
2019-09-16 19:55:06 +02:00
fieldView.render();
2016-04-04 20:45:17 +02:00
fieldView.edit();
this.fieldViews.push(fieldView);
}
2019-09-16 19:24:15 +02:00
}
2016-04-10 09:02:32 +02:00
2019-08-18 10:17:09 +02:00
toggleAutoType() {
2016-04-23 16:50:40 +02:00
if (this.views.autoType) {
this.views.autoType.remove();
delete this.views.autoType;
return;
}
2019-09-16 19:09:57 +02:00
this.views.autoType = new DetailsAutoTypeView(this.model);
this.views.autoType.render();
2019-09-16 19:24:15 +02:00
}
2016-04-23 16:50:40 +02:00
2020-05-09 10:37:34 +02:00
autoType(sequence) {
const entry = this.model;
const hasOtp = sequence?.includes('{TOTP}') || (entry.external && !sequence);
if (hasOtp) {
2020-05-09 10:37:34 +02:00
const otpField = this.getFieldView('$otp');
otpField.refreshOtp(err => {
if (!err) {
Events.emit('auto-type', {
entry,
sequence,
context: { resolved: { totp: otpField.otpValue } }
});
}
});
} else {
Events.emit('auto-type', { entry, sequence });
}
}
2019-09-16 19:24:15 +02:00
}
2015-10-17 23:49:24 +02:00
2019-09-16 19:24:15 +02:00
Object.assign(DetailsView.prototype, Scrollable);
Object.assign(DetailsView.prototype, Copyable);
2015-10-17 23:49:24 +02:00
2019-09-15 14:16:32 +02:00
export { DetailsView };