diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index edc13e5a..a2b7a758 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -21,7 +21,8 @@ var AppSettingsModel = Backbone.Model.extend({ lockOnMinimize: true, lockOnCopy: false, helpTipCopyShown: false, - skipOpenLocalWarn: false + skipOpenLocalWarn: false, + hideEmptyFields: false }, initialize: function() { diff --git a/app/scripts/util/locale.js b/app/scripts/util/locale.js index 1aab2367..534c7495 100644 --- a/app/scripts/util/locale.js +++ b/app/scripts/util/locale.js @@ -163,7 +163,6 @@ var Locale = { detUpdated: 'Updated', detHistory: 'History', detNetField: 'New Field', - detAddField: 'add field', detAttachments: 'Attachments', detDelFromTrash: 'Delete from trash?', detDelFromTrashBody: 'You will not be able to put it back.', @@ -171,6 +170,12 @@ var Locale = { detFieldCopied: 'Copied', detFieldCopiedTime: 'Copied for {} seconds', detCopyHint: 'You can copy field value with click on its title', + detMore: 'more', + detClickToAddField: 'click to add a new field', + detMenuAddNewField: 'Add new field', + detMenuShowEmpty: 'Show empty fields', + detMenuHideEmpty: 'Hide empty fields', + detMenuAddField: 'Add {}', appSecWarn: 'Not Secure!', appSecWarnBody1: 'You have loaded this app with insecure connection. ' + diff --git a/app/scripts/views/details/details-add-field-view.js b/app/scripts/views/details/details-add-field-view.js new file mode 100644 index 00000000..fb9e27fc --- /dev/null +++ b/app/scripts/views/details/details-add-field-view.js @@ -0,0 +1,28 @@ +'use strict'; + +var Backbone = require('backbone'); + +var DetailsAddFieldView = Backbone.View.extend({ + template: require('templates/details/details-add-field.hbs'), + + events: { + 'click .details__field-label': 'fieldLabelClick', + 'click .details__field-value': 'fieldValueClick' + }, + + render: function () { + this.renderTemplate(); + this.labelEl = this.$el.find('.details__field-label'); + return this; + }, + + fieldLabelClick: function() { + this.trigger('more-click'); + }, + + fieldValueClick: function() { + this.trigger('add-field'); + } +}); + +module.exports = DetailsAddFieldView; diff --git a/app/scripts/views/details/details-view.js b/app/scripts/views/details/details-view.js index 8344333c..e6195113 100644 --- a/app/scripts/views/details/details-view.js +++ b/app/scripts/views/details/details-view.js @@ -1,6 +1,7 @@ 'use strict'; var Backbone = require('backbone'), + kdbxweb = require('kdbxweb'), GroupModel = require('../../models/group-model'), AppSettingsModel = require('../../models/app-settings-model'), Scrollable = require('../../mixins/scrollable'), @@ -15,6 +16,8 @@ var Backbone = require('backbone'), IconSelectView = require('../icon-select-view'), DetailsHistoryView = require('./details-history-view'), DetailsAttachmentView = require('./details-attachment-view'), + DetailsAddFieldView = require('./details-add-field-view'), + DropdownView = require('../../views/dropdown-view'), Keys = require('../../const/keys'), KeyHandler = require('../../comp/key-handler'), Alerts = require('../../comp/alerts'), @@ -23,8 +26,7 @@ var Backbone = require('backbone'), Locale = require('../../util/locale'), Tip = require('../../util/tip'), Timeouts = require('../../const/timeouts'), - FileSaver = require('filesaver'), - kdbxweb = require('kdbxweb'); + FileSaver = require('filesaver'); var DetailsView = Backbone.View.extend({ template: require('templates/details/details.hbs'), @@ -36,7 +38,6 @@ var DetailsView = Backbone.View.extend({ passEditView: null, userEditView: null, urlEditView: null, - addNewFieldView: null, fieldCopyTip: null, events: { @@ -152,19 +153,8 @@ var DetailsView = Backbone.View.extend({ this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field, value: function() { return model.fields[field]; } } })); }, this); - var newFieldTitle = Locale.detNetField; - if (model.fields[newFieldTitle]) { - for (var i = 1; ; i++) { - var newFieldTitleVariant = newFieldTitle + i; - if (!model.fields[newFieldTitleVariant]) { - newFieldTitle = newFieldTitleVariant; - break; - } - } - } - this.addNewFieldView = new FieldViewCustom({ model: { name: '$', title: Locale.detAddField, newField: newFieldTitle, - value: function() { return ''; } } }); - this.fieldViews.push(this.addNewFieldView); + + var hideEmptyFields = AppSettingsModel.instance.get('hideEmptyFields'); var fieldsMainEl = this.$el.find('.details__body-fields'); var fieldsAsideEl = this.$el.find('.details__body-aside'); @@ -172,7 +162,92 @@ var DetailsView = Backbone.View.extend({ fieldView.setElement(fieldView.readonly ? fieldsAsideEl : fieldsMainEl).render(); fieldView.on('change', this.fieldChanged.bind(this)); fieldView.on('copy', this.fieldCopied.bind(this)); + if (hideEmptyFields && !fieldView.model.value()) { + fieldView.hide(); + } }, this); + + this.moreView = new DetailsAddFieldView(); + this.moreView.setElement(fieldsMainEl).render(); + this.moreView.on('add-field', this.addNewField.bind(this)); + this.moreView.on('more-click', this.toggleMoreOptions.bind(this)); + }, + + addNewField: function() { + this.moreView.remove(); + this.moreView = null; + var newFieldTitle = Locale.detNetField; + if (this.model.fields[newFieldTitle]) { + for (var i = 1; ; i++) { + var newFieldTitleVariant = newFieldTitle + i; + if (!this.model.fields[newFieldTitleVariant]) { + newFieldTitle = newFieldTitleVariant; + break; + } + } + } + var fieldView = new FieldViewCustom({ model: { name: '$' + newFieldTitle, title: newFieldTitle, newField: newFieldTitle, + value: function() { return ''; } } }); + fieldView.on('change', this.fieldChanged.bind(this)); + fieldView.setElement(this.$el.find('.details__body-fields')).render(); + fieldView.edit(); + this.fieldViews.push(fieldView); + }, + + toggleMoreOptions: function() { + if (this.views.dropdownView) { + this.views.dropdownView.remove(); + this.views.dropdownView = null; + } else { + this.setTimeout(function() { + var dropdownView = new DropdownView(); + this.listenTo(dropdownView, 'cancel', this.toggleMoreOptions); + this.listenTo(dropdownView, 'select', this.moreOptionsSelect); + var hideEmptyFields = AppSettingsModel.instance.get('hideEmptyFields'); + var moreOptions = []; + if (hideEmptyFields) { + this.fieldViews.forEach(function(fieldView) { + if (!fieldView.model.value()) { + moreOptions.push({value: 'add:' + fieldView.model.name, icon: 'pencil', + text: Locale.detMenuAddField.replace('{}', fieldView.model.title)}); + } + }, this); + moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField}); + moreOptions.push({value: 'toggle-empty', icon: 'eye', text: Locale.detMenuShowEmpty}); + } else { + moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField}); + moreOptions.push({value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty}); + } + var rect = this.moreView.labelEl[0].getBoundingClientRect(); + dropdownView.render({ + position: {top: rect.bottom, right: rect.right}, + options: moreOptions + }); + this.views.dropdownView = dropdownView; + }); + } + }, + + moreOptionsSelect: function(e) { + this.views.dropdownView.remove(); + this.views.dropdownView = null; + switch (e.item) { + case 'add-new': + this.addNewField(); + break; + case 'toggle-empty': + var hideEmptyFields = AppSettingsModel.instance.get('hideEmptyFields'); + AppSettingsModel.instance.set('hideEmptyFields', !hideEmptyFields); + this.render(); + break; + default: + if (e.item.lastIndexOf('add:', 0) === 0) { + var fieldName = e.item.substr(4); + var fieldView = _.find(this.fieldViews, function(f) { return f.model.name === fieldName; }); + fieldView.show(); + fieldView.edit(); + } + } }, getUserNameCompletions: function(part) { @@ -289,7 +364,7 @@ var DetailsView = Backbone.View.extend({ copyKeyPress: function(editView) { if (!window.getSelection().toString()) { var fieldValue = editView.value; - var fieldText = fieldValue.isProtected ? fieldValue.getText() : fieldValue; + var fieldText = fieldValue && fieldValue.isProtected ? fieldValue.getText() : fieldValue; if (!fieldText) { return; } @@ -325,8 +400,8 @@ var DetailsView = Backbone.View.extend({ } AppSettingsModel.instance.set('helpTipCopyShown', true); this.helpTipCopyShown = true; - var newFieldLabel = this.addNewFieldView.labelEl; - var tip = new Tip(newFieldLabel, { title: Locale.detCopyHint, placement: 'right' }); + var label = this.moreView.labelEl; + var tip = new Tip(label, { title: Locale.detCopyHint, placement: 'right' }); tip.show(); setTimeout(function() { tip.hide(); }, Timeouts.AutoHideHint); }, @@ -335,7 +410,7 @@ var DetailsView = Backbone.View.extend({ if (e.field) { if (e.field[0] === '$') { var fieldName = e.field.substr(1); - if (e.newField && e.newField !== fieldName) { + if (e.newField) { if (fieldName) { this.model.setField(fieldName, undefined); } @@ -370,6 +445,9 @@ var DetailsView = Backbone.View.extend({ fieldView.update(); } }, this); + } else if (e.newField) { + this.render(); + return; } if (e.tab) { this.focusNextField(e.tab); @@ -538,7 +616,7 @@ var DetailsView = Backbone.View.extend({ var fieldView = this.fieldViews[i]; if (fieldView.model.name === config.field) { found = true; - } else if (found && !fieldView.readonly) { + } else if (found && !fieldView.readonly && !fieldView.isHidden()) { nextFieldView = fieldView; break; } diff --git a/app/scripts/views/dropdown-view.js b/app/scripts/views/dropdown-view.js index 8bb1c57a..a2ea21bd 100644 --- a/app/scripts/views/dropdown-view.js +++ b/app/scripts/views/dropdown-view.js @@ -19,7 +19,8 @@ var DropdownView = Backbone.View.extend({ this.renderTemplate(config); this.$el.appendTo(document.body); var ownRect = this.$el[0].getBoundingClientRect(); - this.$el.css({ top: config.position.top, left: config.position.right - ownRect.right + ownRect.left }); + var left = config.position.left || (config.position.right - ownRect.right + ownRect.left); + this.$el.css({ top: config.position.top, left: left }); return this; }, diff --git a/app/scripts/views/fields/field-view-custom.js b/app/scripts/views/fields/field-view-custom.js index 18db3edb..42c133ee 100644 --- a/app/scripts/views/fields/field-view-custom.js +++ b/app/scripts/views/fields/field-view-custom.js @@ -4,7 +4,6 @@ var Backbone = require('backbone'), FieldViewText = require('./field-view-text'), FieldView = require('./field-view'), Keys = require('../../const/keys'), - Locale = require('../../util/locale'), kdbxweb = require('kdbxweb'); var FieldViewCustom = FieldViewText.extend({ @@ -18,10 +17,6 @@ var FieldViewCustom = FieldViewText.extend({ startEdit: function() { FieldViewText.prototype.startEdit.call(this); - if (this.model.newField && this.model.title === Locale.detAddField) { - this.model.title = this.model.newField; - this.$el.find('.details__field-label').text(this.model.newField); - } this.$el.addClass('details__field--can-edit-title'); if (this.isProtected === undefined) { this.isProtected = this.value instanceof kdbxweb.ProtectedValue; @@ -50,10 +45,6 @@ var FieldViewCustom = FieldViewText.extend({ } } FieldView.prototype.endEdit.call(this, newVal, extra); - if (!newVal && this.model.newField) { - this.model.title = Locale.detAddField; - this.$el.find('.details__field-label').text(this.model.title); - } if (this.model.titleChanged) { delete this.model.titleChanged; } @@ -88,11 +79,10 @@ var FieldViewCustom = FieldViewText.extend({ fieldLabelClick: function(e) { e.stopImmediatePropagation(); - if (this.editing) { - this.startEditTitle(); - } else if (this.model.newField) { - this.edit(); + if (this.model.newField) { this.startEditTitle(true); + } else if (this.editing) { + this.startEditTitle(); } else { FieldViewText.prototype.fieldLabelClick.call(this, e); } diff --git a/app/styles/areas/_details.scss b/app/styles/areas/_details.scss index e9dfe527..86a7de22 100644 --- a/app/styles/areas/_details.scss +++ b/app/styles/areas/_details.scss @@ -196,6 +196,9 @@ overflow: hidden; text-overflow: ellipsis; margin-right: 20px; + &-add-label { + color: transparent; + } .details__field--editable & { border-radius: $base-border-radius; &:hover { @@ -204,6 +207,10 @@ border: 1px solid light-border-color(); box-shadow: 0 0 3px form-box-shadow-color(); } + .details__field-value-add-label { + @include th { color: muted-color(); } + transition: color $base-duration $base-timing; + } } } .details__field--multiline & { diff --git a/app/templates/details/details-add-field.hbs b/app/templates/details/details-add-field.hbs new file mode 100644 index 00000000..f0dce4d1 --- /dev/null +++ b/app/templates/details/details-add-field.hbs @@ -0,0 +1,6 @@ +
+
{{res 'detMore'}}…
+
+
{{res 'detClickToAddField'}}
+
+
diff --git a/release-notes.md b/release-notes.md index 329ea0c7..3c6c4760 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,6 +17,7 @@ Storage providers, usability improvements `+` build for 32-bit linux `+` ability to import xml `+` warning for kdb files +`+` hide empty fields `-` fix #88: capslock indicator `-` fix file settings input behavior