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

542 lines
20 KiB
JavaScript
Raw Normal View History

2015-10-17 23:49:24 +02:00
'use strict';
var Backbone = require('backbone'),
2015-11-08 22:25:00 +01:00
GroupModel = require('../../models/group-model'),
2016-01-13 18:46:43 +01:00
AppSettingsModel = require('../../models/app-settings-model'),
Scrollable = require('../../mixins/scrollable'),
2015-10-17 23:49:24 +02:00
FieldViewText = require('../fields/field-view-text'),
FieldViewDate = require('../fields/field-view-date'),
FieldViewTags = require('../fields/field-view-tags'),
FieldViewUrl = require('../fields/field-view-url'),
FieldViewReadOnly = require('../fields/field-view-read-only'),
FieldViewHistory = require('../fields/field-view-history'),
FieldViewCustom = require('../fields/field-view-custom'),
2015-10-31 20:09:32 +01:00
IconSelectView = require('../icon-select-view'),
2015-10-17 23:49:24 +02:00
DetailsHistoryView = require('./details-history-view'),
DetailsAttachmentView = require('./details-attachment-view'),
Keys = require('../../const/keys'),
KeyHandler = require('../../comp/key-handler'),
2015-11-09 19:15:39 +01:00
Alerts = require('../../comp/alerts'),
CopyPaste = require('../../comp/copy-paste'),
2015-10-17 23:49:24 +02:00
Format = require('../../util/format'),
2015-12-17 19:25:25 +01:00
Locale = require('../../util/locale'),
2016-01-10 21:58:21 +01:00
Tip = require('../../util/tip'),
2016-01-11 18:56:52 +01:00
Timeouts = require('../../const/timeouts'),
2015-10-17 23:49:24 +02:00
FileSaver = require('filesaver'),
kdbxweb = require('kdbxweb');
var DetailsView = Backbone.View.extend({
2015-12-16 22:50:45 +01:00
template: require('templates/details/details.hbs'),
emptyTemplate: require('templates/details/details-empty.hbs'),
groupTemplate: require('templates/details/details-group.hbs'),
2015-10-17 23:49:24 +02:00
fieldViews: null,
views: null,
2016-01-11 18:56:52 +01:00
passEditView: null,
2016-01-13 18:46:43 +01:00
addNewFieldView: null,
2016-01-11 18:56:52 +01:00
passCopyTip: null,
2015-10-17 23:49:24 +02:00
events: {
'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',
2015-10-17 23:49:24 +02:00
'dragover .details': 'dragover',
'dragleave .details': 'dragleave',
'drop .details': 'drop'
},
initialize: function () {
this.fieldViews = [];
this.views = {};
this.initScroll();
this.listenTo(Backbone, 'select-entry', this.showEntry);
KeyHandler.onKey(Keys.DOM_VK_C, this.copyKeyPress, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_DELETE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
KeyHandler.onKey(Keys.DOM_VK_BACK_SPACE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
},
remove: function() {
KeyHandler.offKey(Keys.DOM_VK_C, this.copyKeyPress, this);
KeyHandler.offKey(Keys.DOM_VK_DELETE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
KeyHandler.offKey(Keys.DOM_VK_BACK_SPACE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
this.removeFieldViews();
Backbone.View.prototype.remove.call(this);
},
removeFieldViews: function() {
this.fieldViews.forEach(function(fieldView) { fieldView.remove(); });
this.fieldViews = [];
2016-01-11 18:56:52 +01:00
if (this.passCopyTip) {
this.passCopyTip.hide();
this.passCopyTip = null;
}
2015-10-17 23:49:24 +02:00
},
render: function () {
this.removeFieldViews();
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
}
if (!this.model) {
this.$el.html(this.emptyTemplate());
return;
}
2015-11-08 22:25:00 +01:00
if (this.model instanceof GroupModel) {
this.$el.html(this.groupTemplate());
2016-01-10 21:58:21 +01:00
Tip.createTips(this.$el);
2015-11-08 22:25:00 +01:00
return;
}
2015-11-09 19:32:51 +01:00
var model = $.extend({ deleted: this.appModel.filter.trash }, this.model);
2015-11-09 19:15:39 +01:00
this.$el.html(this.template(model));
2016-01-10 21:58:21 +01:00
Tip.createTips(this.$el);
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();
2015-10-17 23:49:24 +02:00
return this;
},
addFieldViews: function() {
var model = this.model;
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewText({ model: { name: '$UserName', title: Locale.detUser,
2015-10-17 23:49:24 +02:00
value: function() { return model.user; } } }));
2016-01-11 18:56:52 +01:00
this.passEditView = new FieldViewText({ model: { name: '$Password', title: Locale.detPassword, canGen: true,
value: function() { return model.password; } } });
this.fieldViews.push(this.passEditView);
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewUrl({ model: { name: '$URL', title: Locale.detWebsite,
2015-10-17 23:49:24 +02:00
value: function() { return model.url; } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewText({ model: { name: '$Notes', title: Locale.detNotes, multiline: 'true',
2015-10-17 23:49:24 +02:00
value: function() { return model.notes; } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewTags({ model: { name: 'Tags', title: Locale.detTags, tags: this.appModel.tags,
2015-10-17 23:49:24 +02:00
value: function() { return model.tags; } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: Locale.detExpires, lessThanNow: '(' + Locale.detExpired + ')',
2015-10-17 23:49:24 +02:00
value: function() { return model.expires; } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Locale.detFile,
2015-10-17 23:49:24 +02:00
value: function() { return model.fileName; } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Created', title: Locale.detCreated,
2015-10-17 23:49:24 +02:00
value: function() { return Format.dtStr(model.created); } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detUpdated,
2015-10-17 23:49:24 +02:00
value: function() { return Format.dtStr(model.updated); } } }));
2015-12-17 19:25:25 +01:00
this.fieldViews.push(new FieldViewHistory({ model: { name: 'History', title: Locale.detHistory,
2015-10-17 23:49:24 +02:00
value: function() { return { length: model.historyLength, unsaved: model.unsaved }; } } }));
_.forEach(model.fields, function(value, field) {
this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field,
value: function() { return model.fields[field]; } } }));
}, this);
2015-12-17 19:25:25 +01:00
var newFieldTitle = Locale.detNetField;
2015-10-17 23:49:24 +02:00
if (model.fields[newFieldTitle]) {
for (var i = 1; ; i++) {
var newFieldTitleVariant = newFieldTitle + i;
if (!model.fields[newFieldTitleVariant]) {
newFieldTitle = newFieldTitleVariant;
break;
}
}
}
2016-01-16 13:35:34 +01:00
this.addNewFieldView = new FieldViewCustom({ model: { name: '$', title: Locale.detAddField, newField: newFieldTitle,
2016-01-13 18:46:43 +01:00
value: function() { return ''; } } });
this.fieldViews.push(this.addNewFieldView);
2015-10-17 23:49:24 +02:00
var fieldsMainEl = this.$el.find('.details__body-fields');
var fieldsAsideEl = this.$el.find('.details__body-aside');
this.fieldViews.forEach(function(fieldView) {
fieldView.setElement(fieldView.readonly ? fieldsAsideEl : fieldsMainEl).render();
fieldView.on('change', this.fieldChanged.bind(this));
}, this);
},
setSelectedColor: function(color) {
this.$el.find('.details__colors-popup > .details__colors-popup-item').removeClass('details__colors-popup-item--active');
var colorEl = this.$el.find('.details__header-color')[0];
_.forEach(colorEl.classList, function(cls) {
if (cls.indexOf('color') > 0 && cls.lastIndexOf('details', 0) !== 0) {
colorEl.classList.remove(cls);
}
});
if (color) {
this.$el.find('.details__colors-popup > .' + color + '-color').addClass('details__colors-popup-item--active');
colorEl.classList.add(color + '-color');
}
},
selectColor: function(e) {
var color = $(e.target).closest('.details__colors-popup-item').data('color');
if (!color) {
return;
}
if (color === this.model.color) {
color = null;
}
this.model.setColor(color);
this.entryUpdated();
},
toggleIcons: function() {
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();
2015-11-21 23:15:51 +01:00
var subView = new IconSelectView({
el: this.scroller,
model: {
iconId: this.model.customIconId || this.model.iconId,
url: this.model.url, file: this.model.file
}
});
2015-10-17 23:49:24 +02:00
this.listenTo(subView, 'select', this.iconSelected);
subView.render();
this.pageResized();
this.views.sub = subView;
},
toggleAttachment: function(e) {
var attBtn = $(e.target).closest('.details__attachment');
var id = attBtn.data('id');
var attachment = this.model.attachments[id];
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();
var subView = new DetailsAttachmentView({ el: this.scroller, model: attachment });
subView.attId = id;
subView.render(this.pageResized.bind(this));
this.views.sub = subView;
attBtn.addClass('details__attachment--active');
},
removeSubView: function() {
this.$el.find('.details__attachment').removeClass('details__attachment--active');
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
}
},
downloadAttachment: function(attachment) {
var data = attachment.getBinary();
if (!data) {
return;
}
var mimeType = attachment.mimeType || 'application/octet-stream';
var blob = new Blob([data], {type: mimeType});
FileSaver.saveAs(blob, attachment.title);
},
2015-11-21 23:15:51 +01:00
iconSelected: function(sel) {
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();
}
},
showEntry: function(entry) {
this.model = entry;
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();
}
2015-10-17 23:49:24 +02:00
},
copyKeyPress: function() { // TODO: fix this in Safari
if (!window.getSelection().toString()) {
var pw = this.model.password;
2016-01-16 15:19:33 +01:00
var password = pw.isProtected ? pw.getText() : pw;
2016-01-22 18:51:36 +01:00
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(password);
}
var copyRes = CopyPaste.copy(password);
if (copyRes && !this.passCopyTip) {
2016-01-11 18:56:52 +01:00
var passLabel = this.passEditView.labelEl;
2016-01-22 18:51:36 +01:00
var clipboardTime = copyRes.seconds;
2016-01-11 18:56:52 +01:00
var msg = clipboardTime ? Locale.detPassCopiedTime.replace('{}', clipboardTime)
: Locale.detPassCopied;
2016-01-12 22:08:24 +01:00
var tip = new Tip(passLabel, { title: msg, placement: 'right', fast: true });
2016-01-11 18:56:52 +01:00
this.passCopyTip = tip;
tip.show();
var that = this;
setTimeout(function() {
tip.hide();
that.passCopyTip = null;
}, Timeouts.CopyTip);
}
2015-10-17 23:49:24 +02:00
}
},
2016-01-13 18:46:43 +01:00
showCopyTip: function() {
if (this.helpTipCopyShown) {
return;
}
this.helpTipCopyShown = AppSettingsModel.instance.get('helpTipCopyShown');
if (this.helpTipCopyShown) {
return;
}
AppSettingsModel.instance.set('helpTipCopyShown', true);
this.helpTipCopyShown = true;
var newFieldLabel = this.addNewFieldView.labelEl;
var tip = new Tip(newFieldLabel, { title: Locale.detCopyHint, placement: 'right' });
tip.show();
setTimeout(function() { tip.hide(); }, Timeouts.AutoHideHint);
},
2015-10-17 23:49:24 +02:00
fieldChanged: function(e) {
if (e.field) {
if (e.field[0] === '$') {
var fieldName = e.field.substr(1);
2016-01-16 13:35:34 +01:00
if (e.newField && e.newField !== fieldName) {
if (fieldName) {
this.model.setField(fieldName, undefined);
}
fieldName = e.newField;
var i = 0;
while (this.model.hasField(fieldName)) {
i++;
fieldName = e.newField + i;
}
this.model.setField(fieldName, e.val);
2015-10-17 23:49:24 +02:00
this.entryUpdated();
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') {
var dt = e.val || undefined;
if (!_.isEqual(dt, this.model.expires)) {
this.model.setExpires(dt);
}
}
this.entryUpdated(true);
this.fieldViews.forEach(function(fieldView, ix) {
if (fieldView instanceof FieldViewCustom && !fieldView.model.newField &&
!this.model.hasField(fieldView.model.title)) {
fieldView.remove();
this.fieldViews.splice(ix, 1);
} else {
fieldView.update();
}
}, this);
}
if (e.tab) {
this.focusNextField(e.tab);
}
},
dragover: function(e) {
e.preventDefault();
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
if (this.model && !this.dragging) {
this.dragging = true;
this.$el.find('.details').addClass('details--drag');
}
},
dragleave: function() {
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
this.dragTimeout = setTimeout((function() {
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
}).bind(this), 100);
},
drop: function(e) {
e.preventDefault();
if (!this.model) {
return;
}
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
}
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
var files = e.target.files || e.dataTransfer.files;
_.forEach(files, function(file) {
var reader = new FileReader();
reader.onload = (function() {
this.addAttachment(file.name, reader.result);
}).bind(this);
reader.readAsArrayBuffer(file);
}, this);
},
addAttachment: function(name, data) {
this.model.addAttachment(name, data);
this.entryUpdated();
},
deleteKeyPress: function() {
if (this.views.sub && this.views.sub.attId !== undefined) {
var attachment = this.model.attachments[this.views.sub.attId];
this.model.removeAttachment(attachment.title);
this.render();
}
},
editTitle: function() {
var input = $('<input/>')
.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);
},
titleInputBlur: function(e) {
this.setTitle(e.target.value);
},
titleInputInput: function(e) {
e.stopPropagation();
},
titleInputKeydown: function(e) {
2015-11-17 22:49:12 +01:00
KeyHandler.reg();
2015-10-17 23:49:24 +02:00
e.stopPropagation();
var code = e.keyCode || e.which;
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();
Backbone.trigger('refresh');
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' });
}
}
},
setTitle: function(title) {
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);
}
2015-11-04 20:06:39 +01:00
var 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);
},
entryUpdated: function(skipRender) {
Backbone.trigger('entry-updated', { entry: this.model });
if (!skipRender) {
this.render();
}
},
focusNextField: function(config) {
var found = false, nextFieldView;
if (config.field === '$Title' && !config.prev) {
found = true;
}
var start = config.prev ? this.fieldViews.length - 1 : 0;
var end = config.prev ? -1 : this.fieldViews.length;
var inc = config.prev ? -1 : 1;
for (var i = start; i !== end; i += inc) {
var fieldView = this.fieldViews[i];
if (fieldView.model.name === config.field) {
found = true;
} else if (found && !fieldView.readonly) {
nextFieldView = fieldView;
break;
}
}
if (nextFieldView) {
nextFieldView.edit();
}
},
showHistory: function() {
this.removeSubView();
var subView = new DetailsHistoryView({ el: this.scroller, model: this.model });
this.listenTo(subView, 'close', this.historyClosed.bind(this));
subView.render();
this.pageResized();
this.views.sub = subView;
},
historyClosed: function(e) {
if (e.updated) {
this.entryUpdated();
} else {
this.render();
}
},
moveToTrash: function() {
this.model.moveToTrash();
Backbone.trigger('refresh');
2015-10-26 22:07:19 +01:00
},
2015-11-09 19:15:39 +01:00
deleteFromTrash: function() {
Alerts.yesno({
2015-12-17 19:25:25 +01:00
header: Locale.detDelFromTrash,
body: Locale.detDelFromTrashBody + ' <p class="muted-color">' + Locale.detDelFromTrashBodyHint + '</p>',
2015-11-09 19:15:39 +01:00
icon: 'minus-circle',
success: (function() {
this.model.deleteFromTrash();
Backbone.trigger('refresh');
}).bind(this)
});
},
2015-10-26 22:07:19 +01:00
backClick: function() {
Backbone.trigger('toggle-details', false);
2015-10-17 23:49:24 +02:00
}
});
_.extend(DetailsView.prototype, Scrollable);
module.exports = DetailsView;