/* * * (c) Copyright Ascensio System Limited 2010-2016 * * This program is a free software product. You can redistribute it and/or * modify it under the terms of the GNU Affero General Public License (AGPL) * version 3 as published by the Free Software Foundation. In accordance with * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect * that Ascensio System SIA expressly excludes the warranty of non-infringement * of any third-party rights. * * This program is distributed WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html * * You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, * EU, LV-1021. * * The interactive user interfaces in modified source and object code versions * of the Program must display Appropriate Legal Notices, as required under * Section 5 of the GNU AGPL version 3. * * Pursuant to Section 7(b) of the License you must retain the original Product * logo when distributing the program. Pursuant to Section 7(e) we decline to * grant you any rights under trademark law for use of our trademarks. * * All the Product's GUI elements, including illustrations and icon sets, as * well as technical writing content are licensed under the terms of the * Creative Commons Attribution-ShareAlike 4.0 International. See the License * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode * */ /** * DataView.js * * A mechanism for displaying data using custom layout templates and formatting. * * Created by Alexander Yuzhin on 1/24/14 * Copyright (c) 2014 Ascensio System SIA. All rights reserved. * */ /** * The View uses an template as its internal templating mechanism, and is bound to an * {@link Common.UI.DataViewStore} so that as the data in the store changes the view is automatically updated * to reflect the changes. * * The example below binds a View to a {@link Common.UI.DataViewStore} and renders it into an el. * * new Common.UI.DataView({ * el: $('#id'), * store: new Common.UI.DataViewStore([{value: 1, value: 2}]), * itemTemplate: _.template(['
  • <%= value %>
  • '].join('')) * }); * * * @property {Object} el * Backbone el * * * @property {Object} store * The Store class encapsulates a client side cache of Model objects. * * * @property {String} emptyText * The text to display in the view when there is no data to display. * * * @cfg {Object} itemTemplate * The inner portion of the item template to be rendered. * */ if (Common === undefined) var Common = {}; define([ 'common/main/lib/component/BaseView', 'common/main/lib/component/Scroller' ], function () { 'use strict'; Common.UI.DataViewGroupModel = Backbone.Model.extend({ defaults: function() { return { id: Common.UI.getId(), caption: '' } } }); Common.UI.DataViewGroupStore = Backbone.Collection.extend({ model: Common.UI.DataViewGroupModel }); Common.UI.DataViewModel = Backbone.Model.extend({ defaults: function() { return { id: Common.UI.getId(), selected: false, allowSelected: true, value: null } } }); Common.UI.DataViewStore = Backbone.Collection.extend({ model: Common.UI.DataViewModel }); Common.UI.DataViewItem = Common.UI.BaseView.extend({ options : { }, template: _.template([ '
    <%= value %>
    ' ].join('')), initialize : function(options) { Common.UI.BaseView.prototype.initialize.call(this, options); var me = this; me.template = me.options.template || me.template; me.listenTo(me.model, 'change', me.render); me.listenTo(me.model, 'change:selected', me.onSelectChange); me.listenTo(me.model, 'remove', me.remove); }, render: function () { if (_.isUndefined(this.model.id)) return this; var el = $(this.el); el.html(this.template(this.model.toJSON())); el.addClass('item'); el.toggleClass('selected', this.model.get('selected') && this.model.get('allowSelected')); el.off('click').on('click', _.bind(this.onClick, this)); el.off('dblclick').on('dblclick', _.bind(this.onDblClick, this)); el.off('contextmenu').on('contextmenu', _.bind(this.onContextMenu, this)); if (!_.isUndefined(this.model.get('cls'))) el.addClass(this.model.get('cls')); this.trigger('change', this, this.model); return this; }, remove: function() { this.stopListening(this.model); this.trigger('remove', this, this.model); Common.UI.BaseView.prototype.remove.call(this); }, onClick: function(e) { this.trigger('click', this, this.model, e); }, onDblClick: function(e) { this.trigger('dblclick', this, this.model, e); }, onContextMenu: function(e) { this.trigger('contextmenu', this, this.model, e); }, onSelectChange: function(model, selected) { this.trigger('select', this, model, selected); } }); Common.UI.DataView = Common.UI.BaseView.extend({ options : { multiSelect: false, handleSelect: true, enableKeyEvents: true, keyMoveDirection: 'both', // 'vertical', 'horizontal' restoreHeight: 0, emptyText: '', listenStoreEvents: true, allowScrollbar: true, showLast: true, useBSKeydown: false }, template: _.template([ '
    ', '<% _.each(groups, function(group) { %>', '
    ', '
    ', '<%= group.caption %>', '
    ', '
    ', '
    ', '
    ', '<% }); %>', '
    ' ].join('')), initialize : function(options) { Common.UI.BaseView.prototype.initialize.call(this, options); var me = this; me.template = me.options.template || me.template; me.store = me.options.store || new Common.UI.DataViewStore(); me.groups = me.options.groups || null; me.itemTemplate = me.options.itemTemplate || null; me.multiSelect = me.options.multiSelect; me.handleSelect = me.options.handleSelect; me.parentMenu = me.options.parentMenu; me.enableKeyEvents= me.options.enableKeyEvents; me.useBSKeydown = me.options.useBSKeydown; // only with enableKeyEvents && parentMenu me.showLast = me.options.showLast; me.style = me.options.style || ''; me.emptyText = me.options.emptyText || ''; me.listenStoreEvents= (me.options.listenStoreEvents!==undefined) ? me.options.listenStoreEvents : true; me.allowScrollbar = (me.options.allowScrollbar!==undefined) ? me.options.allowScrollbar : true; me.rendered = false; me.dataViewItems = []; if (me.options.keyMoveDirection=='vertical') me.moveKeys = [Common.UI.Keys.UP, Common.UI.Keys.DOWN]; else if (me.options.keyMoveDirection=='horizontal') me.moveKeys = [Common.UI.Keys.LEFT, Common.UI.Keys.RIGHT]; else me.moveKeys = [Common.UI.Keys.UP, Common.UI.Keys.DOWN, Common.UI.Keys.LEFT, Common.UI.Keys.RIGHT]; if (me.options.el) me.render(); }, render: function (parentEl) { var me = this; this.trigger('render:before', this); this.cmpEl = $(this.el); if (parentEl) { this.setElement(parentEl, false); this.cmpEl = $(this.template({ groups: me.groups ? me.groups.toJSON() : null, style: me.style })); parentEl.html(this.cmpEl); } else { this.cmpEl.html(this.template({ groups: me.groups ? me.groups.toJSON() : null, style: me.style })); } if (!this.rendered) { if (this.listenStoreEvents) { this.listenTo(this.store, 'add', this.onAddItem); this.listenTo(this.store, 'reset', this.onResetItems); } this.onResetItems(); if (this.parentMenu) { this.cmpEl.closest('li').css('height', '100%'); this.cmpEl.css('height', '100%'); this.parentMenu.on('show:after', _.bind(this.alignPosition, this)); } if (this.enableKeyEvents && this.parentMenu && this.handleSelect) { if (!me.showLast) this.parentMenu.on('show:before', function(menu) { me.deselectAll(); }); this.parentMenu.on('show:after', function(menu) { if (me.showLast) me.showLastSelected(); Common.NotificationCenter.trigger('dataview:focus'); _.delay(function() { menu.cmpEl.find('.dataview').focus(); }, 10); }).on('hide:after', function() { Common.NotificationCenter.trigger('dataview:blur'); }); } } if (_.isUndefined(this.scroller) && this.allowScrollbar) { this.scroller = new Common.UI.Scroller({ el: $(this.el).find('.inner').andSelf().filter('.inner'), useKeyboard: this.enableKeyEvents && !this.handleSelect, minScrollbarLength : 40, wheelSpeed: 10 }); } var modalParents = this.cmpEl.closest('.asc-window'); if (modalParents.length > 0) { this.tipZIndex = parseInt(modalParents.css('z-index')) + 10; } this.rendered = true; this.cmpEl.on('click', function(e){ if (/dataview/.test(e.target.className)) return false; }); this.trigger('render:after', this); return this; }, setStore: function(store) { if (store) { this.stopListening(this.store); this.store = store; if (this.listenStoreEvents) { this.listenTo(this.store, 'add', this.onAddItem); this.listenTo(this.store, 'reset', this.onResetItems); } } }, selectRecord: function(record, suspendEvents) { if (!this.handleSelect) return; if (suspendEvents) this.suspendEvents(); if (!this.multiSelect) { _.each(this.store.where({selected: true}), function(rec){ rec.set({selected: false}); }); if (record) record.set({selected: true}); } else { if (record) record.set({selected: !record.get('selected')}); } if (suspendEvents) this.resumeEvents(); }, selectByIndex: function(index, suspendEvents) { if (this.store.length > 0 && index > -1 && index < this.store.length) { this.selectRecord(this.store.at(index), suspendEvents); } }, deselectAll: function(suspendEvents) { if (suspendEvents) this.suspendEvents(); _.each(this.store.where({selected: true}), function(record){ record.set({selected: false}); }); if (suspendEvents) this.resumeEvents(); }, getSelectedRec: function() { if (this.multiSelect) { var items = []; _.each(this.store.where({selected: true}), function(rec){ items.push(rec); }); return items; } return this.store.where({selected: true}); }, onAddItem: function(record, index, opts) { var view = new Common.UI.DataViewItem({ template: this.itemTemplate, model: record }); if (view) { var innerEl = $(this.el).find('.inner').andSelf().filter('.inner'); if (this.groups && this.groups.length > 0) { var group = this.groups.findWhere({id: record.get('group')}); if (group) { innerEl = innerEl.find('#' + group.id + ' ' + '.group-items-container'); } } if (innerEl) { if (opts && opts.at == 0) innerEl.prepend(view.render().el); else innerEl.append(view.render().el); innerEl.find('.empty-text').remove(); this.dataViewItems.push(view); if (record.get('tip')) { var view_el = $(view.el); view_el.attr('data-toggle', 'tooltip'); view_el.tooltip({ title : record.get('tip'), placement : 'cursor', zIndex : this.tipZIndex }); } this.listenTo(view, 'change', this.onChangeItem); this.listenTo(view, 'remove', this.onRemoveItem); this.listenTo(view, 'click', this.onClickItem); this.listenTo(view, 'dblclick', this.onDblClickItem); this.listenTo(view, 'select', this.onSelectItem); this.listenTo(view, 'contextmenu', this.onContextMenuItem); if (!this.isSuspendEvents) this.trigger('item:add', this, view, record); } } }, onResetItems: function() { _.each(this.dataViewItems, function(item) { var tip = item.$el.data('bs.tooltip'); if (tip) (tip.tip()).remove(); }, this); $(this.el).html(this.template({ groups: this.groups ? this.groups.toJSON() : null, style: this.style })); if (!_.isUndefined(this.scroller)) { this.scroller.destroy(); delete this.scroller; } if (this.store.length < 1 && this.emptyText.length > 0) $(this.el).find('.inner').andSelf().filter('.inner').append('
    ' + this.emptyText + '
    '); _.each(this.dataViewItems, function(item) { this.stopListening(item); item.stopListening(item.model); }, this); this.dataViewItems = []; this.store.each(this.onAddItem, this); if (this.allowScrollbar) { this.scroller = new Common.UI.Scroller({ el: $(this.el).find('.inner').andSelf().filter('.inner'), useKeyboard: this.enableKeyEvents && !this.handleSelect, minScrollbarLength : 40, wheelSpeed: 10 }); } this.attachKeyEvents(); this.lastSelectedRec = null; this._layoutParams = undefined; }, onChangeItem: function(view, record) { if (!this.isSuspendEvents) { this.trigger('item:change', this, view, record); } }, onRemoveItem: function(view, record) { this.stopListening(view); view.stopListening(); if (this.store.length < 1 && this.emptyText.length > 0) { var el = $(this.el).find('.inner').andSelf().filter('.inner'); if ( el.find('.empty-text').length<=0 ) el.append('
    ' + this.emptyText + '
    '); } for (var i=0; i < this.dataViewItems.length; i++) { if (_.isEqual(view, this.dataViewItems[i]) ) { this.dataViewItems.splice(i, 1); break; } } if (!this.isSuspendEvents) { this.trigger('item:remove', this, view, record); } }, onClickItem: function(view, record, e) { if ( this.disabled ) return; window._event = e; // for FireFox only if (this.showLast) this.selectRecord(record); this.lastSelectedRec = null; var tip = view.$el.data('bs.tooltip'); if (tip) (tip.tip()).remove(); if (!this.isSuspendEvents) { this.trigger('item:click', this, view, record, e); } }, onDblClickItem: function(view, record, e) { if ( this.disabled ) return; window._event = e; // for FireFox only this.selectRecord(record); this.lastSelectedRec = null; if (!this.isSuspendEvents) { this.trigger('item:dblclick', this, view, record, e); } }, onSelectItem: function(view, record, selected) { if (!this.isSuspendEvents) { this.trigger(selected ? 'item:select' : 'item:deselect', this, view, record, this._fromKeyDown); } }, onContextMenuItem: function(view, record, e) { if (!this.isSuspendEvents) { this.trigger('item:contextmenu', this, view, record, e); } }, scrollToRecord: function (record) { var innerEl = $(this.el).find('.inner'), inner_top = innerEl.offset().top, idx = _.indexOf(this.store.models, record), div = (idx>=0 && this.dataViewItems.length>idx) ? $(this.dataViewItems[idx].el) : innerEl.find('#' + record.get('id')); if (div.length<=0) return; var div_top = div.offset().top; if (div_top < inner_top || div_top+div.outerHeight() > inner_top + innerEl.height()) { if (this.scroller && this.allowScrollbar) { this.scroller.scrollTop(innerEl.scrollTop() + div_top - inner_top, 0); } else { innerEl.scrollTop(innerEl.scrollTop() + div_top - inner_top); } } }, onKeyDown: function (e, data) { if ( this.disabled ) return; if (data===undefined) data = e; if (_.indexOf(this.moveKeys, data.keyCode)>-1 || data.keyCode==Common.UI.Keys.RETURN) { data.preventDefault(); data.stopPropagation(); var rec = this.getSelectedRec()[0]; if (this.lastSelectedRec===null) this.lastSelectedRec = rec; if (data.keyCode==Common.UI.Keys.RETURN) { this.lastSelectedRec = null; if (this.selectedBeforeHideRec) // only for ComboDataView menuPicker rec = this.selectedBeforeHideRec; this.trigger('item:click', this, this, rec, e); this.trigger('item:select', this, this, rec, e); this.trigger('entervalue', this, rec, e); if (this.parentMenu) this.parentMenu.hide(); } else { var idx = _.indexOf(this.store.models, rec); if (idx<0) { if (data.keyCode==Common.UI.Keys.LEFT) { var target = $(e.target).closest('.dropdown-submenu.over'); if (target.length>0) { target.removeClass('over'); target.find('> a').focus(); } else idx = 0; } else idx = 0; } else if (this.options.keyMoveDirection == 'both') { if (this._layoutParams === undefined) this.fillIndexesArray(); var topIdx = this.dataViewItems[idx].topIdx, leftIdx = this.dataViewItems[idx].leftIdx; idx = undefined; if (data.keyCode==Common.UI.Keys.LEFT) { while (idx===undefined) { leftIdx--; if (leftIdx<0) { var target = $(e.target).closest('.dropdown-submenu.over'); if (target.length>0) { target.removeClass('over'); target.find('> a').focus(); break; } else leftIdx = this._layoutParams.columns-1; } idx = this._layoutParams.itemsIndexes[topIdx][leftIdx]; } } else if (data.keyCode==Common.UI.Keys.RIGHT) { while (idx===undefined) { leftIdx++; if (leftIdx>this._layoutParams.columns-1) leftIdx = 0; idx = this._layoutParams.itemsIndexes[topIdx][leftIdx]; } } else if (data.keyCode==Common.UI.Keys.UP) { while (idx===undefined) { topIdx--; if (topIdx<0) topIdx = this._layoutParams.rows-1; idx = this._layoutParams.itemsIndexes[topIdx][leftIdx]; } } else { while (idx===undefined) { topIdx++; if (topIdx>this._layoutParams.rows-1) topIdx = 0; idx = this._layoutParams.itemsIndexes[topIdx][leftIdx]; } } } else { idx = (data.keyCode==Common.UI.Keys.UP || data.keyCode==Common.UI.Keys.LEFT) ? Math.max(0, idx-1) : Math.min(this.store.length - 1, idx + 1) ; } if (idx !== undefined && idx>=0) rec = this.store.at(idx); if (rec) { this._fromKeyDown = true; this.selectRecord(rec); this._fromKeyDown = false; this.scrollToRecord(rec); } } } else { this.trigger('item:keydown', this, rec, e); } }, attachKeyEvents: function() { if (this.enableKeyEvents && this.handleSelect) { var el = $(this.el).find('.inner').andSelf().filter('.inner'); el.addClass('canfocused'); el.attr('tabindex', '0'); el.on((this.parentMenu && this.useBSKeydown) ? 'dataview:keydown' : 'keydown', _.bind(this.onKeyDown, this)); } }, showLastSelected: function() { if ( this.lastSelectedRec) { this.selectRecord(this.lastSelectedRec, true); this.scrollToRecord(this.lastSelectedRec); this.lastSelectedRec = null; } else { var rec = this.getSelectedRec()[0]; if (rec) this.scrollToRecord(rec); } }, setDisabled: function(disabled) { this.disabled = disabled; $(this.el).find('.inner').andSelf().filter('.inner').toggleClass('disabled', disabled); }, isDisabled: function() { return this.disabled; }, setEmptyText: function(emptyText) { this.emptyText = emptyText; }, alignPosition: function() { var menuRoot = (this.parentMenu.cmpEl.attr('role') === 'menu') ? this.parentMenu.cmpEl : this.parentMenu.cmpEl.find('[role=menu]'), innerEl = $(this.el).find('.inner').andSelf().filter('.inner'), docH = Common.Utils.innerHeight(), menuH = menuRoot.outerHeight(), top = parseInt(menuRoot.css('top')); if (menuH > docH) { innerEl.css('max-height', (docH - parseInt(menuRoot.css('padding-top')) - parseInt(menuRoot.css('padding-bottom'))-5) + 'px'); if (this.allowScrollbar) this.scroller.update({minScrollbarLength : 40}); } else if ( innerEl.height() < this.options.restoreHeight ) { innerEl.css('max-height', (Math.min(docH - parseInt(menuRoot.css('padding-top')) - parseInt(menuRoot.css('padding-bottom'))-5, this.options.restoreHeight)) + 'px'); menuH = menuRoot.outerHeight(); if (top+menuH > docH) { menuRoot.css('top', 0); } if (this.allowScrollbar) this.scroller.update({minScrollbarLength : 40}); } }, fillIndexesArray: function() { if (this.dataViewItems.length<=0) return; var top, left, el = $(this.dataViewItems[0].el), itemW = el.outerWidth() + parseInt(el.css('margin-left')) + parseInt(el.css('margin-right')), itemH = el.outerHeight() + parseInt(el.css('margin-top')) + parseInt(el.css('margin-bottom')), offsetLeft = this.$el.offset().left, offsetTop = this.$el.offset().top, idxOffset = 0; this._layoutParams = { itemsIndexes: [], columns: 0, rows: 0 }; if (this.groups && this.groups.length > 0) { var group_desc = this.cmpEl.find('.group-description:first'); if (group_desc.length>0) offsetLeft += group_desc.width(); } for (var i=0; i this._layoutParams.itemsIndexes.length-1) { this._layoutParams.itemsIndexes.push([]); } this._layoutParams.itemsIndexes[top][left] = i; this.dataViewItems[i].topIdx = top; this.dataViewItems[i].leftIdx = left; if (this._layoutParams.columns