diff --git a/.babelrc b/.babelrc index 4397fe97..f131a5d2 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], + "presets": [], "plugins": [ ["@babel/plugin-proposal-class-properties", { "loose": true }], "@babel/plugin-external-helpers" diff --git a/app/scripts/util/ui/tip.js b/app/scripts/util/ui/tip.js index cc5254cd..81a55240 100644 --- a/app/scripts/util/ui/tip.js +++ b/app/scripts/util/ui/tip.js @@ -11,6 +11,9 @@ const Tip = function(el, config) { this.hideTimeout = null; this.force = (config && config.force) || false; this.hide = this.hide.bind(this); + this.destroy = this.destroy.bind(this); + this.mouseenter = this.mouseenter.bind(this); + this.mouseleave = this.mouseleave.bind(this); }; Tip.enabled = !Features.isMobile; @@ -21,8 +24,8 @@ Tip.prototype.init = function() { } this.el.removeAttr('title'); this.el.attr('data-title', this.title); - this.el.mouseenter(this.mouseenter.bind(this)).mouseleave(this.mouseleave.bind(this)); - this.el.click(this.mouseleave.bind(this)); + this.el.mouseenter(this.mouseenter).mouseleave(this.mouseleave); + this.el.click(this.mouseleave); }; Tip.prototype.show = function() { @@ -80,8 +83,15 @@ Tip.prototype.hide = function() { if (this.tipEl) { this.tipEl.remove(); this.tipEl = null; + Backbone.off('page-geometry', this.hide); } - Backbone.off('page-geometry', this.hide); +}; + +Tip.prototype.destroy = function() { + this.hide(); + this.el.off('mouseenter', this.mouseenter); + this.el.off('mouseleave', this.mouseleave); + this.el.off('click', this.mouseleave); }; Tip.prototype.mouseenter = function() { @@ -138,7 +148,7 @@ Tip.createTips = function(container) { if (!Tip.enabled) { return; } - container.find('[title]').each((ix, el) => { + $('[title]', container).each((ix, el) => { Tip.createTip(el); }); }; @@ -156,10 +166,10 @@ Tip.createTip = function(el, options) { }; Tip.hideTips = function(container) { - if (!Tip.enabled) { + if (!Tip.enabled || !container) { return; } - container.find('[data-title]').each((ix, el) => { + $('[data-title]', container).each((ix, el) => { Tip.hideTip(el); }); }; @@ -180,4 +190,13 @@ Tip.updateTip = function(el, props) { } }; +Tip.destroyTips = function(container) { + $('[data-title]', container).each((ix, el) => { + if (el._tip) { + el._tip.destroy(); + el._tip = undefined; + } + }); +}; + export { Tip }; diff --git a/app/scripts/view-engine/scrollable.js b/app/scripts/view-engine/scrollable.js index a9605a89..36996f7f 100644 --- a/app/scripts/view-engine/scrollable.js +++ b/app/scripts/view-engine/scrollable.js @@ -13,6 +13,7 @@ const Scrollable = { this.removeScroll(); } this.scroll = baron(opts); + this.once('remove', () => this.removeScroll); } this.scroller = this.$el.find('.scroller'); this.scrollerBar = this.$el.find('.scroller__bar'); @@ -21,7 +22,9 @@ const Scrollable = { removeScroll() { if (this.scroll) { - this.scroll.dispose(); + try { + this.scroll.dispose(); + } catch {} this.scroll = null; } }, diff --git a/app/scripts/view-engine/view.js b/app/scripts/view-engine/view.js new file mode 100644 index 00000000..dba4d612 --- /dev/null +++ b/app/scripts/view-engine/view.js @@ -0,0 +1,177 @@ +import morphdom from 'morphdom'; +import EventEmitter from 'events'; +import { Tip } from 'util/ui/tip'; +import { KeyHandler } from 'comp/browser/key-handler'; +import { Logger } from 'util/logger'; + +class View extends EventEmitter { + parent = undefined; + template = undefined; + events = {}; + views = {}; + hidden = false; + removed = false; + boundEvents = []; + debugLogger = localStorage.debugViews ? new Logger('view', this.constructor.name) : undefined; + + constructor(model) { + super(); + + this.model = model; + } + + render(templateData) { + if (this.removed) { + return; + } + + this.debugLogger && this.debugLogger.debug('Render start'); + + if (this.el) { + Tip.destroyTips(this.el); + } + + this.unbindEvents(); + this.renderElement(templateData); + this.bindEvents(); + + Tip.createTips(this.el); + + this.debugLogger && this.debugLogger.debug('Render finished'); + + return this; + } + + renderElement(templateData) { + const html = this.template(templateData); + if (this.el) { + morphdom(this.el, html); + } else { + const el = document.createElement('div'); + el.innerHTML = html; + this.el = el.firstChild; + if (this.parent) { + const parent = document.querySelector(this.parent); + if (!parent) { + throw new Error( + `Error rendering ${this.constructor.name}: parent not found: ${parent}` + ); + } + parent.appendChild(this.el); + } else { + throw new Error( + `Error rendering ${this.constructor.name}: I don't know how to insert the view` + ); + } + this.$el = $(this.el); // legacy + } + } + + bindEvents() { + for (const [eventDef, method] of Object.entries(this.events)) { + const spaceIx = eventDef.indexOf(' '); + let event, targets; + if (spaceIx > 0) { + event = eventDef.substr(0, spaceIx); + const selector = eventDef.substr(spaceIx + 1); + targets = this.el.querySelectorAll(selector); + } else { + event = eventDef; + targets = [this.el]; + } + for (const target of targets) { + const listener = e => { + this.debugLogger && this.debugLogger.debug('Listener', method); + this[method](e); + }; + target.addEventListener(event, listener); + this.boundEvents.push({ target, event, listener }); + } + } + } + + unbindEvents() { + for (const boundEvent of this.boundEvents) { + const { target, event, listener } = boundEvent; + target.removeEventListener(event, listener); + } + } + + remove() { + this.emit('remove'); + + this.removeInnerViews(); + Tip.hideTips(this.el); + this.el.remove(); + this.removed = true; + + this.debugLogger && this.debugLogger.debug('Remove'); + } + + removeInnerViews() { + if (this.views) { + for (const view of Object.values(this.views)) { + if (view) { + if (view instanceof Array) { + view.forEach(v => v.remove()); + } else { + view.remove(); + } + } + } + this.views = {}; + } + } + + listenTo(model, event, callback) { + const boundCallback = callback.bind(this); + model.on(event, boundCallback); + this.once('remove', () => model.off(event, boundCallback)); + } + + stopListening(model, event, callback) { + model.off(event, callback); + } + + hide() { + Tip.hideTips(this.el); + return this.toggle(false); + } + + show() { + return this.toggle(true); + } + + toggle(visible) { + if (visible === undefined) { + visible = this.hidden; + } + this.el.classList.toggle('show', !!visible); + this.el.classList.toggle('hide', !visible); + this.hidden = !visible; + this.emit(visible ? 'show' : 'hide'); + if (!visible) { + Tip.hideTips(this.el); + } + return this; + } + + isHidden() { + return this.hidden; + } + + isVisible() { + return !this.hidden; + } + + afterPaint(callback) { + requestAnimationFrame(() => requestAnimationFrame(callback)); + } + + onKey(key, handler, shortcut, modal, noPrevent) { + KeyHandler.onKey(key, handler, this, shortcut, modal, noPrevent); + this.once('remove', () => KeyHandler.offKey(key, handler, this)); + } +} + +export { View }; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index ab8a787a..1ef120e3 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -45,7 +45,7 @@ const AppView = Backbone.View.extend({ this.views = {}; this.views.menu = new MenuView({ model: this.model.menu }); this.views.menuDrag = new DragView('x'); - this.views.footer = new FooterView({ model: this.model }); + this.views.footer = new FooterView(this.model); this.views.listWrap = new ListWrapView({ model: this.model }); this.views.list = new ListView({ model: this.model }); this.views.listDrag = new DragView('x'); @@ -146,7 +146,7 @@ const AppView = Backbone.View.extend({ this.views.listWrap.setElement(this.$el.find('.app__list-wrap')).render(); this.views.menu.setElement(this.$el.find('.app__menu')).render(); this.views.menuDrag.setElement(this.$el.find('.app__menu-drag')).render(); - this.views.footer.setElement(this.$el.find('.app__footer')).render(); + this.views.footer.render(); this.views.list.setElement(this.$el.find('.app__list')).render(); this.views.listDrag.setElement(this.$el.find('.app__list-drag')).render(); this.views.details.setElement(this.$el.find('.app__details')).render(); @@ -240,7 +240,8 @@ const AppView = Backbone.View.extend({ this.views.listDrag.hide(); this.views.details.hide(); this.hidePanelView(); - this.views.panel = view.setElement(this.panelEl).render(); + view.render(); + this.views.panel = view; this.panelEl.removeClass('hide'); }, @@ -280,11 +281,11 @@ const AppView = Backbone.View.extend({ }, showEditGroup(group) { - this.showPanelView(new GrpView({ model: group })); + this.showPanelView(new GrpView(group)); }, showEditTag() { - this.showPanelView(new TagView({ model: this.model })); + this.showPanelView(new TagView(this.model)); }, showKeyChange(file, viewConfig) { @@ -688,7 +689,7 @@ const AppView = Backbone.View.extend({ if (this.views.settings) { this.showEntries(); } - this.showPanelView(new GeneratorPresetsView({ model: this.model })); + this.showPanelView(new GeneratorPresetsView(this.model)); } else { this.showEntries(); } diff --git a/app/scripts/views/fields/field-view-text.js b/app/scripts/views/fields/field-view-text.js index 29d04f05..b69ef3cf 100644 --- a/app/scripts/views/fields/field-view-text.js +++ b/app/scripts/views/fields/field-view-text.js @@ -93,10 +93,8 @@ const FieldViewText = FieldView.extend({ const fieldRect = this.input[0].getBoundingClientRect(); const shadowSpread = parseInt(this.input.css('--focus-shadow-spread')); this.gen = new GeneratorView({ - model: { - pos: { left: fieldRect.left, top: fieldRect.bottom + shadowSpread }, - password: this.value - } + pos: { left: fieldRect.left, top: fieldRect.bottom + shadowSpread }, + password: this.value }).render(); this.gen.once('remove', this.generatorClosed.bind(this)); this.gen.once('result', this.generatorResult.bind(this)); diff --git a/app/scripts/views/footer-view.js b/app/scripts/views/footer-view.js index 3be5ccc5..bea9f07e 100644 --- a/app/scripts/views/footer-view.js +++ b/app/scripts/views/footer-view.js @@ -1,68 +1,61 @@ import Backbone from 'backbone'; +import { View } from 'view-engine/view'; import { KeyHandler } from 'comp/browser/key-handler'; import { Keys } from 'const/keys'; import { UpdateModel } from 'models/update-model'; import { GeneratorView } from 'views/generator-view'; +import template from 'templates/footer.hbs'; -const FooterView = Backbone.View.extend({ - template: require('templates/footer.hbs'), +class FooterView extends View { + parent = '.app__footer'; - events: { + template = template; + + events = { 'click .footer__db-item': 'showFile', 'click .footer__db-open': 'openFile', 'click .footer__btn-help': 'toggleHelp', 'click .footer__btn-settings': 'toggleSettings', 'click .footer__btn-generate': 'genPass', 'click .footer__btn-lock': 'lockWorkspace' - }, + }; - initialize() { - this.views = {}; + constructor(model) { + super(model); - KeyHandler.onKey( - Keys.DOM_VK_L, - this.lockWorkspace, - this, - KeyHandler.SHORTCUT_ACTION, - false, - true - ); - KeyHandler.onKey(Keys.DOM_VK_G, this.genPass, this, KeyHandler.SHORTCUT_ACTION); - KeyHandler.onKey(Keys.DOM_VK_O, this.openFile, this, KeyHandler.SHORTCUT_ACTION); - KeyHandler.onKey(Keys.DOM_VK_S, this.saveAll, this, KeyHandler.SHORTCUT_ACTION); - KeyHandler.onKey(Keys.DOM_VK_COMMA, this.toggleSettings, this, KeyHandler.SHORTCUT_ACTION); + this.onKey(Keys.DOM_VK_L, this.lockWorkspace, KeyHandler.SHORTCUT_ACTION, false, true); + this.onKey(Keys.DOM_VK_G, this.genPass, KeyHandler.SHORTCUT_ACTION); + this.onKey(Keys.DOM_VK_O, this.openFile, KeyHandler.SHORTCUT_ACTION); + this.onKey(Keys.DOM_VK_S, this.saveAll, KeyHandler.SHORTCUT_ACTION); + this.onKey(Keys.DOM_VK_COMMA, this.toggleSettings, KeyHandler.SHORTCUT_ACTION); this.listenTo(this, 'hide', this.viewHidden); this.listenTo(this.model.files, 'update reset change', this.render); this.listenTo(Backbone, 'set-locale', this.render); this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render); - }, + } render() { - this.renderTemplate( - { - files: this.model.files, - updateAvailable: - ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0 - }, - { plain: true } - ); - return this; - }, + super.render({ + files: this.model.files, + updateAvailable: + ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0 + }); + } viewHidden() { if (this.views.gen) { this.views.gen.remove(); delete this.views.gen; } - }, + } lockWorkspace(e) { if (this.model.files.hasOpenFiles()) { e.preventDefault(); Backbone.trigger('lock-workspace'); } - }, + } genPass(e) { e.stopPropagation(); @@ -75,14 +68,12 @@ const FooterView = Backbone.View.extend({ const bodyRect = document.body.getBoundingClientRect(); const right = bodyRect.right - rect.right; const bottom = bodyRect.bottom - rect.top; - const generator = new GeneratorView({ - model: { copy: true, pos: { right, bottom } } - }).render(); + const generator = new GeneratorView({ copy: true, pos: { right, bottom } }).render(); generator.once('remove', () => { delete this.views.gen; }); this.views.gen = generator; - }, + } showFile(e) { const fileId = $(e.target) @@ -91,23 +82,23 @@ const FooterView = Backbone.View.extend({ if (fileId) { Backbone.trigger('show-file', { fileId }); } - }, + } openFile() { Backbone.trigger('open-file'); - }, + } saveAll() { Backbone.trigger('save-all'); - }, + } toggleHelp() { Backbone.trigger('toggle-settings', 'help'); - }, + } toggleSettings() { Backbone.trigger('toggle-settings', 'general'); } -}); +} export { FooterView }; diff --git a/app/scripts/views/generator-presets-view.js b/app/scripts/views/generator-presets-view.js index 53940b97..0189fd79 100644 --- a/app/scripts/views/generator-presets-view.js +++ b/app/scripts/views/generator-presets-view.js @@ -1,13 +1,17 @@ import Backbone from 'backbone'; +import { View } from 'view-engine/view'; import { GeneratorPresets } from 'comp/app/generator-presets'; import { PasswordGenerator } from 'util/generators/password-generator'; import { Locale } from 'util/locale'; import { Scrollable } from 'view-engine/scrollable'; +import template from 'templates/generator-presets.hbs'; -const GeneratorPresetsView = Backbone.View.extend({ - template: require('templates/generator-presets.hbs'), +class GeneratorPresetsView extends View { + parent = '.app__panel'; - events: { + template = template; + + events = { 'click .back-button': 'returnToApp', 'change .gen-ps__list': 'changePreset', 'click .gen-ps__btn-create': 'createPreset', @@ -18,44 +22,36 @@ const GeneratorPresetsView = Backbone.View.extend({ 'input #gen-ps__field-length': 'changeLength', 'change .gen-ps__check-range': 'changeRange', 'input #gen-ps__field-include': 'changeInclude' - }, + }; - selected: null, + selected = null; - reservedTitles: [Locale.genPresetDerived], - - initialize() { - this.appModel = this.model; - }, + reservedTitles = [Locale.genPresetDerived]; render() { this.presets = GeneratorPresets.all; if (!this.selected || !this.presets.some(p => p.name === this.selected)) { this.selected = (this.presets.filter(p => p.default)[0] || this.presets[0]).name; } - this.renderTemplate( - { - presets: this.presets, - selected: this.getPreset(this.selected), - ranges: this.getSelectedRanges() - }, - true - ); + super.render({ + presets: this.presets, + selected: this.getPreset(this.selected), + ranges: this.getSelectedRanges() + }); this.createScroll({ root: this.$el.find('.gen-ps')[0], scroller: this.$el.find('.scroller')[0], bar: this.$el.find('.scroller__bar')[0] }); this.renderExample(); - return this; - }, + } renderExample() { const selectedPreset = this.getPreset(this.selected); const example = PasswordGenerator.generate(selectedPreset); this.$el.find('.gen-ps__example').text(example); this.pageResized(); - }, + } getSelectedRanges() { const sel = this.getPreset(this.selected); @@ -73,20 +69,20 @@ const GeneratorPresetsView = Backbone.View.extend({ }; } ); - }, + } getPreset(name) { return this.presets.filter(p => p.name === name)[0]; - }, + } returnToApp() { Backbone.trigger('edit-generator-presets'); - }, + } changePreset(e) { this.selected = e.target.value; this.render(); - }, + } createPreset() { let name; @@ -116,12 +112,12 @@ const GeneratorPresetsView = Backbone.View.extend({ GeneratorPresets.add(preset); this.selected = name; this.render(); - }, + } deletePreset() { GeneratorPresets.remove(this.selected); this.render(); - }, + } changeTitle(e) { const title = $.trim(e.target.value); @@ -139,17 +135,17 @@ const GeneratorPresetsView = Backbone.View.extend({ GeneratorPresets.setPreset(this.selected, { title }); this.$el.find('.gen-ps__list option[selected]').text(title); } - }, + } changeEnabled(e) { const enabled = e.target.checked; GeneratorPresets.setDisabled(this.selected, !enabled); - }, + } changeDefault(e) { const isDefault = e.target.checked; GeneratorPresets.setDefault(isDefault ? this.selected : null); - }, + } changeLength(e) { const length = +e.target.value; @@ -161,7 +157,7 @@ const GeneratorPresetsView = Backbone.View.extend({ } this.presets = GeneratorPresets.all; this.renderExample(); - }, + } changeRange(e) { const enabled = e.target.checked; @@ -169,7 +165,7 @@ const GeneratorPresetsView = Backbone.View.extend({ GeneratorPresets.setPreset(this.selected, { [range]: enabled }); this.presets = GeneratorPresets.all; this.renderExample(); - }, + } changeInclude(e) { const include = e.target.value; @@ -179,8 +175,8 @@ const GeneratorPresetsView = Backbone.View.extend({ this.presets = GeneratorPresets.all; this.renderExample(); } -}); +} -_.extend(GeneratorPresetsView.prototype, Scrollable); +Object.assign(GeneratorPresetsView.prototype, Scrollable); export { GeneratorPresetsView }; diff --git a/app/scripts/views/generator-view.js b/app/scripts/views/generator-view.js index 6e96964a..ef0ba712 100644 --- a/app/scripts/views/generator-view.js +++ b/app/scripts/views/generator-view.js @@ -1,20 +1,21 @@ import Backbone from 'backbone'; +import { View } from 'view-engine/view'; import { GeneratorPresets } from 'comp/app/generator-presets'; import { CopyPaste } from 'comp/browser/copy-paste'; import { AppSettingsModel } from 'models/app-settings-model'; import { PasswordGenerator } from 'util/generators/password-generator'; import { Locale } from 'util/locale'; import { Tip } from 'util/ui/tip'; +import template from 'templates/generator.hbs'; -const GeneratorView = Backbone.View.extend({ - el: 'body', +class GeneratorView extends View { + parent = 'body'; - template: require('templates/generator.hbs'), + template = template; - events: { + events = { 'click': 'click', 'mousedown .gen__length-range': 'generate', - 'mousemove .gen__length-range': 'lengthMouseMove', 'input .gen__length-range': 'lengthChange', 'change .gen__length-range': 'lengthChange', 'change .gen__check input[type=checkbox]': 'checkChange', @@ -22,9 +23,9 @@ const GeneratorView = Backbone.View.extend({ 'click .gen__btn-ok': 'btnOkClick', 'change .gen__sel-tpl': 'presetChange', 'click .gen__btn-refresh': 'newPass' - }, + }; - valuesMap: [ + valuesMap = [ 3, 4, 5, @@ -51,19 +52,20 @@ const GeneratorView = Backbone.View.extend({ 32, 48, 64 - ], + ]; - presets: null, - preset: null, + presets = null; + preset = null; - initialize() { + constructor(model) { + super(model); this.createPresets(); const preset = this.preset; this.gen = _.clone(_.find(this.presets, pr => pr.name === preset)); this.hide = AppSettingsModel.instance.get('generatorHidePassword'); $('body').one('click', this.remove.bind(this)); this.listenTo(Backbone, 'lock-workspace', this.remove.bind(this)); - }, + } render() { const canCopy = document.queryCommandSupported('copy'); @@ -72,7 +74,7 @@ const GeneratorView = Backbone.View.extend({ ? Locale.alertCopy : Locale.alertClose : Locale.alertOk; - this.renderTemplate({ + super.render({ btnTitle, showToggleButton: this.model.copy, opt: this.gen, @@ -84,7 +86,7 @@ const GeneratorView = Backbone.View.extend({ this.$el.css(this.model.pos); this.generate(); return this; - }, + } createPresets() { this.presets = GeneratorPresets.enabled; @@ -100,10 +102,10 @@ const GeneratorView = Backbone.View.extend({ const defaultPreset = this.presets.filter(p => p.default)[0] || this.presets[0]; this.preset = defaultPreset.name; } - this.presets.forEach(function(pr) { + this.presets.forEach(pr => { pr.pseudoLength = this.lengthToPseudoValue(pr.length); - }, this); - }, + }); + } lengthToPseudoValue(length) { for (let ix = 0; ix < this.valuesMap.length; ix++) { @@ -112,7 +114,7 @@ const GeneratorView = Backbone.View.extend({ } } return this.valuesMap.length - 1; - }, + } showPassword() { if (this.hide && !this.model.copy) { @@ -120,11 +122,11 @@ const GeneratorView = Backbone.View.extend({ } else { this.resultEl.text(this.password); } - }, + } click(e) { e.stopPropagation(); - }, + } lengthChange(e) { const val = this.valuesMap[e.target.value]; @@ -134,7 +136,7 @@ const GeneratorView = Backbone.View.extend({ this.optionChanged('length'); this.generate(); } - }, + } checkChange(e) { const id = $(e.target).data('id'); @@ -143,7 +145,7 @@ const GeneratorView = Backbone.View.extend({ } this.optionChanged(id); this.generate(); - }, + } optionChanged(option) { if ( @@ -154,32 +156,31 @@ const GeneratorView = Backbone.View.extend({ } this.preset = this.gen.name = 'Custom'; this.$el.find('.gen__sel-tpl').val(''); - }, + } generate() { this.password = PasswordGenerator.generate(this.gen); this.showPassword(); const isLong = this.password.length > 32; this.resultEl.toggleClass('gen__result--long-pass', isLong); - }, + } hideChange(e) { this.hide = e.target.checked; - // AppSettingsModel.instance.unset('generatorHidePassword', { silent: true }); AppSettingsModel.instance.set('generatorHidePassword', this.hide); const label = this.$el.find('.gen__check-hide-label'); Tip.updateTip(label[0], { title: this.hide ? Locale.genShowPass : Locale.genHidePass }); this.showPassword(); - }, + } btnOkClick() { if (!CopyPaste.simpleCopy) { CopyPaste.createHiddenInput(this.password); } CopyPaste.copy(this.password); - this.trigger('result', this.password); + this.emit('result', this.password); this.remove(); - }, + } presetChange(e) { const name = e.target.value; @@ -192,11 +193,11 @@ const GeneratorView = Backbone.View.extend({ const preset = _.find(this.presets, t => t.name === name); this.gen = _.clone(preset); this.render(); - }, + } newPass() { this.generate(); } -}); +} export { GeneratorView }; diff --git a/app/scripts/views/grp-view.js b/app/scripts/views/grp-view.js index 7240cd31..9f8e5aff 100644 --- a/app/scripts/views/grp-view.js +++ b/app/scripts/views/grp-view.js @@ -1,13 +1,17 @@ +import { View } from 'view-engine/view'; import { AutoType } from 'auto-type'; import Backbone from 'backbone'; import { Scrollable } from 'view-engine/scrollable'; import { AutoTypeHintView } from 'views/auto-type-hint-view'; import { IconSelectView } from 'views/icon-select-view'; +import template from 'templates/grp.hbs'; -const GrpView = Backbone.View.extend({ - template: require('templates/grp.hbs'), +class GrpView extends View { + parent = '.app__panel'; - events: { + template = template; + + events = { 'click .grp__icon': 'showIconsSelect', 'click .grp__buttons-trash': 'moveToTrash', 'click .back-button': 'returnToApp', @@ -16,28 +20,21 @@ const GrpView = Backbone.View.extend({ 'input #grp__field-auto-type-seq': 'changeAutoTypeSeq', 'change #grp__check-search': 'setEnableSearching', 'change #grp__check-auto-type': 'setEnableAutoType' - }, - - initialize() { - this.views = {}; - }, + }; render() { this.removeSubView(); - this.renderTemplate( - { - title: this.model.get('title'), - icon: this.model.get('icon') || 'folder', - customIcon: this.model.get('customIcon'), - enableSearching: this.model.getEffectiveEnableSearching(), - readonly: this.model.get('top'), - canAutoType: AutoType.enabled, - autoTypeSeq: this.model.get('autoTypeSeq'), - autoTypeEnabled: this.model.getEffectiveEnableAutoType(), - defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq() - }, - true - ); + super.render({ + title: this.model.get('title'), + icon: this.model.get('icon') || 'folder', + customIcon: this.model.get('customIcon'), + enableSearching: this.model.getEffectiveEnableSearching(), + readonly: this.model.get('top'), + canAutoType: AutoType.enabled, + autoTypeSeq: this.model.get('autoTypeSeq'), + autoTypeEnabled: this.model.getEffectiveEnableAutoType(), + defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq() + }); if (!this.model.get('title')) { this.$el.find('#grp__field-title').focus(); } @@ -47,15 +44,14 @@ const GrpView = Backbone.View.extend({ bar: this.$el.find('.scroller__bar')[0] }); this.pageResized(); - return this; - }, + } removeSubView() { if (this.views.sub) { this.views.sub.remove(); delete this.views.sub; } - }, + } changeTitle(e) { const title = $.trim(e.target.value); @@ -69,7 +65,7 @@ const GrpView = Backbone.View.extend({ Backbone.trigger('edit-group'); } } - }, + } changeAutoTypeSeq(e) { const el = e.target; @@ -80,7 +76,7 @@ const GrpView = Backbone.View.extend({ this.model.setAutoTypeSeq(seq); } }); - }, + } focusAutoTypeSeq(e) { if (!this.views.hint) { @@ -89,7 +85,7 @@ const GrpView = Backbone.View.extend({ delete this.views.hint; }); } - }, + } showIconsSelect() { if (this.views.sub) { @@ -107,7 +103,7 @@ const GrpView = Backbone.View.extend({ this.views.sub = subView; } this.pageResized(); - }, + } iconSelected(sel) { if (sel.custom) { @@ -118,28 +114,28 @@ const GrpView = Backbone.View.extend({ this.model.setIcon(+sel.id); } this.render(); - }, + } moveToTrash() { this.model.moveToTrash(); Backbone.trigger('select-all'); - }, + } setEnableSearching(e) { const enabled = e.target.checked; this.model.setEnableSearching(enabled); - }, + } setEnableAutoType(e) { const enabled = e.target.checked; this.model.setEnableAutoType(enabled); - }, + } returnToApp() { Backbone.trigger('edit-group'); } -}); +} -_.extend(GrpView.prototype, Scrollable); +Object.assign(GrpView.prototype, Scrollable); export { GrpView }; diff --git a/app/scripts/views/tag-view.js b/app/scripts/views/tag-view.js index a0fc2248..bd583122 100644 --- a/app/scripts/views/tag-view.js +++ b/app/scripts/views/tag-view.js @@ -1,40 +1,36 @@ import Backbone from 'backbone'; +import { View } from 'view-engine/view'; import { Alerts } from 'comp/ui/alerts'; import { Locale } from 'util/locale'; +import template from 'templates/tag.hbs'; -const TagView = Backbone.View.extend({ - template: require('templates/tag.hbs'), +class TagView extends View { + parent = '.app__panel'; - events: { + template = template; + + events = { 'click .tag__buttons-trash': 'moveToTrash', 'click .back-button': 'returnToApp', 'click .tag__btn-rename': 'renameTag' - }, - - initialize() { - this.appModel = this.model; - }, + }; render() { - if (this.model) { - this.renderTemplate( - { - title: this.model.get('title') - }, - true - ); + if (this.tag) { + super.render({ + title: this.tag.get('title') + }); } - return this; - }, + } showTag(tag) { - this.model = tag; + this.tag = tag; this.render(); - }, + } renameTag() { const title = $.trim(this.$el.find('#tag__field-title').val()); - if (!title || title === this.model.get('title')) { + if (!title || title === this.tag.get('title')) { return; } if (/[;,:]/.test(title)) { @@ -44,13 +40,13 @@ const TagView = Backbone.View.extend({ }); return; } - if (this.appModel.tags.some(t => t.toLowerCase() === title.toLowerCase())) { + if (this.model.tags.some(t => t.toLowerCase() === title.toLowerCase())) { Alerts.error({ header: Locale.tagExists, body: Locale.tagExistsBody }); return; } - this.appModel.renameTag(this.model.get('title'), title); + this.model.renameTag(this.tag.get('title'), title); Backbone.trigger('select-all'); - }, + } moveToTrash() { this.title = null; @@ -58,15 +54,15 @@ const TagView = Backbone.View.extend({ header: Locale.tagTrashQuestion, body: Locale.tagTrashQuestionBody, success: () => { - this.appModel.renameTag(this.model.get('title'), undefined); + this.model.renameTag(this.tag.get('title'), undefined); Backbone.trigger('select-all'); } }); - }, + } returnToApp() { Backbone.trigger('edit-tag'); } -}); +} export { TagView }; diff --git a/package-lock.json b/package-lock.json index e73c8710..889c6634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8189,6 +8189,11 @@ } } }, + "morphdom": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.5.6.tgz", + "integrity": "sha512-uw+fgVRCV7DK9EWJ87NeiFXTDdLklajJQNLHCAJStqTY/uwFpK5ormeU2PYSX5DDk+cI9dtFli/MHKd2wP/KGg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index 95fda725..e7d7e5d9 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "load-grunt-tasks": "5.1.0", "marked": "^0.7.0", "mini-css-extract-plugin": "^0.8.0", + "morphdom": "^2.5.6", "node-sass": "^4.12.0", "node-stream-zip": "1.8.2", "normalize.css": "8.0.1", diff --git a/webpack.config.js b/webpack.config.js index 34cffc7b..db5a73d7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,6 +25,7 @@ function config(grunt, mode = 'production') { 'jquery', 'underscore', 'backbone', + 'morphdom', 'kdbxweb', 'baron', 'pikaday', @@ -61,6 +62,7 @@ function config(grunt, mode = 'production') { underscore: `underscore/underscore${devMode ? '-min' : ''}.js`, _: `underscore/underscore${devMode ? '-min' : ''}.js`, jquery: `jquery/dist/jquery${devMode ? '.min' : ''}.js`, + morphdom: `morphdom/dist/morphdom-umd${devMode ? '.min' : ''}.js`, kdbxweb: 'kdbxweb/dist/kdbxweb.js', baron: `baron/baron${devMode ? '.min' : ''}.js`, qrcode: `jsqrcode/dist/qrcode${devMode ? '.min' : ''}.js`,