web-apps/apps/common/main/lib/component/DataView.js
2021-09-23 00:35:35 +03:00

1540 lines
64 KiB
JavaScript

/*
*
* (c) Copyright Ascensio System SIA 2010-2019
*
* 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 20A-12 Ernesta Birznieka-Upisha
* street, Riga, Latvia, EU, LV-1050.
*
* 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) 2018 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(['<li id="<%= id %>"><a href="#"><%= value %></a></li>'].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: '',
inline: false,
headername: undefined
}
}
});
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,
disabled: false
}
}
});
Common.UI.DataViewStore = Backbone.Collection.extend({
model: Common.UI.DataViewModel
});
Common.UI.DataViewItem = Common.UI.BaseView.extend({
options : {
},
template: _.template([
'<div id="<%= id %>"><%= value %></div>'
].join('')),
initialize : function(options) {
Common.UI.BaseView.prototype.initialize.call(this, options);
var me = this;
me.template = me.options.template || me.template;
me.dataHint = me.options.dataHint || '';
me.dataHintDirection = me.options.dataHintDirection || '';
me.dataHintOffset = me.options.dataHintOffset || '';
me.listenTo(me.model, 'change', this.model.get('skipRenderOnChange') ? me.onChange : 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 || $(this.el);
el.html(this.template(this.model.toJSON()));
el.addClass('item');
el.toggleClass('selected', this.model.get('selected') && this.model.get('allowSelected'));
if (this.dataHint !== '') {
el.attr('data-hint', this.dataHint);
el.attr('data-hint-direction', this.dataHintDirection);
el.attr('data-hint-offset', this.dataHintOffset);
}
if (!_.isUndefined(this.model.get('contentTarget')))
el.attr('content-target', this.model.get('contentTarget'));
el.off('click dblclick contextmenu');
el.on({ 'click': _.bind(this.onClick, this),
'dblclick': _.bind(this.onDblClick, this),
'contextmenu': _.bind(this.onContextMenu, this) });
el.toggleClass('disabled', !!this.model.get('disabled'));
if (!_.isUndefined(this.model.get('cls')))
el.addClass(this.model.get('cls'));
var tip = el.data('bs.tooltip');
if (tip) {
if (tip.dontShow===undefined && el.is(':hover'))
tip.dontShow = true;
}
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) {
if (this.model.get('disabled')) return false;
this.trigger('click', this, this.model, e);
},
onDblClick: function(e) {
if (this.model.get('disabled')) return false;
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);
},
onChange: function () {
if (_.isUndefined(this.model.id))
return this;
var el = this.$el || $(this.el);
el.toggleClass('selected', this.model.get('selected') && this.model.get('allowSelected'));
el.toggleClass('disabled', !!this.model.get('disabled'));
this.trigger('change', this, this.model);
return this;
}
});
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,
scrollAlwaysVisible: false,
showLast: true,
useBSKeydown: false,
cls: ''
},
template: _.template([
'<div class="dataview inner <%= cls %>" style="<%= style %>">',
'<% _.each(groups, function(group) { %>',
'<% if (group.headername !== undefined) { %>',
'<div class="header-name"><%= group.headername %></div>',
'<% } %>',
'<div class="grouped-data <% if (group.inline) { %> inline <% } %> <% if (!_.isEmpty(group.caption)) { %> margin <% } %>" id="<%= group.id %>">',
'<% if (!_.isEmpty(group.caption)) { %>',
'<div class="group-description">',
'<span><%= group.caption %></span>',
'</div>',
'<% } %>',
'<div class="group-items-container">',
'</div>',
'</div>',
'<% }); %>',
'</div>'
].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.itemDataHint = me.options.itemDataHint || '';
me.itemDataHintDirection = me.options.itemDataHintDirection || '';
me.itemDataHintOffset = me.options.itemDataHintOffset || '';
me.multiSelect = me.options.multiSelect;
me.handleSelect = me.options.handleSelect;
me.parentMenu = me.options.parentMenu;
me.outerMenu = me.options.outerMenu;
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.cls = me.options.cls || '';
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.scrollAlwaysVisible = me.options.scrollAlwaysVisible || false;
me.tabindex = me.options.tabindex || 0;
me.delayRenderTips = me.options.delayRenderTips || false;
if (me.parentMenu)
me.parentMenu.options.restoreHeight = (me.options.restoreHeight>0);
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);
if (parentEl) {
this.setElement(parentEl, false);
this.cmpEl = $(this.template({
groups: me.groups ? me.groups.toJSON() : null,
style: me.style,
cls: me.cls
}));
parentEl.html(this.cmpEl);
} else {
this.cmpEl = me.$el || $(this.el);
this.cmpEl.html(this.template({
groups: me.groups ? me.groups.toJSON() : null,
style: me.style,
cls: me.cls
}));
}
var modalParents = this.cmpEl.closest('.asc-window');
if (modalParents.length < 1)
modalParents = this.cmpEl.closest('[id^="menu-container-"]'); // context menu
if (modalParents.length > 0) {
this.tipZIndex = parseInt(modalParents.css('z-index')) + 10;
}
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, e) {
if (e && (menu.el !== e.target)) return;
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').addBack().filter('.inner'),
useKeyboard: this.enableKeyEvents && !this.handleSelect,
minScrollbarLength : 40,
wheelSpeed: 10,
alwaysVisibleY: this.scrollAlwaysVisible
});
}
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();
return record;
},
selectByIndex: function(index, suspendEvents) {
if (this.store.length > 0 && index > -1 && index < this.store.length) {
return 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() {
return (this.multiSelect) ? this.store.where({selected: true}) : this.store.findWhere({selected: true});
},
onAddItem: function(record, store, opts) {
var view = new Common.UI.DataViewItem({
template: this.itemTemplate,
model: record,
dataHint: this.itemDataHint,
dataHintDirection: this.itemDataHintDirection,
dataHintOffset: this.itemDataHintOffset
});
if (view) {
var innerEl = $(this.el).find('.inner').addBack().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);
(this.dataViewItems.length<1) && innerEl.find('.empty-text').remove();
var idx = _.indexOf(this.store.models, record);
this.dataViewItems = this.dataViewItems.slice(0, idx).concat(view).concat(this.dataViewItems.slice(idx));
var me = this,
view_el = $(view.el),
tip = record.get('tip');
if (tip) {
if (this.delayRenderTips)
view_el.one('mouseenter', function(){ // hide tooltip when mouse is over menu
view_el.attr('data-toggle', 'tooltip');
view_el.tooltip({
title : tip,
placement : 'cursor',
zIndex : me.tipZIndex
});
view_el.mouseenter();
});
else {
view_el.attr('data-toggle', 'tooltip');
view_el.tooltip({
title : tip,
placement : 'cursor',
zIndex : me.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) {
if (tip.dontShow===undefined)
tip.dontShow = true;
(tip.tip()).remove();
}
}, this);
$(this.el).html(this.template({
groups: this.groups ? this.groups.toJSON() : null,
style: this.style,
cls: this.cls
}));
if (!_.isUndefined(this.scroller)) {
this.scroller.destroy();
delete this.scroller;
}
if (this.store.length < 1 && this.emptyText.length > 0)
$(this.el).find('.inner').addBack().filter('.inner').append('<table cellpadding="10" class="empty-text"><tr><td>' + this.emptyText + '</td></tr></table>');
_.each(this.dataViewItems, function(item) {
this.stopListening(item);
item.stopListening(item.model);
}, this);
this.dataViewItems = [];
var me = this;
this.store.each(function(item){
me.onAddItem(item, me.store);
}, this);
if (this.allowScrollbar) {
this.scroller = new Common.UI.Scroller({
el: $(this.el).find('.inner').addBack().filter('.inner'),
useKeyboard: this.enableKeyEvents && !this.handleSelect,
minScrollbarLength : 40,
wheelSpeed: 10,
alwaysVisibleY: this.scrollAlwaysVisible
});
}
if (this.disabled)
this.setDisabled(this.disabled);
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) {
var tip = view.$el.data('bs.tooltip');
if (tip) {
if (tip.dontShow===undefined)
tip.dontShow = true;
(tip.tip()).remove();
}
this.stopListening(view);
view.stopListening();
if (this.store.length < 1 && this.emptyText.length > 0) {
var el = $(this.el).find('.inner').addBack().filter('.inner');
if ( el.find('.empty-text').length<=0 )
el.append('<table cellpadding="10" class="empty-text"><tr><td>' + this.emptyText + '</td></tr></table>');
}
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
if (this.showLast) 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, force) {
if (!record) return;
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,
div_first = $(this.dataViewItems[0].el),
div_first_top = (div_first.length>0) ? div_first[0].clientTop : 0;
if (force || div_top < inner_top + div_first_top || div_top+div.outerHeight()*0.9 > inner_top + div_first_top + innerEl.height()) {
if (this.scroller && this.allowScrollbar) {
this.scroller.scrollTop(innerEl.scrollTop() + div_top - inner_top - div_first_top, 0);
} else {
innerEl.scrollTop(innerEl.scrollTop() + div_top - inner_top - div_first_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();
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) {
if (topIdx==0 && this.outerMenu && this.outerMenu.menu) {
this.deselectAll(true);
this.outerMenu.menu.focusOuter && this.outerMenu.menu.focusOuter(data, this.outerMenu.index);
return;
} else
while (idx===undefined) {
topIdx--;
if (topIdx<0) topIdx = this._layoutParams.rows-1;
idx = this._layoutParams.itemsIndexes[topIdx][leftIdx];
}
} else {
if (topIdx==this._layoutParams.rows-1 && this.outerMenu && this.outerMenu.menu) {
this.deselectAll(true);
this.outerMenu.menu.focusOuter && this.outerMenu.menu.focusOuter(data, this.outerMenu.index);
return;
} 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.scrollToRecord(rec);
this._fromKeyDown = false;
}
}
} else {
this.trigger('item:keydown', this, rec, e);
}
},
attachKeyEvents: function() {
if (this.enableKeyEvents && this.handleSelect) {
var el = $(this.el).find('.inner').addBack().filter('.inner');
el.addClass('canfocused');
el.attr('tabindex', this.tabindex.toString());
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 {
this.scrollToRecord(this.getSelectedRec());
}
},
setDisabled: function(disabled) {
this.disabled = disabled;
$(this.el).find('.inner').addBack().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]'),
docH = Common.Utils.innerHeight()-10,
innerEl = $(this.el).find('.inner').addBack().filter('.inner'),
parent = innerEl.parent(),
margins = parseInt(parent.css('margin-top')) + parseInt(parent.css('margin-bottom')) + parseInt(menuRoot.css('margin-top')),
paddings = parseInt(menuRoot.css('padding-top')) + parseInt(menuRoot.css('padding-bottom')),
menuH = menuRoot.outerHeight(),
top = parseInt(menuRoot.css('top')),
props = {minScrollbarLength : 40};
this.scrollAlwaysVisible && (props.alwaysVisibleY = this.scrollAlwaysVisible);
if (top + menuH > docH ) {
innerEl.css('max-height', (docH - top - paddings - margins) + 'px');
if (this.allowScrollbar) this.scroller.update(props);
} else if ( top + menuH < docH && innerEl.height() < this.options.restoreHeight ) {
innerEl.css('max-height', (Math.min(docH - top - paddings - margins, this.options.restoreHeight)) + 'px');
if (this.allowScrollbar) this.scroller.update(props);
}
},
fillIndexesArray: function() {
if (this.dataViewItems.length<=0) return;
this._layoutParams = {
itemsIndexes: [],
columns: 0,
rows: 0
};
var el = $(this.dataViewItems[0].el),
itemW = el.outerWidth() + parseInt(el.css('margin-left')) + parseInt(el.css('margin-right')),
offsetLeft = this.$el.offset().left,
offsetTop = el.offset().top,
prevtop = -1, topIdx = 0, leftIdx = 0;
for (var i=0; i<this.dataViewItems.length; i++) {
var top = $(this.dataViewItems[i].el).offset().top - offsetTop;
leftIdx = Math.floor(($(this.dataViewItems[i].el).offset().left - offsetLeft)/itemW);
if (top>prevtop) {
prevtop = top;
this._layoutParams.itemsIndexes.push([]);
topIdx = this._layoutParams.itemsIndexes.length-1;
}
this._layoutParams.itemsIndexes[topIdx][leftIdx] = i;
this.dataViewItems[i].topIdx = topIdx;
this.dataViewItems[i].leftIdx = leftIdx;
if (this._layoutParams.columns<leftIdx) this._layoutParams.columns = leftIdx;
}
this._layoutParams.rows = this._layoutParams.itemsIndexes.length;
this._layoutParams.columns++;
},
onResize: function() {
this._layoutParams = undefined;
},
focus: function(index) {
$(this.el).find('.inner').addBack().filter('.inner').focus();
var rec;
if (typeof index == 'string') {
if (index == 'first') {
rec = this.selectByIndex(0, true);
} else if (index == 'last') {
if (this._layoutParams === undefined)
this.fillIndexesArray();
rec = this.selectByIndex(this._layoutParams.itemsIndexes[this._layoutParams.rows-1][0], true);
}
} else if (index !== undefined)
rec = this.selectByIndex(index, true);
this.scrollToRecord(rec);
},
focusInner: function(e) {
this.focus(e.keyCode == Common.UI.Keys.DOWN ? 'first' : 'last');
}
});
Common.UI.DataViewSimple = Common.UI.BaseView.extend({
options : {
handleSelect: true,
enableKeyEvents: true,
keyMoveDirection: 'both', // 'vertical', 'horizontal'
restoreHeight: 0,
scrollAlwaysVisible: false,
useBSKeydown: false
},
template: _.template([
'<div class="dataview inner" style="<%= style %>">',
'<% _.each(items, function(item) { %>',
'<% if (!item.id) item.id = Common.UI.getId(); %>',
'<div class="item" <% if(!!item.tip) { %> data-toggle="tooltip" <% } %> data-hint="<%= item.dataHint %>" data-hint-direction="<%= item.dataHintDirection %>" data-hint-offset="<%= item.dataHintOffset %>"><%= itemTemplate(item) %></div>',
'<% }) %>',
'</div>'
].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.itemTemplate = me.options.itemTemplate || null;
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.style = me.options.style || '';
me.scrollAlwaysVisible = me.options.scrollAlwaysVisible || false;
me.tabindex = me.options.tabindex || 0;
if (me.parentMenu)
me.parentMenu.options.restoreHeight = (me.options.restoreHeight>0);
me.rendered = false;
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);
if (parentEl) {
this.setElement(parentEl, false);
this.cmpEl = $(this.template({
items: me.store.toJSON(),
itemTemplate: me.itemTemplate,
style: me.style
}));
parentEl.html(this.cmpEl);
} else {
this.cmpEl = me.$el || $(this.el);
this.cmpEl.html(this.template({
items: me.store.toJSON(),
itemTemplate: me.itemTemplate,
style: me.style,
options: me.options
}));
}
var modalParents = this.cmpEl.closest('.asc-window');
if (modalParents.length < 1)
modalParents = this.cmpEl.closest('[id^="menu-container-"]'); // context menu
if (modalParents.length > 0) {
this.tipZIndex = parseInt(modalParents.css('z-index')) + 10;
}
if (!this.rendered) {
if (this.parentMenu) {
this.cmpEl.closest('li').css('height', '100%');
this.cmpEl.css('height', '100%');
this.parentMenu.on('show:after', _.bind(this.alignPosition, this));
this.parentMenu.on('show:after', _.bind(this.onAfterShowMenu, this));
} else if (this.store.length>0)
this.onAfterShowMenu();
if (this.enableKeyEvents && this.parentMenu && this.handleSelect) {
this.parentMenu.on('show:before', function(menu) { me.deselectAll(); });
this.parentMenu.on('show:after', function(menu) {
Common.NotificationCenter.trigger('dataview:focus');
_.delay(function() {
menu.cmpEl.find('.dataview').focus();
}, 10);
}).on('hide:after', function() {
Common.NotificationCenter.trigger('dataview:blur');
});
}
this.attachKeyEvents();
this.cmpEl.on( "click", "div.item", _.bind(me.onClickItem, me));
}
if (_.isUndefined(this.scroller)) {
this.scroller = new Common.UI.Scroller({
el: $(this.el).find('.inner').addBack().filter('.inner'),
useKeyboard: this.enableKeyEvents && !this.handleSelect,
minScrollbarLength : 40,
wheelSpeed: 10,
alwaysVisibleY: this.scrollAlwaysVisible
});
}
this.rendered = true;
this.cmpEl.on('click', function(e){
if (/dataview/.test(e.target.className)) return false;
});
this.trigger('render:after', this);
return this;
},
selectRecord: function(record, suspendEvents) {
if (!this.handleSelect)
return;
if (suspendEvents)
this.suspendEvents();
this.deselectAll(suspendEvents);
if (record) {
record.set({selected: true});
var idx = _.indexOf(this.store.models, record);
if (idx>=0 && this.dataViewItems && this.dataViewItems.length>idx) {
this.dataViewItems[idx].el.addClass('selected');
}
}
if (suspendEvents)
this.resumeEvents();
return record;
},
selectByIndex: function(index, suspendEvents) {
if (this.store.length > 0 && index > -1 && index < this.store.length) {
return 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});
});
this.cmpEl.find('.item.selected').removeClass('selected');
if (suspendEvents)
this.resumeEvents();
},
getSelectedRec: function() {
return this.store.findWhere({selected: true});
},
onResetItems: function() {
this.dataViewItems && _.each(this.dataViewItems, function(item) {
var tip = item.el.data('bs.tooltip');
if (tip) {
if (tip.dontShow===undefined)
tip.dontShow = true;
(tip.tip()).remove();
}
}, this);
this.dataViewItems = null;
var template = _.template([
'<% _.each(items, function(item) { %>',
'<% if (!item.id) item.id = Common.UI.getId(); %>',
'<div class="item" <% if(!!item.tip) { %> data-toggle="tooltip" <% } %> data-hint="<%= item.dataHint %>" data-hint-direction="<%= item.dataHintDirection %>" data-hint-offset="<%= item.dataHintOffset %>"><%= itemTemplate(item) %></div>',
'<% }) %>'
].join(''));
this.cmpEl && this.cmpEl.find('.inner').html(template({
items: this.store.toJSON(),
itemTemplate: this.itemTemplate,
style : this.style
}));
if (!_.isUndefined(this.scroller)) {
this.scroller.destroy();
delete this.scroller;
}
this.scroller = new Common.UI.Scroller({
el: $(this.el).find('.inner').addBack().filter('.inner'),
useKeyboard: this.enableKeyEvents && !this.handleSelect,
minScrollbarLength : 40,
wheelSpeed: 10,
alwaysVisibleY: this.scrollAlwaysVisible
});
if (!this.parentMenu && this.store.length>0)
this.onAfterShowMenu();
this._layoutParams = undefined;
},
setStore: function(store) {
if (store) {
this.store = store;
this.onResetItems();
}
},
onClickItem: function(e) {
if ( this.disabled ) return;
window._event = e; // for FireFox only
var index = $(e.currentTarget).closest('div.item').index(),
record = (index>=0) ? this.store.at(index) : null,
view = (index>=0) ? this.dataViewItems[index] : null;
if (!record || !view) return;
record.set({selected: true});
var tip = view.el.data('bs.tooltip');
if (tip) (tip.tip()).remove();
if (!this.isSuspendEvents) {
this.trigger('item:click', this, view.el, record, e);
}
},
onAfterShowMenu: function(e) {
if (!this.dataViewItems) {
var me = this;
this.dataViewItems = [];
_.each(this.cmpEl.find('div.item'), function(item, index) {
var $item = $(item),
rec = me.store.at(index);
me.dataViewItems.push({el: $item});
if (rec.get('tip')) {
$item.tooltip({
title : rec.get('tip'),
placement : 'cursor',
zIndex : me.tipZIndex
});
}
});
}
},
scrollToRecord: function (record) {
if (!record) return;
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,
div_first = this.dataViewItems[0].el,
div_first_top = (div_first.length>0) ? div_first[0].offsetTop : 0;
if (div_top < inner_top + div_first_top || div_top+div.outerHeight() > inner_top + innerEl.height()) {
if (this.scroller) {
this.scroller.scrollTop(innerEl.scrollTop() + div_top - inner_top - div_first_top, 0);
} else {
innerEl.scrollTop(innerEl.scrollTop() + div_top - inner_top - div_first_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();
if (data.keyCode==Common.UI.Keys.RETURN) {
if (this.selectedBeforeHideRec) // only for ComboDataView menuPicker
rec = this.selectedBeforeHideRec;
if (this.canAddRecents) // only for DaraViewShape
this.addRecentItem(rec);
this.trigger('item:click', this, 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) {
if (topIdx==0 && this.outerMenu && this.outerMenu.menu) {
this.deselectAll(true);
this.outerMenu.menu.focusOuter && this.outerMenu.menu.focusOuter(data, this.outerMenu.index);
return;
} else
while (idx===undefined) {
topIdx--;
if (topIdx<0) topIdx = this._layoutParams.rows-1;
idx = this._layoutParams.itemsIndexes[topIdx][leftIdx];
}
} else {
if (topIdx==this._layoutParams.rows-1 && this.outerMenu && this.outerMenu.menu) {
this.deselectAll(true);
this.outerMenu.menu.focusOuter && this.outerMenu.menu.focusOuter(data, this.outerMenu.index);
return;
} 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.scrollToRecord(rec);
this._fromKeyDown = false;
}
}
} else {
this.trigger('item:keydown', this, rec, e);
}
},
attachKeyEvents: function() {
if (this.enableKeyEvents && this.handleSelect) {
var el = $(this.el).find('.inner').addBack().filter('.inner');
el.addClass('canfocused');
el.attr('tabindex', this.tabindex.toString());
el.on((this.parentMenu && this.useBSKeydown) ? 'dataview:keydown' : 'keydown', _.bind(this.onKeyDown, this));
}
},
setDisabled: function(disabled) {
this.disabled = disabled;
$(this.el).find('.inner').addBack().filter('.inner').toggleClass('disabled', disabled);
},
isDisabled: function() {
return this.disabled;
},
alignPosition: function() {
var menuRoot = (this.parentMenu.cmpEl.attr('role') === 'menu')
? this.parentMenu.cmpEl
: this.parentMenu.cmpEl.find('[role=menu]'),
docH = Common.Utils.innerHeight()-10,
innerEl = $(this.el).find('.inner').addBack().filter('.inner'),
parent = innerEl.parent(),
margins = parseInt(parent.css('margin-top')) + parseInt(parent.css('margin-bottom')) + parseInt(menuRoot.css('margin-top')),
paddings = parseInt(menuRoot.css('padding-top')) + parseInt(menuRoot.css('padding-bottom')),
menuH = menuRoot.outerHeight(),
top = parseInt(menuRoot.css('top')),
props = {minScrollbarLength : 40};
this.scrollAlwaysVisible && (props.alwaysVisibleY = this.scrollAlwaysVisible);
if (top + menuH > docH ) {
innerEl.css('max-height', (docH - top - paddings - margins) + 'px');
this.scroller.update(props);
} else if ( top + menuH < docH && innerEl.height() < this.options.restoreHeight ) {
innerEl.css('max-height', (Math.min(docH - top - paddings - margins, this.options.restoreHeight)) + 'px');
this.scroller.update(props);
}
},
fillIndexesArray: function() {
if (this.dataViewItems.length<=0) return;
this._layoutParams = {
itemsIndexes: [],
columns: 0,
rows: 0
};
var el = this.dataViewItems[0].el,
itemW = el.outerWidth() + parseInt(el.css('margin-left')) + parseInt(el.css('margin-right')),
offsetLeft = this.$el.offset().left,
offsetTop = el.offset().top,
prevtop = -1, topIdx = 0, leftIdx = 0;
for (var i=0; i<this.dataViewItems.length; i++) {
var item = this.dataViewItems[i];
var top = item.el.offset().top - offsetTop;
leftIdx = Math.floor((item.el.offset().left - offsetLeft)/itemW);
if (top>prevtop) {
prevtop = top;
this._layoutParams.itemsIndexes.push([]);
topIdx = this._layoutParams.itemsIndexes.length-1;
}
this._layoutParams.itemsIndexes[topIdx][leftIdx] = i;
item.topIdx = topIdx;
item.leftIdx = leftIdx;
if (this._layoutParams.columns<leftIdx) this._layoutParams.columns = leftIdx;
}
this._layoutParams.rows = this._layoutParams.itemsIndexes.length;
this._layoutParams.columns++;
},
onResize: function() {
this._layoutParams = undefined;
},
focus: function() {
this.cmpEl && this.cmpEl.find('.dataview').focus();
}
});
$(document).on('keydown.dataview', '[data-toggle=dropdown], [role=menu]', function(e) {
if (e.keyCode !== Common.UI.Keys.UP && e.keyCode !== Common.UI.Keys.DOWN && e.keyCode !== Common.UI.Keys.LEFT && e.keyCode !== Common.UI.Keys.RIGHT && e.keyCode !== Common.UI.Keys.RETURN) return;
_.defer(function(){
var target = $(e.target).closest('.dropdown-toggle');
if (target.length)
target.parent().find('.inner.canfocused').trigger('dataview:keydown', e);
else {
$(e.target).closest('.dropdown-submenu').find('.inner.canfocused').trigger('dataview:keydown', e);
}
}, 100);
});
Common.UI.DataViewShape = Common.UI.DataViewSimple.extend(_.extend({
template: _.template([
'<div class="dataview inner" style="<%= style %>">',
'<% _.each(options.groups, function(group, index) { %>',
'<div class="grouped-data <% if (index === 0) { %> recent-group <% } %> " id="<%= group.id %>" <% if (!options.recentShapes && index === 0) { %> style="display: none;" <% } %>>',
'<% if (!_.isEmpty(group.groupName)) { %>',
'<div class="group-description">',
'<span><%= group.groupName %></span>',
'</div>',
'<% } %>',
'<div class="group-items-container <% if (index === 0) { %> recent-items <% } %>">',
'<% _.each(group.groupStore.toJSON(), function(item) { %>',
'<% if (!item.id) item.id = Common.UI.getId(); %>',
'<div class="item" <% if(!!item.tip) { %> data-toggle="tooltip" <% } %> ><%= itemTemplate(item) %></div>',
'<% }); %>',
'</div>',
'</div>',
'<% }); %>',
'</div>'
].join('')),
initialize : function(options) {
var me = this;
this.canAddRecents = true;
var filter = Common.localStorage.getKeysFilter();
this.appPrefix = (filter && filter.length) ? filter.split(',')[0] : '';
me.groups = options.groups;
if (options.isFromImage) {
var store = me.groups[0].groupStore.clone();
store.shift();
me.groups[0].groupStore = store;
}
// add recent shapes to store
var recentStore = new Common.UI.DataViewGroupStore,
recentArr = options.recentShapes || [],
cols = (recentArr.length) > 18 ? 7 : 6,
height = Math.ceil(recentArr.length/cols) * 35 + 3,
width = 30 * cols;
me.recentShapes = recentArr;
recentStore.add(recentArr);
me.groups.unshift({
groupName : options.textRecentlyUsed,
groupStore : recentStore,
groupWidth : width,
groupHeight : height
});
var store = new Common.UI.DataViewStore();
_.each(me.groups, function (group) {
store.add(group.groupStore.models);
});
options.store = store;
Common.UI.DataViewSimple.prototype.initialize.call(this, options);
me.parentMenu.on('show:before', function() { me.updateRecents(); });
if (me.recentShapes.length > 0 && !me.cmpEl.find('.recent-group').is(':visible')) {
me.cmpEl.find('.recent-group').show();
}
},
onAfterShowMenu: function(e) {
var me = this;
if (!me.dataViewItems) {
me.dataViewItems = [];
_.each(me.cmpEl.find('div.grouped-data'), function (group, indexGroup) {
_.each($(group).find('div.item'), function (item, index) {
var $item = $(item),
rec = me.options.groups.at(indexGroup).groupStore.at(index);
me.dataViewItems.push({el: $item, groupIndex: indexGroup, index: index});
var tip = rec.get('tip');
if (tip) {
$item.one('mouseenter', function(){ // hide tooltip when mouse is over menu
$item.attr('data-toggle', 'tooltip');
$item.tooltip({
title : tip,
placement : 'cursor',
zIndex : me.tipZIndex
});
$item.mouseenter();
});
}
});
});
}
if (me.updateDataViewItems && me.cmpEl.is(':visible')) {
// add recent item in dataViewItems
var recent = _.where(me.dataViewItems, {groupIndex: 0});
var len = recent ? recent.length : 0;
for (var i = 0; i < len; i++) {
var tip = me.dataViewItems[i].el.data('bs.tooltip');
if (tip) {
if (tip.dontShow===undefined)
tip.dontShow = true;
(tip.tip()).remove();
}
}
me.dataViewItems = me.dataViewItems.slice(len);
var recentViewItems = [];
_.each(me.cmpEl.find('.recent-group div.item'), function (item, index) {
var $item = $(item),
rec = me.recentShapes[index];
recentViewItems.push({el: $item, groupIndex: 0, index: index});
var tip = rec.tip;
if (tip) {
$item.one('mouseenter', function(){ // hide tooltip when mouse is over menu
$item.attr('data-toggle', 'tooltip');
$item.tooltip({
title: tip,
placement: 'cursor',
zIndex : me.tipZIndex
});
$item.mouseenter();
});
}
});
me.dataViewItems = recentViewItems.concat(me.dataViewItems);
me.fillIndexesArray();
if (me.recentShapes.length === 1) {
$('.recent-group').show();
}
me.updateDataViewItems = false;
}
},
onClickItem: function(e) {
if ( this.disabled ) return;
window._event = e; // for FireFox only
var groupIndex = $(e.currentTarget).closest('div.grouped-data').index(),
itemIndex = $(e.currentTarget).closest('div.item').index();
var index = _.findIndex(this.dataViewItems, function (item) {
return (item.groupIndex === groupIndex && item.index === itemIndex);
});
var record = (index>=0) ? this.store.at(index) : null,
view = (index>=0) ? this.dataViewItems[index] : null;
if (!record || !view) return;
record.set({selected: true});
var tip = view.el.data('bs.tooltip');
if (tip) (tip.tip()).remove();
if (!this.isSuspendEvents) {
this.trigger('item:click', this, view.el, record, e);
}
this.addRecentItem(record);
},
addRecentItem: function (rec) {
var me = this,
exist = false,
type = rec.get('data').shapeType;
for (var i = 0; i < me.recentShapes.length; i++) {
if (me.recentShapes[i].data.shapeType === type) {
exist = true;
break;
}
}
if (exist) return;
var item = rec.toJSON(),
model = {
data: item.data,
tip: item.tip,
allowSelected: item.allowSelected,
selected: false
};
me.recentShapes.unshift(model);
if (me.recentShapes.length > 14) {
me.recentShapes.splice(14, 1);
}
Common.localStorage.setItem(this.appPrefix + 'recent-shapes', JSON.stringify(me.recentShapes));
me.recentShapes = undefined;
},
updateRecents: function () {
var me = this,
recents = Common.localStorage.getItem(this.appPrefix + 'recent-shapes');
recents = recents ? JSON.parse(recents) : [];
var diff = false;
if (me.recentShapes) {
for (var i = 0; i < recents.length; i++) {
if (!me.recentShapes[i] || (me.recentShapes[i] && recents[i].tip !== me.recentShapes[i].tip)) {
diff = true;
}
}
} else {
diff = true;
}
if (recents.length > 0 && diff) {
me.recentShapes = recents;
me.groups.at(0).groupStore.reset(me.recentShapes);
var store = new Common.UI.DataViewStore();
_.each(me.groups, function (group) {
store.add(group.groupStore.models);
});
me.store = store;
var template = _.template([
'<% _.each(items, function(item) { %>',
'<% if (!item.id) item.id = Common.UI.getId(); %>',
'<div class="item" <% if(!!item.tip) { %> data-toggle="tooltip" <% } %> ><%= itemTemplate(item) %></div>',
'<% }) %>'
].join(''));
me.cmpEl && me.cmpEl.find('.recent-items').html(template({
items: me.recentShapes,
itemTemplate: this.itemTemplate,
style : this.style
}));
me.updateDataViewItems = true;
}
}
}));
});