/** * Actions menu view, aka LayersPanel * * - It needs at least visualization and user models. * Globalerror to show connection or fetching errors. * * var menu = new cdb.admin.LayersPanel({ * vis: vis_model, * user: user_model, * globalError: globalError_obj * }); */ cdb.admin.LayersPanel = cdb.ui.common.TabPane.extend({ className: 'table_panel', animation_time: 300, _TEXTS: { sort: { torque: _t('Animated layers must be on top of other layers.'), tiled: _t('Tiled layers can\'t be above animated layers.') }, raster: _t("You can't add a raster layer into a visualization"), error: { default: _t('Something went wrong, try again later') } }, events: { 'click .add_layer': '_addLayerDialog' }, initialize: function() { this.layer_panels = []; // Layer panels added in the current visualization this.vis = this.options.vis; this.master_vis = this.options.master_vis; this.map = this.vis.map; this.user = this.options.user; this.globalError = this.options.globalError; // View bindings _.bindAll(this, '_sortStart', '_sortChange', '_sortBeforeStop', '_sortLayers', '_manageLayers', '_checkResize', '_moveSortTooltip'); this.map.layers.bind('add', this._newLayer, this); this.map.layers.bind('reset', this._resetLayers, this); this.map.layers.bind('add remove', this._updateLayerPositionLabel, this); $(window).bind('resize', this._checkResize); this.add_related_model(this.map.layers); this.add_related_model(this.map); this._bindSort(); // Bind sort movement // the following call is not needed, 'reset' on map.layers will be raised // after this is loaded. If the map data was not loaded using AJAX this would // need to be enabled //this._addLayers(this.map.layers); // Setting several view parameters this.model = new cdb.core.Model({ open: true, activeWorkView: 'table' }); // Bind model changes this.model.bind('change:open', this._setPanelState, this); // Visualization changes this.vis.bind('change:type', this.hide, this); this.add_related_model(this.vis); cdb.ui.common.TabPane.prototype.initialize.call(this); }, // Setup panel depending view model state _setPanelState: function() { // If it is open, add open class this.$el[ this.model.get('open') ? 'addClass' : 'removeClass' ]('opened'); }, /* Layer panel functions */ _resetLayers: function() { //clean all views this.removeTabs(); // Empty layers array this.layer_panels = []; // Add again the layers this._addLayers(this.map.layers); }, _addLayers: function(layers) { // Add 'add layer' button first this._addLayerAddButton(); // Add layers var self = this; layers.each(function(l, pos) { self._addLayer(l, pos); }); // Set default layer this._setDefaultLayer(); }, _newLayer: function(layer) { this._addLayer(layer); }, _addLayer: function(layer, pos) { var self = this; if (layer.get('type') == 'CartoDB' || layer.get('type') == 'torque') { var v = new cdb.admin.LayerPanelView({ model: layer, vis: self.vis, user: self.user, globalError: self.options.globalError }); v.bind('toggle', self._toggle, self); v.bind('switchTo', self._switchTo, self); v.bind('delete', self._openDeleteLayerDialog, self); v.bind('show', self.show, self); v.bind('destroy', self._removeLayer, self); v.bind('addColumn', self._addColumn, self); self.addTab(layer.cid, v, { after: 0 }); v.setActiveWorkView(self.model.get('activeWorkView')); self.layer_panels.push(v); self._bindLayerTooltip(v); // no pos -> new layer -> set as current if (!pos) { self._switchTo(v, true); v.setPanelStatus(); } self._checkLayers(); } }, _addColumn: function() { table.tableTab.tableView.addColumn(); }, _removeLayer: function(dataLayerCid) { var layer_view = this.getPane(dataLayerCid); var self = this; this.layer_panels = _.filter(this.layer_panels, function(view) { if (view != layer_view) return view; }); var tabBind = function(dataLayerCid) { var view = self.getPane(dataLayerCid); self.trigger('switch', view); self.vis.save('active_layer_id', view.dataLayer.id); self.show(view.panels.activeTab); self.unbind('tabEnabled', tabBind); } this.bind('tabEnabled', tabBind); this._unbindLayerTooltip(layer_view); this.removeTab(dataLayerCid); this._checkLayers(); this._manageLayers(); if (self.map.layers.getDataLayers().length === 1) { _.last(self.map.layers.getDataLayers()).set('visible', true); } }, _setDefaultLayer: function() { var layer_id = this.vis.get('active_layer_id'); var layer = _.last(this.layer_panels); // Get layer from layer_id if (layer_id) { for (var i in this.layer_panels) { var lyr = this.layer_panels[i]; if (lyr.dataLayer.id == layer_id) { layer = lyr; break; } } } // Any layer? Switch to it if (layer) { this._switchTo(layer, false); } }, // Check how many layers-view there are in the panel _checkLayers: function() { var cartodbLayers = this.map.layers.getDataLayers().length; if (cartodbLayers === 1) { this._unbindSort(); } else { this._bindSort(); } }, /* Add layer functions */ _addLayerAddButton: function() { this.$('.add_layer').remove(); var template = this.getTemplate('table/views/add_layer'); this.$el.prepend(template()); }, _addLayerDialog: function(e) { this.killEvent(e); if (!this.user.canAddLayerTo(this.map)) { var dlg = new cdb.editor.LimitsReachView({ clean_on_hide: true, enter_to_confirm: true, user: this.user }); dlg.appendToBody(); return; } if (this.vis.isVisualization()) { this._createAddLayerDialog(); } else { this.createVisDlg = new cdb.editor.CreateVisFirstView({ clean_on_hide: true, enter_to_confirm: true, model: this.master_vis, router: window.table_router, title: 'A map is required to add layers', explanation: 'A map is a shareable mix of layers, styles and queries. You can view all your maps in your dashboard.', success: this._createAddLayerDialog.bind(this) }); this.createVisDlg.appendToBody(); } }, _createAddLayerDialog: function() { var model = new cdb.editor.AddLayerModel({}, { vis: this.vis, map: this.map, user: this.user }); var dialog = new cdb.editor.AddLayerView({ model: model, user: this.user }); dialog.appendToBody(); }, _bindLayerTooltip: function(v) { var self = this; this._unbindLayerTooltip(v); v && v.$('.info').tipsy({ live: true, gravity: 'e', offset: 0, fade: true, title: function() { if (!self.vis.isVisualization() || self.model.get('open')) { return ''; } return $(this).find('span.name').text(); } }); }, _unbindLayerTooltip: function(v) { v && v.$(".info").unbind('mouseenter mouseleave'); if (v && v.$(".info").data('tipsy')) v.$(".info").data('tipsy').remove(); }, _hideLayerTooltip: function(v) { v && v.$(".info").tipsy("hide") }, //////////////////// // Sort functions // //////////////////// _unbindSort: function() { this.$el.sortable('destroy') }, _bindSort: function() { this._unbindSort(); this.$el.sortable({ axis: "y", items: "> .layer_panel", handle: ".layer-info .info", cancel: '', contaiment: "parent", opacity: 0.5, start: this._sortStart, change: this._sortChange, beforeStop: this._sortBeforeStop, update: this._sortLayers, forcePlaceholderSize: false }).disableSelection(); }, // Before start sorting action _sortStart: function(e, ui) { this.sort_actions = { original_layer_pos: $(ui.item).index(), $layer: $(ui.item) } var active_height = $(ui.item).outerHeight(); $(ui.placeholder).outerHeight(active_height); }, // When any change happens sorting // - If a torque layer tries to move to other position, sorting is cancelled // and a yellow tooltip should appear. // - If a cartodb layer tries to move to other position and there is a torque // layer in the visualization, sorting is cancelled and a yellow tooltip // should appear. _sortChange: function(e, ui) { var isAnyTorque = _.contains(this.map.layers.pluck('type'), 'torque'); var isLayerTorque = $(ui.item).attr('layer-type') == "torque"; var active_height = $(ui.item).outerHeight(); // Torque if (isAnyTorque) { // Torque layer if (isLayerTorque) { if (ui.placeholder.index() == 1) { this.cancelSort = false; this._removeSortTooltip(); } else { this.cancelSort = true; if (!this.sortTooltip) this._addSortTooltip(e, 'torque'); } this.$('section.add_layer').after( $(ui.placeholder) .outerHeight(active_height) .css('display','block') ) } else { if (ui.placeholder.index() != 1) { this.cancelSort = false; $(ui.placeholder) .outerHeight(active_height) .css('display','block'); this._removeSortTooltip(); } else { this.cancelSort = true; this.$('section.layer_panel:eq(' + this.sort_actions.original_layer_pos + ')').before( $(ui.placeholder) .outerHeight(active_height) .css('display','block') ) if (!this.sortTooltip) this._addSortTooltip(e, 'tiled'); } } } else { this.cancelSort = false; $(ui.placeholder) .outerHeight(active_height) .css('display','block'); } }, // If it is necessary to cancel de sorting _sortBeforeStop: function() { this._removeSortTooltip(); if (this.cancelSort) this.$el.sortable('cancel'); this.cancelSort = false; }, _addSortTooltip: function(e, type) { this.sortTooltip = new cdb.admin.TooltipTrails({ className: 'tooltip-trails', msg: this._TEXTS.sort[type] }); this.sort_actions.$layer.bind('mousemove', this._moveSortTooltip); this.$el.append(this.sortTooltip.render().el); this.sortTooltip.show({ left: e.pageX - this.$el.offset().left, top: e.pageY - this.$el.offset().top }); this.addView(this.sortTooltip); }, _moveSortTooltip: function(e) { // Get correct first drag coordinates var parentOffset = this.$el.offset(); var position = { left: (e.pageX - parentOffset.left), top: (e.pageY - parentOffset.top) }; this.sortTooltip.move(position); }, _removeSortTooltip: function() { this.sort_actions.$layer.unbind('mousemove', this._moveSortTooltip); if (this.sortTooltip) { this.sortTooltip.clean(); this.removeView(this.sortTooltip); delete this.sortTooltip; } }, _sortLayers: function(event, ui) { var self = this; // New index of the layer in the collection of layers, assuming there // is a base layer at position 0 var newIndex = $('.layer_panel').length - $('.layer_panel').index(ui.item); var layerId = ui.item.attr('model-id'); var layer = this.map.layers.get(layerId); // layers don't need to be reset when they are sorted // and save the new layers order to the server this.map.layers.unbind('reset', this._resetLayers, this); this.map.layers.moveLayer(layer, { to: newIndex, complete: function() { self._updateLayerPositionLabel(); self.map.layers.bind('reset', self._resetLayers, self); } }); }, _updateLayerPositionLabel: function() { _.each(this.layer_panels, function(layer) { layer.setLayerOrder(layer.model); }); }, /* Magic layers */ _manageLayers: function() { var cartodbLayers = this.map.layers.getDataLayers().length; // If there isn't any layer added if (cartodbLayers === 0) { return false; } var $active_layer = this.activePane.$el; // Move rest of panels this.$('.layer_panel').removeClass('active'); // Add active class $active_layer.addClass('active'); // Set rest of layer_panels to height 60px // Decide later about compress them // TODO: not use jquery this.$('.layer_panel').not('.layer_panel.active').css({ height: 66 }); // Get previous siblings and calculate space // TODO: not use jquery var pre_size = $active_layer.prevAll('.layer_panel').size(); var t_s = (pre_size == 0) ? 0 : (pre_size * 43); // Get next siblings and calculate space // TODO: not use jquery var next_size = $active_layer.nextAll('.layer_panel').size(); var b_s = (next_size == 0) ? 0 : (next_size * 43); // Set layers offset var offset = -15; $active_layer.css({ height: this.$el.height() - (t_s + b_s) + offset }) }, /* testing resize event */ _checkResize: function(e) { if (this.resize) clearTimeout(this.resize); var self = this; this.resize = setTimeout(self._manageLayers, 100); }, ////////////////////// // Dialog functions // ////////////////////// show: function(modName) { // Select the tab if it is not activated var mod = this.getActivePane(); mod.tabs.activate(modName); mod.panels.active(modName); // Move right panel this.movePanel(); }, // Move right panel movePanel: function() { var mod = this.getActivePane(); if (!mod) return false; this._hideDropdowns(); // Get the action type and width // of the active module // var action = { module_event: narrow or show, module_width: 450 (integer) } var active_pane = mod.panels.getActivePane(); var action = active_pane.getModuleAction(); // Show panel -> trigger cdb.god.trigger('panel_action', action.type); // Set open model attribute as true this.model.set('open', true); this.$el.animate({ width: action.width, right: 0 }, this.animation_time, function() { cdb.god.trigger("end_" + action.type); }); }, hide: function(modName) { var panel_width = this.$el.width(); // Hide the tab var mod = this.getActivePane(); this._hideDropdowns(); // Hide panel -> trigger cdb.god.trigger("panel_action", "hide"); // Set open model attribute as true this.model.set('open', false); this.$el.animate({ right: 63 - panel_width }, this.animation_time); }, _hideDropdowns: function() { cdb.god.trigger("closeDialogs"); cdb.god.trigger("closeDialogs:color"); }, // l -> layer view // open -> flag to know if it is necessary to open the panel _switchTo: function(l, open) { // If there is any change within layer tabs if (this.activePane) this.activePane.unbind('tabChanged', null, null); l.bind('tabChanged', this.movePanel, this); // Check if switched layer is active one if (this.activePane == l) { if (open !== undefined) { if (open === true) { this.show(l.panels.activeTab); } else { this.hide(); } } else { if (this.model.get('open')) { this.hide(); } else { this.show(l.panels.activeTab); } } } // if not, activate it if (this.activePane != l) { this._hideLayerTooltip(l); this.active(l.dataLayer.cid); if (l.dataLayer.id && this.vis.get('active_layer_id') != l.dataLayer.id) { this.vis.save('active_layer_id', l.dataLayer.id); } if (open === undefined) this.show(l.panels.activeTab); } // Send trigger this.trigger('switch', l); // Manage available layers this._manageLayers(); }, _toggle: function(modName) { // only hide if we click on active tab if (this.model.get('open') && modName == this.getActivePane().panels.activeTab) { this.hide(modName); } else { this.show(modName); } }, _openDeleteLayerDialog: function(layerView) { var view = new cdb.editor.DeleteLayerView({ model: layerView.model }); view.appendToBody(); }, setActiveWorkView: function(workView) { this.hide(); this.model.set('activeWorkView', workView); // Set active work view for all panes, // not only for the current one this.each(function(tab, pane){ pane.setActiveWorkView(workView); }) }, clean: function() { $(window).unbind('resize', this._checkResize); if (this.resize) clearTimeout(this.resize); this.removeTabs(); this._unbindSort(); this.elder('clean'); } });