Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<div class="CDB-Overlay">
|
||||
<button type="button" class="CDB-Attribution-button js-button">©</button>
|
||||
<p class="CDB-Attribution-text js-text u-ellipsis"><%= attributions %></p>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var View = require('../../../core/view');
|
||||
var Model = require('../../../core/model');
|
||||
var template = require('./attribution-template.tpl');
|
||||
var Sanitize = require('../../../core/sanitize');
|
||||
|
||||
/**
|
||||
* Attribution overlay
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'CDB-Attribution',
|
||||
|
||||
events: {
|
||||
'click .js-button': '_toggleAttributions',
|
||||
'dblclick': 'killEvent'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.model = new Model({
|
||||
visible: false
|
||||
});
|
||||
this.map = this.options.map;
|
||||
|
||||
this._onDocumentKeyDown = this._onDocumentKeyDown.bind(this);
|
||||
this._toggleAttributionsBasedOnContainer = this._toggleAttributionsBasedOnContainer.bind(this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var attributions = _.compact(this.map.get('attribution')).join(', ');
|
||||
var isGMaps = this.map.get('provider') !== 'leaflet';
|
||||
this.$el.html(
|
||||
template({
|
||||
attributions: Sanitize.html(attributions)
|
||||
})
|
||||
);
|
||||
this.$el.toggleClass('CDB-Attribution--gmaps', !!isGMaps);
|
||||
this._toggleAttributionsBasedOnContainer();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:visible', function (mdl, isVisible) {
|
||||
this[ isVisible ? '_showAttributions' : '_hideAttributions' ]();
|
||||
}, this);
|
||||
this.map.bind('change:attribution', this.render, this);
|
||||
this.add_related_model(this.map);
|
||||
$(window).bind('resize', this._toggleAttributionsBasedOnContainer);
|
||||
},
|
||||
|
||||
_enableDocumentBinds: function () {
|
||||
$(document).bind('keydown', this._onDocumentKeyDown);
|
||||
},
|
||||
|
||||
_disableDocumentBinds: function () {
|
||||
$(document).unbind('keydown', this._onDocumentKeyDown);
|
||||
},
|
||||
|
||||
_onDocumentKeyDown: function (ev) {
|
||||
if (ev && ev.keyCode === 27) {
|
||||
this._toggleAttributions();
|
||||
}
|
||||
},
|
||||
|
||||
_showAttributions: function () {
|
||||
this.$el.addClass('is-active');
|
||||
this._enableDocumentBinds();
|
||||
},
|
||||
|
||||
_hideAttributions: function () {
|
||||
this.$el.removeClass('is-active');
|
||||
this._disableDocumentBinds();
|
||||
},
|
||||
|
||||
_toggleAttributions: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
_toggleAttributionsBasedOnContainer: function () {
|
||||
var MAP_CONTAINER_MOBILE_WIDTH = 650;
|
||||
if (this.map.getMapViewSize().x > MAP_CONTAINER_MOBILE_WIDTH) {
|
||||
this.model.set('visible', true);
|
||||
} else {
|
||||
this.model.set('visible', false);
|
||||
}
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._disableDocumentBinds();
|
||||
$(window).unbind('resize', this._toggleAttributionsBasedOnContainer);
|
||||
View.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="CDB-infowindow CDB-infowindow--light js-infowindow">
|
||||
<div class="CDB-infowindow-close js-close"></div>
|
||||
<div class="CDB-infowindow-container">
|
||||
<% if (typeof loading !== 'undefined' && loading) { %>
|
||||
<div class="CDB-Loader js-loader is-visible"></div>
|
||||
<% } %>
|
||||
<div class="CDB-infowindow-bg">
|
||||
<div class="CDB-infowindow-inner js-inner">
|
||||
<ul class="CDB-infowindow-list js-content">
|
||||
<% if (content.fields) { %>
|
||||
<% _.each(content.fields, function (field) { %>
|
||||
<li class="CDB-infowindow-listItem">
|
||||
<% if (field.title) { %><h5 class="CDB-infowindow-subtitle"><%- field.title %></h5><% } %>
|
||||
<% if (field.value) { %><h4 class="CDB-infowindow-title"><%- field.value %></h4><% } %>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="CDB-hook">
|
||||
<div class="CDB-hook-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,154 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var defaultInfowindowTemplate = require('./default-infowindow-template.tpl');
|
||||
|
||||
var InfowindowModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
offset: [28, 0], // offset of the tip calculated from the bottom left corner
|
||||
maxHeight: 180, // max height of the content, not the whole infowindow
|
||||
autoPan: true,
|
||||
template_type: 'mustache',
|
||||
content: '',
|
||||
alternative_names: { },
|
||||
visibility: false
|
||||
},
|
||||
|
||||
DEFAULT_TEMPLATE: defaultInfowindowTemplate,
|
||||
|
||||
TEMPLATE_ATTRIBUTES: ['template', 'template_type', 'alternative_names', 'width', 'maxHeight', 'offset'],
|
||||
|
||||
initialize: function (attrs) {
|
||||
attrs = attrs || {};
|
||||
this._fields = new Backbone.Collection(attrs.fields || []);
|
||||
this.unset('fields', { silent: true });
|
||||
|
||||
// Set a default template
|
||||
if (!this._hasTemplate()) {
|
||||
this.set({
|
||||
template: this.DEFAULT_TEMPLATE,
|
||||
template_type: 'underscore'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_hasTemplate: function () {
|
||||
return this.get('template') && this.get('template').trim && this.get('template').trim();
|
||||
},
|
||||
|
||||
updateContent: function (attributes, options) {
|
||||
options = options || {};
|
||||
options = _.pick(options, 'showEmptyFields');
|
||||
var fields = this._fields.toJSON();
|
||||
this.set('content', InfowindowModel.contentForFields(attributes, fields, options));
|
||||
},
|
||||
|
||||
setInfowindowTemplate: function (infowindowTemplateModel) {
|
||||
var attrs = _.pick(infowindowTemplateModel.toJSON(), this.TEMPLATE_ATTRIBUTES);
|
||||
// Remove keys that have a falsy value
|
||||
attrs = _.pick(attrs, _.identity);
|
||||
this.set(_.clone(attrs));
|
||||
this._fields.reset(infowindowTemplateModel.fields.toJSON());
|
||||
this._infowindowTemplateModel = infowindowTemplateModel;
|
||||
},
|
||||
|
||||
setLoading: function () {
|
||||
this.set({
|
||||
content: {
|
||||
fields: [{
|
||||
type: 'loading',
|
||||
title: 'loading',
|
||||
value: '…'
|
||||
}]
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
setError: function () {
|
||||
this.set({
|
||||
content: {
|
||||
fields: [{
|
||||
title: null,
|
||||
alternative_name: null,
|
||||
value: 'There has been an error...',
|
||||
index: null,
|
||||
type: 'error'
|
||||
}],
|
||||
data: {}
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
setLatLng: function (latLng) {
|
||||
this.set('latlng', latLng);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.set('visibility', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.set('visibility', false);
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return !!this.get('visibility');
|
||||
},
|
||||
|
||||
setCurrentFeatureId: function (featureId) {
|
||||
this.set('currentFeatureId', featureId);
|
||||
},
|
||||
|
||||
unsetCurrentFeatureId: function () {
|
||||
this.unset('currentFeatureId');
|
||||
},
|
||||
|
||||
getCurrentFeatureId: function (featureId) {
|
||||
return this.get('currentFeatureId');
|
||||
},
|
||||
|
||||
getAlternativeName: function (fieldName) {
|
||||
return this.get('alternative_names') && this.get('alternative_names')[fieldName];
|
||||
},
|
||||
|
||||
hasInfowindowTemplate: function (infowindowTemplateModel) {
|
||||
return this._infowindowTemplateModel && this._infowindowTemplateModel === infowindowTemplateModel;
|
||||
}
|
||||
}, {
|
||||
contentForFields: function (attributes, fields, options) {
|
||||
options = options || {};
|
||||
var renderFields = [];
|
||||
|
||||
for (var j = 0; j < fields.length; ++j) {
|
||||
var field = fields[j];
|
||||
var value = attributes[field.name];
|
||||
if (options.showEmptyFields || (value !== undefined && value !== null)) {
|
||||
renderFields.push({
|
||||
name: field.name,
|
||||
title: field.title ? field.name : null,
|
||||
value: (value !== undefined && value !== null) ? value : null,
|
||||
index: j
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// manage when there is no data to render
|
||||
if (renderFields.length === 0) {
|
||||
renderFields.push({
|
||||
title: null,
|
||||
value: 'No data available',
|
||||
index: 0,
|
||||
type: 'empty'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
fields: renderFields,
|
||||
data: attributes
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = InfowindowModel;
|
||||
@@ -0,0 +1,676 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
require('clip-path-polygon');
|
||||
var sanitize = require('../../core/sanitize');
|
||||
var Template = require('../../core/template');
|
||||
var View = require('../../core/view');
|
||||
var util = require('../../core/util');
|
||||
|
||||
var ESC_KEY = 27;
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* var infowindow = new Infowindow({
|
||||
* model: infowindowModel,
|
||||
* mapView: mapView
|
||||
* });
|
||||
*
|
||||
* // Show the infowindow:
|
||||
* infowindow.showInfowindow();
|
||||
*/
|
||||
|
||||
var Infowindow = View.extend({
|
||||
options: {
|
||||
imageTransitionSpeed: 300,
|
||||
hookMargin: 24,
|
||||
hookHeight: 16
|
||||
},
|
||||
|
||||
className: 'CDB-infowindow-wrapper',
|
||||
|
||||
events: {
|
||||
// Close bindings
|
||||
'click .js-close': '_closeInfowindow',
|
||||
// some migrations doesn't have js-close class
|
||||
'click .close': '_closeInfowindow',
|
||||
'touchstart .js-close': '_closeInfowindow',
|
||||
'MSPointerDown .js-close': '_closeInfowindow',
|
||||
// Rest infowindow bindings
|
||||
'dragstart': '_stopPropagation',
|
||||
'mousedown': '_stopPropagation',
|
||||
'pointerdown': '_stopPropagation',
|
||||
'touchstart': '_stopPropagation',
|
||||
'MSPointerDown': '_stopPropagation',
|
||||
'dblclick': '_stopPropagation',
|
||||
'DOMMouseScroll': 'killEvent',
|
||||
'MozMousePixelScroll': 'killEvent',
|
||||
'mousewheel': 'killEvent',
|
||||
'dbclick': '_stopPropagation',
|
||||
'click': '_stopPropagation'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.mapView = this.options.mapView;
|
||||
|
||||
this._compileTemplate();
|
||||
this._initBinds();
|
||||
|
||||
// Hide the element
|
||||
this.$el.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render infowindow content
|
||||
*/
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
if (this.template) {
|
||||
// Clone fields and template name
|
||||
var fields = _.map(this.model.attributes.content.fields, function (field) {
|
||||
return _.clone(field);
|
||||
});
|
||||
|
||||
var data = this.model.get('content') ? this.model.get('content').data : {};
|
||||
|
||||
// Sanitized fields
|
||||
fields = _.map(fields, this._sanitizeField, this);
|
||||
|
||||
// Join plan fields values with content to work with
|
||||
// custom infowindows and CartoDB infowindows.
|
||||
var values = {};
|
||||
|
||||
_.each(fields, function (pair) {
|
||||
values[pair.name] = pair.value;
|
||||
});
|
||||
|
||||
var obj = _.extend({
|
||||
content: {
|
||||
fields: fields,
|
||||
data: data
|
||||
}
|
||||
}, values);
|
||||
|
||||
this.$el.html(
|
||||
sanitize.html(this.template(obj), this.model.get('sanitizeTemplate'))
|
||||
);
|
||||
|
||||
// Set width and max-height from the model only
|
||||
// If there is no width set, we don't force our infowindow
|
||||
if (this.model.get('width')) {
|
||||
this.$('.js-infowindow').css('width', this.model.get('width') + 'px');
|
||||
}
|
||||
|
||||
if (this.model.get('maxHeight')) {
|
||||
this._getContent().css('max-height', this.model.get('maxHeight') + 'px');
|
||||
}
|
||||
|
||||
if (this._containsCover()) {
|
||||
this._loadCover();
|
||||
}
|
||||
|
||||
this._setupClasses();
|
||||
this._renderScroll();
|
||||
this._renderShadows();
|
||||
this._bindScroll();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
_.bindAll(this, '_onKeyUp', '_onLoadImageSuccess', '_onLoadImageError');
|
||||
|
||||
this.listenTo(this.model, 'change:content change:alternative_names change:width change:maxHeight', this.render, this);
|
||||
this.listenTo(this.model, 'change:latlng', this._updateAndAdjustPan, this);
|
||||
this.listenTo(this.model, 'change:visibility', this.toggle, this);
|
||||
this.listenTo(this.model, 'change:template change:sanitizeTemplate', this._compileTemplate, this);
|
||||
|
||||
this.listenTo(this.mapView.map, 'change', this._updatePosition, this);
|
||||
|
||||
this.listenTo(this.mapView, 'zoomstart', function () {
|
||||
this.hide(true);
|
||||
});
|
||||
|
||||
this.listenTo(this.mapView, 'zoomend', function () {
|
||||
this.show();
|
||||
});
|
||||
},
|
||||
|
||||
// migration issue: some infowindows doesn't have this selector
|
||||
_getContent: function () {
|
||||
var $el = this.$('.js-content');
|
||||
if ($el.length === 0) {
|
||||
$el = this.$('.cartodb-popup-content');
|
||||
}
|
||||
|
||||
return $el;
|
||||
},
|
||||
|
||||
_onKeyUp: function (event) {
|
||||
if (event && event.keyCode === ESC_KEY) {
|
||||
this._closeInfowindow();
|
||||
}
|
||||
},
|
||||
|
||||
_setupClasses: function () {
|
||||
var $infowindow = this.$('.js-infowindow');
|
||||
var hasHeader = this.$('.js-header').length;
|
||||
var hasCover = this.$('.js-cover').length;
|
||||
var hasContent = this._getContent().length;
|
||||
var hasTitle = this.$('.CDB-infowindow-title').length;
|
||||
var numberOfFields = this.model.get('content') && this.model.get('content').fields.length;
|
||||
|
||||
if (hasCover) {
|
||||
$infowindow
|
||||
.addClass('has-header-image')
|
||||
.toggleClass('no-content', numberOfFields < 3);
|
||||
}
|
||||
|
||||
if (hasHeader) {
|
||||
$infowindow.addClass('has-header');
|
||||
}
|
||||
|
||||
if (hasContent) {
|
||||
$infowindow.addClass('has-fields');
|
||||
}
|
||||
|
||||
if (hasTitle) {
|
||||
$infowindow.addClass('has-title');
|
||||
}
|
||||
},
|
||||
|
||||
_renderScroll: function () {
|
||||
if (this._getContent().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._content = this._getContent().get(0);
|
||||
this.$('.js-infowindow').addClass('has-scroll');
|
||||
|
||||
Ps.initialize(this._content, {
|
||||
wheelSpeed: 1,
|
||||
wheelPropagation: true,
|
||||
minScrollbarLength: 20
|
||||
});
|
||||
},
|
||||
|
||||
_renderShadows: function () {
|
||||
this.$shadowTop = $('<div>').addClass('CDB-infowindow-canvasShadow CDB-infowindow-canvasShadow--top');
|
||||
this.$shadowBottom = $('<div>').addClass('CDB-infowindow-canvasShadow CDB-infowindow-canvasShadow--bottom');
|
||||
var $inner = this.$('.js-inner');
|
||||
$inner.append(this.$shadowTop);
|
||||
$inner.append(this.$shadowBottom);
|
||||
_.defer(function () {
|
||||
this._showOrHideShadows();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_bindScroll: function () {
|
||||
this.$(this._content)
|
||||
.on('ps-y-reach-start', _.bind(this._onScrollTop, this))
|
||||
.on('ps-y-reach-end', _.bind(this._onScrollBottom, this))
|
||||
.on('ps-scroll-y', _.bind(this._onScroll, this));
|
||||
},
|
||||
|
||||
_onScrollTop: function () {
|
||||
this.$shadowTop.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_onScroll: function () {
|
||||
this._showOrHideShadows();
|
||||
},
|
||||
|
||||
_showOrHideShadows: function () {
|
||||
var $el = $(this._content);
|
||||
if ($el.length) {
|
||||
var currentPos = $el.scrollTop();
|
||||
var max = $el.get(0).scrollHeight;
|
||||
var height = $el.outerHeight();
|
||||
var maxPos = max - height;
|
||||
|
||||
this.$shadowTop.toggleClass('is-visible', currentPos > 0);
|
||||
this.$shadowBottom.toggleClass('is-visible', currentPos < maxPos);
|
||||
}
|
||||
},
|
||||
|
||||
_onScrollBottom: function () {
|
||||
this.$shadowBottom.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_container: function () {
|
||||
return this.el.querySelector('.js-container');
|
||||
},
|
||||
|
||||
/**
|
||||
* Compile template of the infowindow
|
||||
*/
|
||||
_compileTemplate: function () {
|
||||
var template = this.model.get('template');
|
||||
|
||||
if (typeof (template) !== 'function') {
|
||||
this.template = new Template({
|
||||
template: template,
|
||||
type: this.model.get('template_type') || 'mustache'
|
||||
}).asFunction();
|
||||
} else {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
_sanitizeValue: function (key, val) {
|
||||
if (_.isObject(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return String(val);
|
||||
},
|
||||
|
||||
_sanitizeField: function (attr) {
|
||||
// Check null or undefined :| and set both to empty == ''
|
||||
if (attr.value === null || attr.value === undefined) {
|
||||
attr.value = '';
|
||||
}
|
||||
|
||||
// Get the alternative name
|
||||
var alternativeName = this.model.getAlternativeName(attr.name);
|
||||
attr.title = (attr.title && alternativeName) ? alternativeName : attr.title;
|
||||
|
||||
// Save new sanitized value
|
||||
attr.value = JSON.parse(JSON.stringify(attr.value), this._sanitizeValue);
|
||||
|
||||
return attr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Does header contain cover?
|
||||
*/
|
||||
_containsCover: function () {
|
||||
return !!this.$('.js-infowindow').attr('data-cover');
|
||||
},
|
||||
|
||||
_containsTemplateCover: function () {
|
||||
return this.$('.js-cover img').length > 0;
|
||||
},
|
||||
|
||||
_getCoverURL: function () {
|
||||
var content = this.model.get('content');
|
||||
var imageSRC = this.$('.js-cover img').attr('src');
|
||||
|
||||
if (imageSRC) {
|
||||
return imageSRC;
|
||||
}
|
||||
|
||||
if (content && content.fields && content.fields.length > 0) {
|
||||
return (content.fields[0].value || '').toString();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_imageOnHookNotSupported: function () {
|
||||
var noCssClipPath = util.ie || util.browser.ie || util.browser.edge;
|
||||
return noCssClipPath;
|
||||
},
|
||||
|
||||
_loadImageHook: function (imageDimensions, coverDimensions, url) {
|
||||
var $hook = this.$('.CDB-hook');
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._imageOnHookNotSupported()) {
|
||||
$hook.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var $hookImage = $('<img />')
|
||||
.addClass('CDB-hookImage js-hookImage')
|
||||
.attr('src', url);
|
||||
|
||||
$hook.append($hookImage);
|
||||
|
||||
$hookImage.css({
|
||||
marginTop: -(imageDimensions.height - this.options.hookHeight),
|
||||
width: coverDimensions.width,
|
||||
display: 'none'
|
||||
});
|
||||
|
||||
$hookImage.load(
|
||||
function () {
|
||||
$hook.parent().addClass('has-image');
|
||||
$hookImage.clipPath(this._getHookPoints(imageDimensions.height - this.options.hookHeight));
|
||||
$hookImage.show();
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
_getHookPoints: function (imageHeight) {
|
||||
return [
|
||||
[24, imageHeight],
|
||||
[24, imageHeight + 16],
|
||||
[48, imageHeight],
|
||||
[24, imageHeight]
|
||||
];
|
||||
},
|
||||
|
||||
_loadCoverFromTemplate: function (url) {
|
||||
this.$('.js-cover img').remove();
|
||||
this._loadCoverFromUrl(url);
|
||||
},
|
||||
|
||||
_loadCoverFromUrl: function (url) {
|
||||
var $cover = this.$('.js-cover');
|
||||
|
||||
this._startCoverLoader();
|
||||
|
||||
var $img = $("<img class='CDB-infowindow-media-item' />");
|
||||
$cover.append($img);
|
||||
|
||||
$img
|
||||
.load(this._onLoadImageSuccess)
|
||||
.error(this._onLoadImageError)
|
||||
.attr('src', url);
|
||||
},
|
||||
|
||||
_onLoadImageError: function () {
|
||||
this._stopCoverLoader();
|
||||
this._showInfowindowImageError();
|
||||
},
|
||||
|
||||
_onLoadImageSuccess: function () {
|
||||
var $cover = this.$('.js-cover');
|
||||
var $img = this.$('.CDB-infowindow-media-item');
|
||||
var url = $img.attr('src');
|
||||
var numFields = this.model.get('content').fields.length;
|
||||
|
||||
var imageDimensions = { width: $img.width(), height: $img.height() };
|
||||
var coverDimensions = { width: $cover.width(), height: $cover.height() };
|
||||
|
||||
var styles = this._calcImageStyle(imageDimensions, coverDimensions);
|
||||
|
||||
$img.css(styles);
|
||||
|
||||
$cover.css({ height: imageDimensions.height - this.options.hookHeight });
|
||||
|
||||
this._stopCoverLoader();
|
||||
|
||||
$img.fadeIn(150);
|
||||
|
||||
if (numFields < 3 && imageDimensions.height >= this.$el.height()) {
|
||||
this._loadImageHook(imageDimensions, coverDimensions, url);
|
||||
}
|
||||
},
|
||||
|
||||
_calcImageStyle: function (imageDimensions, coverDimensions) {
|
||||
var styles = {};
|
||||
|
||||
var imageRatio = imageDimensions.height / imageDimensions.width;
|
||||
var coverRatio = coverDimensions.height / coverDimensions.width;
|
||||
|
||||
if (imageDimensions.width > coverDimensions.width && imageDimensions.height > coverDimensions.height) {
|
||||
if (imageRatio < coverRatio) {
|
||||
styles = { height: coverDimensions.height };
|
||||
}
|
||||
} else {
|
||||
styles = { width: imageDimensions.width };
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
|
||||
_loadCover: function () {
|
||||
this._renderCoverLoader();
|
||||
this._startCoverLoader();
|
||||
|
||||
var url = this._getCoverURL();
|
||||
|
||||
if (this._isLoadingFields()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isValidURL(url)) {
|
||||
this._stopCoverLoader();
|
||||
this._showInfowindowImageError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._containsTemplateCover()) {
|
||||
this._loadCoverFromTemplate(url);
|
||||
} else {
|
||||
this._loadCoverFromUrl(url);
|
||||
}
|
||||
},
|
||||
|
||||
_isLoadingFields: function () {
|
||||
var content = this.model.get('content');
|
||||
return !content || (content.fields.length === 1 && content.fields[0].type === 'loading');
|
||||
},
|
||||
|
||||
_renderCoverLoader: function () {
|
||||
var $loader = $('<div>').addClass('CDB-Loader js-loader');
|
||||
|
||||
if (this.$('.js-cover').length > 0) {
|
||||
this.$('.js-cover').append($loader);
|
||||
}
|
||||
},
|
||||
|
||||
_startCoverLoader: function () {
|
||||
this.$('.js-infowindow').addClass('is-loading');
|
||||
this.$('.js-loader').addClass('is-visible');
|
||||
},
|
||||
|
||||
_clearInfowindowImageError: function () {
|
||||
this.$('.js-infowindow').removeClass('is-fail');
|
||||
},
|
||||
|
||||
_showInfowindowImageError: function () {
|
||||
this.$('.js-infowindow').addClass('is-fail');
|
||||
this.$('.js-cover').append('<p class="CDB-infowindow-fail">Non-valid picture URL</p>');
|
||||
},
|
||||
|
||||
_stopCoverLoader: function () {
|
||||
this.$('.js-infowindow').removeClass('is-loading');
|
||||
this.$('.js-loader').removeClass('is-visible');
|
||||
},
|
||||
|
||||
/**
|
||||
* Return true if the provided URL is valid
|
||||
*/
|
||||
_isValidURL: function (url) {
|
||||
if (url) {
|
||||
var urlPattern = /^(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-|]*[\w@?^=%&\/~+#-])?$/;
|
||||
return String(url).match(urlPattern) !== null;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle infowindow visibility
|
||||
*/
|
||||
toggle: function () {
|
||||
if (this.model.get('visibility')) {
|
||||
this.show(true);
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_stopPropagation: function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Close infowindow
|
||||
*/
|
||||
_closeInfowindow: function (ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
}
|
||||
if (this.model.get('visibility')) {
|
||||
this.model.set('visibility', false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show infowindow (update, pan, etc)
|
||||
*/
|
||||
show: function (adjustPan) {
|
||||
$(document)
|
||||
.off('keyup', this._onKeyUp)
|
||||
.on('keyup', this._onKeyUp);
|
||||
|
||||
if (this.model.get('visibility')) {
|
||||
this.$el.css({ left: -5000 });
|
||||
this._update(adjustPan);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get infowindow visibility
|
||||
*/
|
||||
isHidden: function () {
|
||||
return !this.model.get('visibility');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set infowindow to hidden
|
||||
*/
|
||||
hide: function (force) {
|
||||
$(document).off('keyup', this._onKeyUp);
|
||||
if (force || !this.model.get('visibility')) this._animateOut();
|
||||
},
|
||||
|
||||
_updateAndAdjustPan: function () {
|
||||
this._update(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update infowindow
|
||||
*/
|
||||
_update: function (adjustPan) {
|
||||
if (!this.isHidden()) {
|
||||
var delay = 0;
|
||||
|
||||
if (adjustPan) {
|
||||
delay = this.adjustPan();
|
||||
}
|
||||
|
||||
this._updatePosition();
|
||||
this._animateIn(delay);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Animate infowindow to show up
|
||||
*/
|
||||
_animateIn: function (delay) {
|
||||
if (!util.ie || (util.browser.ie && util.browser.ie.version > 8)) {
|
||||
this.$el.css({
|
||||
'marginBottom': '-10px',
|
||||
'display': 'block',
|
||||
'visibility': 'visible',
|
||||
opacity: 0
|
||||
});
|
||||
|
||||
this.$el
|
||||
.delay(delay)
|
||||
.animate({
|
||||
opacity: 1,
|
||||
marginBottom: 0
|
||||
}, 300);
|
||||
} else {
|
||||
this.$el.show();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Animate infowindow to disappear
|
||||
*/
|
||||
_animateOut: function () {
|
||||
if (!util.ie || (util.browser.ie && util.browser.ie.version > 8)) {
|
||||
var self = this;
|
||||
this.$el.animate({
|
||||
marginBottom: '-10px',
|
||||
opacity: '0',
|
||||
display: 'block'
|
||||
}, 180, function () {
|
||||
self.$el.css({visibility: 'hidden'});
|
||||
});
|
||||
} else {
|
||||
this.$el.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjust pan to show correctly the infowindow
|
||||
*/
|
||||
// TODO: This can be private
|
||||
adjustPan: function () {
|
||||
var offset = this.model.get('offset');
|
||||
|
||||
if (!this.model.get('autoPan') || this.isHidden()) { return; }
|
||||
|
||||
var containerHeight = this.$el.outerHeight(true) + 15; // Adding some more space
|
||||
var containerWidth = this.$el.width();
|
||||
var pos = this.mapView.latLngToContainerPoint(this.model.get('latlng'));
|
||||
var adjustOffset = {x: 0, y: 0};
|
||||
var size = this.mapView.getSize();
|
||||
var waitCallback = 0;
|
||||
|
||||
if (pos.x - offset[0] < 0) {
|
||||
adjustOffset.x = pos.x - offset[0] - 10;
|
||||
}
|
||||
|
||||
if (pos.x - offset[0] + containerWidth > size.x) {
|
||||
adjustOffset.x = pos.x + containerWidth - size.x - offset[0] + 10;
|
||||
}
|
||||
|
||||
if (pos.y - containerHeight < 0) {
|
||||
adjustOffset.y = pos.y - containerHeight - 10;
|
||||
}
|
||||
|
||||
if (pos.y - containerHeight > size.y) {
|
||||
adjustOffset.y = pos.y + containerHeight - size.y;
|
||||
}
|
||||
|
||||
if (adjustOffset.x || adjustOffset.y) {
|
||||
this.mapView.panBy(adjustOffset);
|
||||
waitCallback = 300;
|
||||
}
|
||||
|
||||
return waitCallback;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the position (private)
|
||||
*/
|
||||
_updatePosition: function () {
|
||||
if (this.isHidden()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = this.model.get('offset');
|
||||
var pos = this.mapView.latLngToContainerPoint(this.model.get('latlng'));
|
||||
var left = pos.x - offset[0];
|
||||
var size = this.mapView.getSize();
|
||||
var bottom = -1 * (pos.y - offset[1] - size.y);
|
||||
|
||||
this.$el.css({ bottom: bottom, left: left });
|
||||
},
|
||||
|
||||
/**
|
||||
* Set visibility infowindow
|
||||
*/
|
||||
showInfowindow: function () {
|
||||
this.model.set('visibility', true);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Infowindow;
|
||||
@@ -0,0 +1,85 @@
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var util = require('../../../../core/util');
|
||||
|
||||
module.exports = Backbone.View.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.el) { throw new Error('element is mandatory.'); }
|
||||
if (!opts.imageClass) { throw new Error('Image class is mandatory.'); }
|
||||
|
||||
this._color = this.$el.data('color');
|
||||
this._icon = this.$el.data('icon');
|
||||
|
||||
this._imageClass = opts.imageClass;
|
||||
this._lastImage = {
|
||||
url: null,
|
||||
content: null
|
||||
};
|
||||
},
|
||||
|
||||
_loadImage: function () {
|
||||
var self = this;
|
||||
var isSVG = this._isSVG(this._icon);
|
||||
|
||||
if (this.$el.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSVG) {
|
||||
this._requestImage(this._icon, function (content) {
|
||||
var svg = content.cloneNode(true);
|
||||
var $svg = $(svg);
|
||||
$svg = $svg.removeAttr('xmlns:a');
|
||||
$svg.attr('class', self._imageClass + ' js-image');
|
||||
|
||||
self.$el.empty().append($svg);
|
||||
|
||||
$svg.css('fill', self._color);
|
||||
$svg.find('g').css('fill', 'inherit');
|
||||
$svg.find('path').css('fill', 'inherit');
|
||||
$svg.find('rect').each(function (_, rect) {
|
||||
var $rect = $(rect);
|
||||
if ($rect.css('fill') !== 'none') {
|
||||
$rect.css('fill', 'inherit');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var $img = $('<img crossorigin="anonymous"/>');
|
||||
$img.attr('class', self._imageClass + ' js-image');
|
||||
$img.attr('src', this._icon + '?req=markup');
|
||||
this.$el.empty().append($img);
|
||||
}
|
||||
},
|
||||
|
||||
_requestImage: function (url, callback) {
|
||||
var self = this;
|
||||
var completeUrl = url + '?req=ajax';
|
||||
|
||||
if (this._lastImage.url === completeUrl) {
|
||||
callback && callback(this._lastImage.content);
|
||||
} else {
|
||||
$.ajax(completeUrl)
|
||||
.done(function (data) {
|
||||
self._lastImage.url = completeUrl;
|
||||
var content = self._lastImage.content = data.getElementsByTagName('svg')[0];
|
||||
callback && callback(content);
|
||||
})
|
||||
.fail(function () {
|
||||
throw new Error("Couldn't get " + completeUrl + ' file.');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_updateImageColor: function (color) {
|
||||
this.$('.js-image').css('fill', color);
|
||||
},
|
||||
|
||||
_isSVG: function (url) {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
var noQueryString = url.split('?')[0];
|
||||
return noQueryString && util.endsWith(noQueryString.toUpperCase(), 'SVG');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<h3 class="CDB-Text CDB-Size-small u-upperCase u-bSpace u-altTextColor u-ellipsis <% if (error) { %> u-<%- error %>TextColor <% } %>" title="<%- title %>">
|
||||
<%- title %>
|
||||
</h3>
|
||||
@@ -0,0 +1,107 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var sanitize = require('../../../../core/sanitize');
|
||||
var legendTitleTemplate = require('./legend-title.tpl');
|
||||
var ImageLoaderView = require('./img-loader-view');
|
||||
|
||||
var LegendViewBase = Backbone.View.extend({
|
||||
|
||||
className: 'CDB-Legend-item u-tSpace-xl',
|
||||
|
||||
initialize: function (opts) {
|
||||
this._placeholderTemplate = opts.placeholderTemplate;
|
||||
|
||||
this.model.on('change', this.render, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this._generateHTML());
|
||||
|
||||
this._loadImages();
|
||||
|
||||
this.model.isVisible()
|
||||
? this.$el.show()
|
||||
: this.$el.hide();
|
||||
|
||||
this._toggleLoadingClass();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_generateHTML: function () {
|
||||
if (this.model.isSuccess() && this.model.isAvailable()) {
|
||||
return this._getLegendHTML();
|
||||
}
|
||||
|
||||
var placeholder = this._getPlaceholderHTML();
|
||||
|
||||
if (this.model.isLoading()) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
var message = this.model.isError()
|
||||
? this._generateTitle({ title: 'Something went wrong', error: 'error' })
|
||||
: this._generateTitle({ title: 'No data available', error: 'alert' });
|
||||
|
||||
return [message, placeholder].join('\n');
|
||||
},
|
||||
|
||||
_getLegendHTML: function () {
|
||||
var html = [];
|
||||
|
||||
// Legend title
|
||||
if (this.model.get('title')) {
|
||||
html.push(this._generateTitle({ title: this.model.get('title') }));
|
||||
}
|
||||
|
||||
// Pre HTML Snippet
|
||||
if (this.model.get('preHTMLSnippet')) {
|
||||
html.push(this._sanitize(this.model.get('preHTMLSnippet')));
|
||||
}
|
||||
|
||||
// Template
|
||||
html.push(this._sanitize(this._getCompiledTemplate()));
|
||||
|
||||
// Post HTML Snippet
|
||||
if (this.model.get('postHTMLSnippet')) {
|
||||
html.push(this._sanitize(this.model.get('postHTMLSnippet')));
|
||||
}
|
||||
|
||||
return html.join('\n');
|
||||
},
|
||||
|
||||
_generateTitle: function (data) {
|
||||
return legendTitleTemplate({ title: data.title, error: data.error });
|
||||
},
|
||||
|
||||
_getPlaceholderHTML: function () {
|
||||
return (this._placeholderTemplate && this._placeholderTemplate()) || '';
|
||||
},
|
||||
|
||||
_getCompiledTemplate: function () {
|
||||
throw new Error('Subclasses of LegendViewBase must implement _getCompiledTemplate');
|
||||
},
|
||||
|
||||
_afterRender: function () { },
|
||||
|
||||
_sanitize: function (html) {
|
||||
return sanitize.html(html);
|
||||
},
|
||||
|
||||
_toggleLoadingClass: function () {
|
||||
this.$el.toggleClass('is-loading', this.model.isLoading());
|
||||
},
|
||||
|
||||
_loadImages: function () {
|
||||
_.each(this.$('.js-image-container'), function ($el) {
|
||||
var iconView = new ImageLoaderView({
|
||||
el: $el,
|
||||
imageClass: 'Legend-fillImageAsset'
|
||||
});
|
||||
|
||||
iconView._loadImage();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LegendViewBase;
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="Bubble-container">
|
||||
<ul class="Bubble-list<% if (hasCustomLabels && labels[1] === '') { %> Bubble-list--custom<% } %>">
|
||||
<% for (var i in bubbleSizes) { %>
|
||||
<%
|
||||
var customCssClass = '';
|
||||
var index = +i;
|
||||
if (hasCustomLabels) {
|
||||
customCssClass = (labels[0] !== '' && index === 0) ? '' : 'Bubble-item--custom';
|
||||
}
|
||||
%>
|
||||
<li class="js-bubbleItem Bubble-item <%- customCssClass %>">
|
||||
<% if (!hasCustomLabels || (hasCustomLabels && index === 0)) {%>
|
||||
<div class="Bubble-label CDB-Text CDB-Size-small" style="height: <%- bubbleSizes[i] %>%;">
|
||||
<div class="Bubble-numbersItem CDB-Text CDB-Size-small"><%= formatter.formatNumber(labels[i]) %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="Bubble-circle">
|
||||
<span class="Bubble-itemCircle" style="height: <%- bubbleSizes[i] %>%; width: <%- bubbleSizes[i] %>%; <%- fillColor ? 'opacity:1; background-color:' + fillColor + ';': ''%>" ></span>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (labels[labels.length - 1]) { %>
|
||||
<li class="js-bubbleItem Bubble-item">
|
||||
<div class="Bubble-label CDB-Text CDB-Size-small" style="height: 0%;">
|
||||
<div class="Bubble-numbersItem CDB-Text CDB-Size-small"><%= formatter.formatNumber(labels[labels.length - 1]) %></div>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<% if (!hasCustomLabels) { %>
|
||||
<p class="Bubble-average CDB-Text CDB-Size-small u-altTextColor <% if (hasCustomLabels) { %>Bubble-average--custom<% } %>" style="bottom: <%- avgSize %>%;">
|
||||
AVG: <%= formatter.formatNumber(avgLabel) %>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
var _ = require('underscore');
|
||||
var LegendViewBase = require('../base/legend-view-base');
|
||||
var template = require('./legend-template.tpl');
|
||||
var formatter = require('../../../../util/formatter');
|
||||
|
||||
var BubbleLegendView = LegendViewBase.extend({
|
||||
|
||||
className: 'CDB-Legend-item u-tSpace-xl u-clearfix',
|
||||
|
||||
_getCompiledTemplate: function () {
|
||||
return template({
|
||||
hasCustomLabels: this._hasCustomLabels(),
|
||||
labels: this._calculateLabels(),
|
||||
bubbleSizes: this._calculateBubbleSizes(),
|
||||
labelPositions: this._calculateLabelPositions(),
|
||||
avgSize: this._calculateAverageSize(),
|
||||
avgLabel: this.model.get('avg'),
|
||||
fillColor: this.model.get('fillColor'),
|
||||
formatter: formatter
|
||||
});
|
||||
},
|
||||
|
||||
_hasCustomLabels: function () {
|
||||
var topLabel = this.model.get('topLabel');
|
||||
var bottomLabel = this.model.get('bottomLabel');
|
||||
return ((topLabel != null && topLabel !== '') || (bottomLabel != null && bottomLabel !== ''));
|
||||
},
|
||||
|
||||
_calculateLabelPositions: function () {
|
||||
var labelPositions;
|
||||
if (this._hasCustomLabels()) {
|
||||
labelPositions = [0, 100];
|
||||
} else {
|
||||
labelPositions = this._calculateBubbleSizes();
|
||||
labelPositions.push(0);
|
||||
}
|
||||
return labelPositions;
|
||||
},
|
||||
|
||||
_calculateLabels: function () {
|
||||
var labels;
|
||||
|
||||
if (this._hasCustomLabels()) {
|
||||
labels = [];
|
||||
labels.push(this.model.get('bottomLabel'));
|
||||
labels.push(this.model.get('topLabel'));
|
||||
labels = labels.reverse();
|
||||
} else {
|
||||
labels = this.model.get('values').slice(0);
|
||||
if (this._areSizesInAscendingOrder()) {
|
||||
labels = labels.reverse();
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
},
|
||||
|
||||
_calculateValues: function () {
|
||||
var sizes = this.model.get('sizes').slice(0);
|
||||
if (this._areSizesInAscendingOrder()) {
|
||||
sizes = sizes.reverse();
|
||||
}
|
||||
return sizes;
|
||||
},
|
||||
|
||||
_areSizesInAscendingOrder: function () {
|
||||
var sizes = this.model.get('sizes').slice(0);
|
||||
return _.first(sizes) < _.last(sizes);
|
||||
},
|
||||
|
||||
_calculateBubbleSizes: function () {
|
||||
var sizes = this._calculateValues();
|
||||
var maxSize = _.max(sizes);
|
||||
return _.map(sizes, function (size, index) {
|
||||
return size * 100 / maxSize;
|
||||
});
|
||||
},
|
||||
|
||||
_calculateAverageSize: function () {
|
||||
// we build the size range, and reverse it to be able to normalize it with values.
|
||||
var sizes = this._calculateBubbleSizes().reverse();
|
||||
|
||||
// we put 0 as the initial of the range
|
||||
sizes.unshift(0);
|
||||
|
||||
// we need values and sizes because avg is a value and we need to place it taking sizes as reference.
|
||||
var values = this.model.get('values').slice(0);
|
||||
var avg = this.model.get('avg');
|
||||
|
||||
// we need to position it in the right range
|
||||
// this reduce thing search the index of the range (index base) that avg belongs to
|
||||
// while avg is greater than the current value, memo is the next index, if not, memo doesn't change anymore
|
||||
// index + 1 because we pushed 0 to sizes and we need both arrays to have the same length
|
||||
var index = _.reduce(values, function (memo, value, index) {
|
||||
return value > avg ? memo : index + 1;
|
||||
}, 0);
|
||||
|
||||
// avg it's at least equal to the last item
|
||||
if (index === values.length) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// with the index calculate above, we get the % of the sizes we have to place the avg
|
||||
var minValue = sizes[index - 1];
|
||||
var maxValue = sizes[index];
|
||||
|
||||
// Inside the range, we position it lineal
|
||||
var offset = (avg - values[index - 1]) / (values[index] - values[index - 1]);
|
||||
return minValue + (maxValue - minValue) * offset;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BubbleLegendView;
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="Legend-placeholder">
|
||||
<div class="CDB-Loader is-visible Legend-loading"></div>
|
||||
<div class="Legend-placeholderInner">
|
||||
<svg width="191px" height="55px" viewBox="0 3 191 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Graph" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 3.000000)">
|
||||
<g id="Group-2" fill="#F9F9F9">
|
||||
<g id="Group-3">
|
||||
<rect id="Rectangle-3147" x="0" y="45" width="72" height="9"></rect>
|
||||
<rect id="Rectangle-Copy-2" x="0" y="2" width="78" height="9"></rect>
|
||||
<rect id="Rectangle-Copy-3" x="0" y="24" width="48" height="9"></rect>
|
||||
<ellipse id="Oval-1585" cx="164" cy="27" rx="27" ry="27"></ellipse>
|
||||
<ellipse id="Oval-1585" stroke="#FFFFFF" cx="164" cy="33" rx="21" ry="21"></ellipse>
|
||||
<circle id="Oval-1585" stroke="#FFFFFF" cx="164" cy="45" r="9"></circle>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group" transform="translate(137.000000, 0.000000)"></g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<% if (categories && categories.length > 0) { %>
|
||||
<ul>
|
||||
<% for(var i in categories) { %>
|
||||
<li class="Legend-categoryListItem u-flex u-alignCenter">
|
||||
<% if (categories[i].icon) { %>
|
||||
<span class="Legend-categoryIcon js-image-container" data-icon="<%= categories[i].icon %>" data-color="<%= categories[i].color %>"></span>
|
||||
<% } else if (categories[i].color) { %>
|
||||
<span class="Legend-categoryCircle" style="opacity:1; background: <%= categories[i].color %>;"></span>
|
||||
<% } %>
|
||||
<p class="Legend-categoryTitle CDB-Text CDB-Size-small u-upperCase u-ellipsis" title="<%= categories[i].title %>"><%= categories[i].title %></p>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% }%>
|
||||
@@ -0,0 +1,25 @@
|
||||
var LegendViewBase = require('../base/legend-view-base');
|
||||
var template = require('./legend-template.tpl');
|
||||
|
||||
var CategoryLegendView = LegendViewBase.extend({
|
||||
_getCompiledTemplate: function () {
|
||||
return template({
|
||||
categories: this._getCategories()
|
||||
});
|
||||
},
|
||||
|
||||
_getCategories: function () {
|
||||
var categories = this.model.get('categories').slice(0);
|
||||
var _default = this.model.get('default');
|
||||
if (_default) {
|
||||
categories.push({
|
||||
title: 'Others',
|
||||
icon: _default.icon,
|
||||
color: _default.color
|
||||
});
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CategoryLegendView;
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="Legend-placeholder">
|
||||
<div class="CDB-Loader is-visible Legend-loading"></div>
|
||||
<div class="Legend-placeholderInner">
|
||||
<svg width="191px" height="57px" viewBox="0 0 191 57" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Group-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Rectangle-3147" fill="#F9F9F9" x="0" y="48" width="72" height="9"></rect>
|
||||
<rect id="Rectangle" fill="#F9F9F9" x="0" y="29" width="78" height="9"></rect>
|
||||
<rect id="Rectangle" fill="#F9F9F9" x="0" y="0" width="117" height="9"></rect>
|
||||
<rect id="Rectangle" fill="#F9F9F9" x="182" y="29" width="9" height="9" rx="4.5"></rect>
|
||||
<rect id="Rectangle-Copy" fill="#F9F9F9" x="182" y="48" width="9" height="9" rx="4.5"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="u-flex u-justifySpace u-bSpace">
|
||||
<% if (!hasCustomLabels) { %>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= labels.left %> <%- suffix %></p>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= labels.right %> <%- suffix %></p>
|
||||
<% } else { %>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= labels.left %> <%- suffix %></p>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= labels.right %> <%- suffix %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="Legend-choropleth <% if (hasCustomLabels) { %>no-average<% } %>" style="opacity:1; background: linear-gradient(90deg <% for(var i in colors) { %>,<%= colors[i].value %><% } %>);">
|
||||
<% if (!hasCustomLabels) { %>
|
||||
<span class="Legend-choroplethAverage CDB-Text CDB-Size-small u-altTextColor" style="opacity:1; left: <%- avgPercentage %>%;">
|
||||
<span class="Legend-choroplethAverageText"><%= formatter.formatNumber(avg) %> AVG</span>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
var LegendViewBase = require('../base/legend-view-base');
|
||||
var template = require('./legend-template.tpl');
|
||||
var formatter = require('../../../../util/formatter');
|
||||
var Sanitize = require('../../../../core/sanitize');
|
||||
|
||||
var ChoroplethLegendView = LegendViewBase.extend({
|
||||
_getCompiledTemplate: function () {
|
||||
return template({
|
||||
colors: this.model.get('colors'),
|
||||
avg: this.model.get('avg'),
|
||||
hasCustomLabels: this._hasCustomLabels(),
|
||||
avgPercentage: this._calculateAVGPercentage(),
|
||||
prefix: this.model.get('prefix'),
|
||||
suffix: this.model.get('suffix'),
|
||||
labels: this._getLabels(),
|
||||
formatter: formatter
|
||||
});
|
||||
},
|
||||
|
||||
_hasCustomLabels: function () {
|
||||
var leftLabel = this.model.get('leftLabel');
|
||||
var rightLabel = this.model.get('rightLabel');
|
||||
return ((leftLabel != null && leftLabel !== '') || (rightLabel != null && rightLabel !== ''));
|
||||
},
|
||||
|
||||
_getLabels: function () {
|
||||
var colors = this.model.get('colors');
|
||||
var leftLabel = this.model.get('leftLabel');
|
||||
var rightLabel = this.model.get('rightLabel');
|
||||
var o = {};
|
||||
o.left = Sanitize.html(formatter.formatNumber(colors[0].label));
|
||||
o.right = Sanitize.html(formatter.formatNumber(colors[colors.length - 1].label));
|
||||
if (leftLabel != null && leftLabel !== '') {
|
||||
o.left = leftLabel;
|
||||
}
|
||||
|
||||
if (rightLabel != null && rightLabel !== '') {
|
||||
o.right = rightLabel;
|
||||
}
|
||||
|
||||
return o;
|
||||
},
|
||||
|
||||
// In order to work with negative values, we need to include the total range
|
||||
_calculateAVGPercentage: function () {
|
||||
return (this.model.get('avg') - this.model.get('min')) * 100 / (this.model.get('max') - this.model.get('min'));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ChoroplethLegendView;
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="Legend-placeholder">
|
||||
<div class="CDB-Loader is-visible Legend-loading"></div>
|
||||
<div class="Legend-placeholderInner">
|
||||
<svg width="192px" height="57px" viewBox="0 0 192 57" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Graph" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 48.000000)">
|
||||
<rect id="Rectangle-3147" fill="#F9F9F9" x="0" y="0" width="192" height="9" rx="4"></rect>
|
||||
</g>
|
||||
<rect id="Rectangle" stroke="none" fill="#F9F9F9" fill-rule="evenodd" x="0" y="29" width="18" height="9"></rect>
|
||||
<rect id="Rectangle" stroke="none" fill="#F9F9F9" fill-rule="evenodd" x="0" y="0" width="117" height="9"></rect>
|
||||
<rect id="Rectangle" stroke="none" fill="#F9F9F9" fill-rule="evenodd" x="169" y="29" width="23" height="9"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="u-flex u-justifySpace u-bSpace--m">
|
||||
<% if (hasCustomLabels) { %>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= leftLabel %> <%- suffix %></p>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%= rightLabel %> <%- suffix %></p>
|
||||
<% } else { %>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%- suffix %></p>
|
||||
<p class="CDB-Text CDB-Size-small"><%- prefix %> <%- suffix %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="Legend-choropleth" style="opacity:1; background: linear-gradient(90deg <% for(var i in colors) { %>,<%= colors[i].color %><% } %>);"></div>
|
||||
@@ -0,0 +1,23 @@
|
||||
var LegendViewBase = require('../base/legend-view-base');
|
||||
var template = require('./legend-template.tpl');
|
||||
|
||||
var ChoroplethLegendView = LegendViewBase.extend({
|
||||
_getCompiledTemplate: function () {
|
||||
return template({
|
||||
colors: this.model.get('colors'),
|
||||
prefix: this.model.get('prefix'),
|
||||
suffix: this.model.get('suffix'),
|
||||
hasCustomLabels: this._hasCustomLabels(),
|
||||
leftLabel: this.model.get('leftLabel'),
|
||||
rightLabel: this.model.get('rightLabel')
|
||||
});
|
||||
},
|
||||
|
||||
_hasCustomLabels: function () {
|
||||
var leftLabel = this.model.get('leftLabel');
|
||||
var rightLabel = this.model.get('rightLabel');
|
||||
return ((leftLabel != null && leftLabel !== '') || (rightLabel != null && rightLabel !== ''));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ChoroplethLegendView;
|
||||
@@ -0,0 +1,14 @@
|
||||
<% if (items && items.length > 0) { %>
|
||||
<ul>
|
||||
<% for(var i in items) { %>
|
||||
<li class="Legend-categoryListItem u-flex u-alignCenter">
|
||||
<% if (items[i].icon) { %>
|
||||
<span class="Legend-categoryIcon js-image-container" data-icon="<%= items[i].icon %>" data-color="<%= items[i].color %>"></span>
|
||||
<% } else if (items[i].color) { %>
|
||||
<span class="Legend-categoryCircle" style="opacity:1; background: <%= items[i].color %>;"></span>
|
||||
<% } %>
|
||||
<p class="Legend-categoryTitle CDB-Text CDB-Size-small u-upperCase u-ellipsis" title="<%= items[i].title %>"><%= items[i].title %></p>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% }%>
|
||||
@@ -0,0 +1,24 @@
|
||||
var LegendViewBase = require('../base/legend-view-base');
|
||||
var template = require('./legend-template.tpl');
|
||||
|
||||
var CustomLegendView = LegendViewBase.extend({
|
||||
_getCompiledTemplate: function () {
|
||||
var htmlTemplate = this.model.get('html');
|
||||
if (htmlTemplate === '') {
|
||||
htmlTemplate = template({
|
||||
items: this.model.get('items')
|
||||
});
|
||||
} else {
|
||||
htmlTemplate = this._patchStylesForIE(htmlTemplate);
|
||||
}
|
||||
return htmlTemplate;
|
||||
},
|
||||
|
||||
_patchStylesForIE: function (str) {
|
||||
var find = /\bstyle=(['"])/g;
|
||||
var replace = 'style="opacity:1; ';
|
||||
return str.replace(find, replace);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CustomLegendView;
|
||||
@@ -0,0 +1,17 @@
|
||||
<h2 class="CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter">
|
||||
<% if (showLayerSelector) { %>
|
||||
<span class="u-iBlock u-rSpace--m">
|
||||
<% if (isLayerVisible) { %>
|
||||
<input class="CDB-Checkbox js-toggle-layer" type="checkbox" checked>
|
||||
<% } else { %>
|
||||
<input class="CDB-Checkbox js-toggle-layer" type="checkbox">
|
||||
<% } %>
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="u-ellipsis"><%- layerName %></span>
|
||||
</h2>
|
||||
|
||||
<% if (showLegends) { %>
|
||||
<div class="Legends js-legends"></div>
|
||||
<% } %>
|
||||
@@ -0,0 +1,123 @@
|
||||
var _ = require('underscore');
|
||||
var View = require('../../../core/view');
|
||||
var template = require('./layer-legends-template.tpl');
|
||||
var LegendViewFactory = require('./legend-view-factory');
|
||||
|
||||
var LayerLegendsView = View.extend({
|
||||
|
||||
className: 'CDB-LayerLegends js-layer-legends',
|
||||
|
||||
events: {
|
||||
'click .js-toggle-layer': '_onToggleLayerCheckboxClicked'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this._legendViews = [];
|
||||
|
||||
this.settingsModel = options.settingsModel;
|
||||
|
||||
this.model.on('change:visible', this.render, this);
|
||||
this.model.on('change:layer_name', this.render, this);
|
||||
|
||||
this._getLegendModels().forEach(function (legendModel) {
|
||||
legendModel.on('change:state', _.debounce(this.render, 150), this);
|
||||
legendModel.on('change:visible', _.debounce(this.render, 150), this);
|
||||
this.add_related_model(legendModel);
|
||||
}, this);
|
||||
|
||||
this.settingsModel.on('change', this.render, this);
|
||||
this.add_related_model(this.settingsModel);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var showLegends = this._shouldLegendsBeVisible();
|
||||
var showLayerSelector = this._shouldLayerSelectorBeVisible();
|
||||
var shouldVisible = this._shouldLayerLegendsBeVisible();
|
||||
|
||||
if (shouldVisible) {
|
||||
this.$el.html(
|
||||
template({
|
||||
layerName: this.model.getName(),
|
||||
isLayerVisible: this._isLayerVisible(),
|
||||
showLegends: showLegends,
|
||||
showLayerSelector: showLayerSelector
|
||||
})
|
||||
);
|
||||
|
||||
this._renderLegends();
|
||||
} else {
|
||||
this.$el.html('');
|
||||
}
|
||||
|
||||
this.trigger('render');
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
return this.$el.html() === '';
|
||||
},
|
||||
|
||||
_shouldLegendsBeVisible: function () {
|
||||
var showLegends = this.settingsModel.get('showLegends');
|
||||
return showLegends && this._isLayerVisible();
|
||||
},
|
||||
|
||||
_shouldLayerLegendsBeVisible: function () {
|
||||
var showLegends = this.settingsModel.get('showLegends');
|
||||
var hasLegends = this.model.legends.hasAnyLegend();
|
||||
return this._shouldLayerSelectorBeVisible() || (this._isLayerVisible() && showLegends && hasLegends);
|
||||
},
|
||||
|
||||
_shouldLayerSelectorBeVisible: function () {
|
||||
var isLayerSelectorEnabled = this.settingsModel.get('layerSelectorEnabled');
|
||||
var showLayerSelector = this.settingsModel.get('showLayerSelector');
|
||||
var shouldVisible = showLayerSelector && isLayerSelectorEnabled;
|
||||
|
||||
return shouldVisible;
|
||||
},
|
||||
|
||||
_renderLegends: function () {
|
||||
_.each(this._getLegendModels(), this._renderLegend, this);
|
||||
},
|
||||
|
||||
_renderLegend: function (legendModel) {
|
||||
var legendView = LegendViewFactory.createLegendView(legendModel);
|
||||
this._legendViews.push(legendView);
|
||||
this._legendsContainer().append(legendView.render().$el);
|
||||
},
|
||||
|
||||
_legendsContainer: function () {
|
||||
return this.$('.js-legends');
|
||||
},
|
||||
|
||||
_onToggleLayerCheckboxClicked: function (event) {
|
||||
var isLayerEnabled = event.target.checked;
|
||||
if (isLayerEnabled) {
|
||||
this.model.show();
|
||||
} else {
|
||||
this.model.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_getLegendViews: function () {
|
||||
return this._legendViews || [];
|
||||
},
|
||||
|
||||
_getLegendModels: function () {
|
||||
return [
|
||||
this.model.legends.custom,
|
||||
this.model.legends.choropleth,
|
||||
this.model.legends.custom_choropleth,
|
||||
this.model.legends.category,
|
||||
this.model.legends.bubble,
|
||||
this.model.legends.torque
|
||||
];
|
||||
},
|
||||
|
||||
_isLayerVisible: function () {
|
||||
return this.model.isVisible();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LayerLegendsView;
|
||||
@@ -0,0 +1,36 @@
|
||||
var BubbleLegendView = require('./bubble/legend-view');
|
||||
var CategoryLegendView = require('./categories/legend-view');
|
||||
var ChoroplethLegendView = require('./choropleth/legend-view');
|
||||
var CustomLegendView = require('./custom/legend-view');
|
||||
var CustomChoroplethLegendView = require('./custom-choropleth/legend-view');
|
||||
|
||||
var LEGEND_VIEW_CONSTRUCTORS = {
|
||||
bubble: BubbleLegendView,
|
||||
category: CategoryLegendView,
|
||||
choropleth: ChoroplethLegendView,
|
||||
custom_choropleth: CustomChoroplethLegendView,
|
||||
custom: CustomLegendView,
|
||||
torque: CustomLegendView
|
||||
};
|
||||
|
||||
var PLACEHOLDER_TEMPLATES = {
|
||||
bubble: require('./bubble/placeholder-template.tpl'),
|
||||
category: require('./categories/placeholder-template.tpl'),
|
||||
choropleth: require('./choropleth/placeholder-template.tpl')
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createLegendView: function (legendModel) {
|
||||
var legendType = legendModel.get('type');
|
||||
var LegendViewClass = LEGEND_VIEW_CONSTRUCTORS[legendType];
|
||||
var placeholderTemplate = PLACEHOLDER_TEMPLATES[legendType];
|
||||
if (LegendViewClass) {
|
||||
return new LegendViewClass({
|
||||
model: legendModel,
|
||||
placeholderTemplate: placeholderTemplate
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Legends of type "' + legendType + '" are not supported');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var View = require('../../../core/view');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
var template = require('./legends-view.tpl');
|
||||
var LayerLegendsView = require('./layer-legends-view');
|
||||
|
||||
var LegendsView = View.extend({
|
||||
|
||||
className: 'CDB-Legends-canvas',
|
||||
|
||||
initialize: function (options) {
|
||||
if (!options.layersCollection) throw new Error('layersCollection is required');
|
||||
if (!options.settingsModel) throw new Error('settingsModel is required');
|
||||
|
||||
this._layersCollection = options.layersCollection;
|
||||
this.settingsModel = options.settingsModel;
|
||||
this._isRendered = false;
|
||||
|
||||
this._initBinds();
|
||||
|
||||
this._layerLegendsViews = [];
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._layersCollection, 'add remove layerMoved', this._onLayersChanged);
|
||||
this.listenTo(this.settingsModel, 'change', this._onSettingsModelChanged);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
var layerModelsWithLegends = this._layersCollection.getLayersWithLegends();
|
||||
_.each(layerModelsWithLegends.reverse(), this._renderLayerLegends, this);
|
||||
this._isRendered = true;
|
||||
this._renderScroll();
|
||||
this._renderShadows();
|
||||
this._bindScroll();
|
||||
this._showOrHide();
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderScroll: function () {
|
||||
Ps.initialize(this._container(), {
|
||||
wheelSpeed: 1,
|
||||
wheelPropagation: false,
|
||||
swipePropagation: true,
|
||||
stopPropagationOnClick: false,
|
||||
minScrollbarLength: 20,
|
||||
suppressScrollX: true
|
||||
});
|
||||
},
|
||||
|
||||
_renderShadows: function () {
|
||||
this.$shadowTop = $('<div>').addClass('CDB-Legends-canvasShadow CDB-Legends-canvasShadow--top');
|
||||
this.$shadowBottom = $('<div>').addClass('CDB-Legends-canvasShadow CDB-Legends-canvasShadow--bottom');
|
||||
this.$el.append(this.$shadowTop);
|
||||
this.$el.append(this.$shadowBottom);
|
||||
_.defer(function () {
|
||||
this._showOrHideShadows();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_bindScroll: function () {
|
||||
this.$(this._container())
|
||||
.on('ps-y-reach-start', _.bind(this._onScrollTop, this))
|
||||
.on('ps-y-reach-end', _.bind(this._onScrollBottom, this))
|
||||
.on('ps-scroll-y', _.bind(this._onScroll, this));
|
||||
},
|
||||
|
||||
_onScrollTop: function () {
|
||||
this.$shadowTop.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_onScroll: function () {
|
||||
this._showOrHideShadows();
|
||||
},
|
||||
|
||||
_showOrHideShadows: function () {
|
||||
var $el = $(this._container());
|
||||
if ($el.length) {
|
||||
var currentPos = $el.scrollTop();
|
||||
var max = $el.get(0).scrollHeight;
|
||||
var height = $el.outerHeight();
|
||||
var maxPos = max - height;
|
||||
|
||||
this.$shadowTop.toggleClass('is-visible', currentPos > 0);
|
||||
this.$shadowBottom.toggleClass('is-visible', currentPos < maxPos);
|
||||
}
|
||||
},
|
||||
|
||||
_onScrollBottom: function () {
|
||||
this.$shadowBottom.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_container: function () {
|
||||
return this.el.querySelector('.js-container');
|
||||
},
|
||||
|
||||
_renderLayerLegends: function (layerModel) {
|
||||
var layerLegendsView = new LayerLegendsView({
|
||||
model: layerModel,
|
||||
settingsModel: this.settingsModel
|
||||
});
|
||||
|
||||
this._layerLegendsViews.push(layerLegendsView);
|
||||
layerLegendsView.on('render', this._showOrHide, this);
|
||||
this.addView(layerLegendsView);
|
||||
|
||||
this.$(this._container()).append(layerLegendsView.render().$el);
|
||||
},
|
||||
|
||||
_onLayersChanged: function (layerModel) {
|
||||
// If view has already been rendered and a layer is added / removed
|
||||
if (this._isRendered && this._hasLegends(layerModel)) {
|
||||
this._clear();
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
_showOrHide: function () {
|
||||
this.$el.toggle(this._isAnyLayerLegendViewVisible());
|
||||
},
|
||||
|
||||
_isAnyLayerLegendViewVisible: function () {
|
||||
return _.any(this._layerLegendsViews, function (layerLegendView) {
|
||||
return !layerLegendView.isEmpty();
|
||||
});
|
||||
},
|
||||
|
||||
_hasLegends: function (layerModel) {
|
||||
return !!layerModel.legends;
|
||||
},
|
||||
|
||||
_clear: function () {
|
||||
_.each(this._layerLegendsViews, function (layerLegendView) {
|
||||
layerLegendView.clean();
|
||||
});
|
||||
this._layerLegendsViews = [];
|
||||
this.$el.html('');
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$el.show();
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.$el.hide();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
View.prototype.clean.call(this);
|
||||
this._clear();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LegendsView;
|
||||
@@ -0,0 +1 @@
|
||||
<div class="CDB-Legends-canvasInner js-container"></div>
|
||||
@@ -0,0 +1,22 @@
|
||||
var View = require('../tiles/tiles-view.js');
|
||||
var template = require('../tiles/tiles-template.tpl');
|
||||
var Sanitize = require('../../../core/sanitize');
|
||||
|
||||
/**
|
||||
* Limits overlay
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'CDB-Limits',
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
limits: Sanitize.html('Some tiles might not be rendering correctly. <a target="_blank" href="https://carto.com/docs/faqs/carto-engine-usage-limits">Learn More</a>')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
var View = require('../../core/view');
|
||||
var template = require('./logo.tpl');
|
||||
var URL = 'https://carto.com';
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'CDB-Logo',
|
||||
tagName: 'a',
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
this.$el.attr('href', URL);
|
||||
this.$el.attr('target', '_blank');
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="64px" height="25px" viewBox="0 0 64 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="#AAAAAA">
|
||||
<circle id="Halo" fill-opacity="0.14" cx="51.6129032" cy="12.3870968" r="12.3870968"></circle>
|
||||
<path d="M4.12518194,16.503468 C5.88495484,16.503468 6.90196645,15.7473046 7.65615484,14.7202766 L5.98779871,13.5465304 C5.50786065,14.1221175 5.01649548,14.5058422 4.18231742,14.5058422 C3.06246194,14.5058422 2.27399226,13.5803884 2.27399226,12.3953562 L2.27399226,12.3727841 C2.27399226,11.2216099 3.06246194,10.2735841 4.18231742,10.2735841 C4.9479329,10.2735841 5.47357935,10.6460228 5.93066323,11.1990379 L7.59901935,9.92371747 C6.89053935,8.96440565 5.83924645,8.28724435 4.20517161,8.28724435 C1.7826271,8.28724435 0,10.0930078 0,12.3953562 L0,12.4179282 C0,14.7767067 1.83976258,16.503468 4.12518194,16.503468 L4.12518194,16.503468 Z M11.4304378,16.3454637 L13.7501385,16.3454637 L14.3214933,14.923425 L17.4182365,14.923425 L17.9895914,16.3454637 L20.3664275,16.3454637 L16.9611527,8.38881855 L14.8242856,8.38881855 L11.4304378,16.3454637 Z M14.9728378,13.2192358 L15.8755785,10.9846035 L16.766892,13.2192358 L14.9728378,13.2192358 Z M24.6830829,16.3454637 L26.8999397,16.3454637 L26.8999397,13.9528272 L27.88267,13.9528272 L29.4938907,16.3454637 L32.0421333,16.3454637 L30.1338081,13.5916745 C31.1279655,13.1740917 31.77931,12.3727841 31.77931,11.1538938 L31.77931,11.1313218 C31.77931,10.3525863 31.539341,9.75442715 31.07083,9.29170027 C30.5337565,8.76125726 29.6881513,8.44524866 28.465452,8.44524866 L24.6830829,8.44524866 L24.6830829,16.3454637 Z M26.8999397,12.2373519 L26.8999397,10.3300142 L28.3626081,10.3300142 C29.0939423,10.3300142 29.5624533,10.6460228 29.5624533,11.2780401 L29.5624533,11.3006121 C29.5624533,11.8761992 29.1167965,12.2373519 28.3740352,12.2373519 L26.8999397,12.2373519 Z M38.3928119,16.3454637 L38.3928119,10.3638723 L36.0045487,10.3638723 L36.0045487,8.44524866 L43.009359,8.44524866 L43.009359,10.3638723 L40.6096687,10.3638723 L40.6096687,16.3454637 L38.3928119,16.3454637 Z M51.5612903,16.516129 C49.2523838,16.516129 47.3806452,14.6674983 47.3806452,12.3870968 C47.3806452,10.1066952 49.2523838,8.25806452 51.5612903,8.25806452 C53.8701969,8.25806452 55.7419355,10.1066952 55.7419355,12.3870968 C55.7419355,14.6674983 53.8701969,16.516129 51.5612903,16.516129 Z" id="Logo"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
||||
<div class="CDB-OverlayContainer"></div>
|
||||
@@ -0,0 +1,171 @@
|
||||
var _ = require('underscore');
|
||||
var View = require('../../core/view');
|
||||
var OverlaysFactory = require('../../vis/overlays-factory');
|
||||
var overlayContainerTemplate = require('./overlays-container.tpl');
|
||||
var C = require('../../constants');
|
||||
var Engine = require('../../engine');
|
||||
|
||||
var CONTAINED_OVERLAYS = ['attribution', 'fullscreen', 'tiles', 'limits', 'logo', 'search', 'zoom'];
|
||||
|
||||
var OverlaysView = View.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.overlaysCollection) throw new Error('overlaysCollection is required');
|
||||
if (!opts.engine) throw new Error('engine is required');
|
||||
if (!opts.visView) throw new Error('visView is required');
|
||||
if (!opts.mapModel) throw new Error('mapModel is required');
|
||||
if (!opts.mapView) throw new Error('mapView is required');
|
||||
|
||||
this._overlaysCollection = opts.overlaysCollection;
|
||||
this._engine = opts.engine;
|
||||
this._visView = opts.visView;
|
||||
this._mapModel = opts.mapModel;
|
||||
this._mapView = opts.mapView;
|
||||
|
||||
this._overlayViews = [];
|
||||
this._overlaysFactory = new OverlaysFactory({
|
||||
mapModel: this._mapModel,
|
||||
mapView: this._mapView,
|
||||
visView: this._visView
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
|
||||
this.$el.append(overlayContainerTemplate());
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._clearOverlays();
|
||||
this._renderOverlays();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._engine.on(Engine.Events.RELOAD_STARTED, this._showLoaderOverlay, this);
|
||||
this._engine.on(Engine.Events.RELOAD_ERROR, this._hideLoaderOverlay, this);
|
||||
this._engine.on(Engine.Events.RELOAD_SUCCESS, this._hideLoaderOverlay, this);
|
||||
|
||||
this.listenTo(this._overlaysCollection, 'add remove change', this.render, this);
|
||||
this.listenTo(this._mapModel, 'error:limit', this._addLimitsOverlay, this);
|
||||
this.listenTo(this._mapModel, 'error:tile', this._addTilesOverlay, this);
|
||||
},
|
||||
|
||||
_clearOverlays: function () {
|
||||
while (this._overlayViews.length !== 0) {
|
||||
this._overlayViews.pop().clean();
|
||||
}
|
||||
},
|
||||
|
||||
_renderOverlays: function () {
|
||||
var overlays = this._overlaysCollection.toJSON();
|
||||
|
||||
// Sort the overlays by its internal order
|
||||
overlays = _.sortBy(overlays, function (overlay) {
|
||||
return overlay.order === null ? Number.MAX_VALUE : overlay.order;
|
||||
});
|
||||
|
||||
overlays.forEach(function (data) {
|
||||
this._renderOverlay(data);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_renderOverlay: function (overlay) {
|
||||
var overlayView = this._createOverlayView(overlay);
|
||||
if (!overlayView) return;
|
||||
|
||||
overlayView.render();
|
||||
|
||||
this._isGlobalOverlay(overlay)
|
||||
? this.$el.append(overlayView.el)
|
||||
: this._overlayContainer().append(overlayView.el);
|
||||
|
||||
this.addView(overlayView);
|
||||
this._overlayViews.push(overlayView);
|
||||
},
|
||||
|
||||
_createOverlayView: function (overlay) {
|
||||
return this._overlaysFactory.create(overlay.type, overlay, {
|
||||
visView: this._visView,
|
||||
map: this._mapModel
|
||||
});
|
||||
},
|
||||
|
||||
_isGlobalOverlay: function (overlay) {
|
||||
return CONTAINED_OVERLAYS.indexOf(overlay.type) === -1;
|
||||
},
|
||||
|
||||
_overlayContainer: function () {
|
||||
return this.$('.CDB-OverlayContainer');
|
||||
},
|
||||
|
||||
_showLoaderOverlay: function () {
|
||||
var loaderOverlay = this._getLoaderOverlay();
|
||||
if (!loaderOverlay) return;
|
||||
loaderOverlay.show();
|
||||
},
|
||||
|
||||
_hideLoaderOverlay: function () {
|
||||
var loaderOverlay = this._getLoaderOverlay();
|
||||
if (!loaderOverlay) return;
|
||||
loaderOverlay.hide();
|
||||
},
|
||||
|
||||
_getLoaderOverlay: function () {
|
||||
return this._getOverlayViewByType('loader');
|
||||
},
|
||||
|
||||
_getOverlayViewByType: function (type) {
|
||||
return _.find(this._overlayViews, function (overlayView) {
|
||||
return overlayView.type === type;
|
||||
});
|
||||
},
|
||||
|
||||
_areLimitsErrorsEnabled: function () {
|
||||
return this._visView.model.get('showLimitErrors');
|
||||
},
|
||||
|
||||
_addLimitsOverlay: function () {
|
||||
if (!this._areLimitsErrorsEnabled()) return;
|
||||
this._removeTilesOverlay();
|
||||
|
||||
var limitsOverlay = this._getOverlayViewByType(C.OVERLAY_TYPES.LIMITS);
|
||||
|
||||
limitsOverlay || this._overlaysCollection.add({
|
||||
type: C.OVERLAY_TYPES.LIMITS
|
||||
});
|
||||
},
|
||||
|
||||
_hasLimitsOverlay: function () {
|
||||
return !!this._getOverlayViewByType(C.OVERLAY_TYPES.LIMITS);
|
||||
},
|
||||
|
||||
_addTilesOverlay: function () {
|
||||
if (this._hasLimitsOverlay()) return;
|
||||
|
||||
var tilesOverlay = this._getOverlayViewByType(C.OVERLAY_TYPES.TILES);
|
||||
|
||||
tilesOverlay || this._overlaysCollection.add({
|
||||
type: C.OVERLAY_TYPES.TILES
|
||||
});
|
||||
},
|
||||
|
||||
_removeLimitsOverlay: function () {
|
||||
var overlay = this._overlaysCollection.findWhere({
|
||||
type: C.OVERLAY_TYPES.LIMITS
|
||||
});
|
||||
this._overlaysCollection.remove(overlay);
|
||||
},
|
||||
|
||||
_removeTilesOverlay: function () {
|
||||
var overlay = this._overlaysCollection.findWhere({
|
||||
type: C.OVERLAY_TYPES.TILES
|
||||
});
|
||||
this._overlaysCollection.remove(overlay);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
View.prototype.clean.apply(this);
|
||||
this._clearOverlays();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = OverlaysView;
|
||||
@@ -0,0 +1,237 @@
|
||||
var _ = require('underscore');
|
||||
var View = require('../../../core/view');
|
||||
var mapboxGeocoder = require('../../../geo/geocoder/mapbox-geocoder');
|
||||
var tomtomGeocoder = require('../../../geo/geocoder/tomtom-geocoder');
|
||||
var InfowindowModel = require('../../../geo/ui/infowindow-model');
|
||||
var Infowindow = require('../../../geo/ui/infowindow-view');
|
||||
var Point = require('../../../geo/geometry-models/point.js');
|
||||
var template = require('./search_template.tpl');
|
||||
var infowindowTemplate = require('./search_infowindow_template.tpl');
|
||||
|
||||
var DEFAULT_GEOCODER = 'tomtom';
|
||||
|
||||
var GEOCODERS = {
|
||||
'tomtom': tomtomGeocoder,
|
||||
'mapbox': mapboxGeocoder
|
||||
};
|
||||
|
||||
var GEOCODERS_WINDOW_API_KEYS = {
|
||||
'tomtom': 'tomtomApiKey',
|
||||
'mapbox': 'mapboxApiKey'
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component to place the map in the
|
||||
* location found by the geocoder.
|
||||
*/
|
||||
|
||||
var Search = View.extend({
|
||||
className: 'CDB-Search CDB-Overlay',
|
||||
|
||||
_ZOOM_BY_CATEGORY: {
|
||||
'address': 18,
|
||||
'building': 18,
|
||||
'venue': 18,
|
||||
'postal-area': 15,
|
||||
'neighbourhood': 15,
|
||||
'locality': 12,
|
||||
'localadmin': 11,
|
||||
'region': 8,
|
||||
'county': 8,
|
||||
'country': 5,
|
||||
'default': 12
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-toggle': '_onToggleClick',
|
||||
'submit .js-form': '_onSubmit',
|
||||
'click': '_stopPropagation',
|
||||
'dblclick': '_stopPropagation',
|
||||
'mousedown': '_stopPropagation'
|
||||
},
|
||||
|
||||
options: {
|
||||
searchPin: true,
|
||||
infowindowWidth: 186,
|
||||
infowindowOffset: [30, 30],
|
||||
iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAfCAYAAADXwvzvAAACuklEQVR4Ae3PQ+AsNxzA8e8vo/Xus237vVN9qW3b7qW2bdu2caxt29bu/meSmaTpqW63Pfc7wemTZPh9K/Xv3zhzxIgVrho0aMsLGo2N9o+iuYDwV02E5NJpM7d5fMGC515dMP/7l6dNMc+OGJY9Uq99cVMc33I4LOJXCQBQuXPBglNnDRm0Xa1RAWewP3yL/vJLul99Q/pNm0/b+qsnbLHngXAVgAI4b9KkXWc1m9vV58ykst56lKdMptyokdTKRJUIV1MMTGTgbOTknWABgFo2SSbOjuN9wlgIBrSIJ0yiVG9QUgGxUigRRAlpCQYrBs+A/QClliuXV6ppPVibDPPqi5irL8G+/QY2S3FZhityrLNYBWkAI2G5WTA2nGTthKDTJfP/FH1sCb76nNBa7I8/knba6Eyj8wJjLbk4qlCdAFNClWXKiiL72kGRUkSRhwUuTUm7XTqZ3z3KnMM7QhAFUfiKMZ9OQci+ydFFH32BIsDh8hxjDF2T0y0KtHHUczCg34P3wgesfWhZozstW1R/cJpuohA8dI7cWrSfxqM4gwEOnoJnn4HXBVDHwHnriNr2W3G0I8FEkKufMbjcIw1DC+iCuRw2OBduEYAKDD8drlkGlk6BHwAtIEDioD/QBnsnnHAI7A9YAAAGenwEnPuAd8+DewHcS+CeB3szvL0b7ADE/FWzYf5BCxa9dMvqa7oLll7WbTlsxKkDYRi9dPqhRz743L0PuKtOPMXtutHmm/InKf5Y6Co15Upl8qSCqVajXiEeUTRb6GqNIojoGaLEDwEA6B0KIKL8lH8JBeS/3AgK73qAPfc/tCLiAACUCmyvsJHnphwEAYFStNs/NoHgn2ATWPmlF54b/9GHH/Khn88/+9SywJx/+q0SsKTZbB45d/6CO0aNHnutv3kbYDQg9JAAIRDwF/0EjlkjUi3fkAMAAAAASUVORK5CYII=',
|
||||
iconAnchor: [7, 31]
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.map = this.model;
|
||||
this.mapView = this.options.mapView;
|
||||
this.template = this.options.template || template;
|
||||
|
||||
this._initializeGeocoder();
|
||||
},
|
||||
|
||||
_initializeGeocoder: function () {
|
||||
const injectedGeocoderConfig = this._getGeocodingInfoFromConfig();
|
||||
|
||||
this.geocoderService = this.options.geocoderService || injectedGeocoderConfig.provider || DEFAULT_GEOCODER;
|
||||
this.geocoder = GEOCODERS[this.geocoderService];
|
||||
|
||||
const windowApiKey = GEOCODERS_WINDOW_API_KEYS[this.geocoderService];
|
||||
this.token = this.options.token || injectedGeocoderConfig.token || window[windowApiKey];
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template(this.options));
|
||||
return this;
|
||||
},
|
||||
|
||||
_stopPropagation: function (ev) {
|
||||
if (ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function (ev) {
|
||||
ev.preventDefault();
|
||||
var address = this.$('.js-textInput').val();
|
||||
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
// Remove previous pin without any timeout (0 represents the timeout for
|
||||
// the animation)
|
||||
this._destroySearchPin(0);
|
||||
// TODO: we a Better way to pass api keys
|
||||
return this.geocoder.geocode(address, this.token).then(this._onResult.bind(this));
|
||||
},
|
||||
|
||||
_onResult: function (places) {
|
||||
var address = this.$('.js-textInput').val();
|
||||
|
||||
if (places && places.length > 0) {
|
||||
var location = places[0];
|
||||
var zoom = this._getZoomByCategory(location.type);
|
||||
this.model.setView(location.center, zoom, { silent: false });
|
||||
|
||||
if (this.options.searchPin) {
|
||||
this._createSearchPin(location.center, address);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onToggleClick: function () {
|
||||
this.$('.CDB-Search-inner').toggleClass('is-active');
|
||||
this.$('.js-textInput').focus();
|
||||
},
|
||||
|
||||
// Getting zoom for each type of location
|
||||
_getZoomByCategory: function (type) {
|
||||
if (type && this._ZOOM_BY_CATEGORY[type]) {
|
||||
return this._ZOOM_BY_CATEGORY[type];
|
||||
}
|
||||
return this._ZOOM_BY_CATEGORY['default'];
|
||||
},
|
||||
|
||||
_createSearchPin: function (position, address) {
|
||||
this._destroySearchPin();
|
||||
this._createPin(position, address);
|
||||
this._createInfowindow(position, address);
|
||||
this._bindEvents();
|
||||
},
|
||||
|
||||
_destroySearchPin: function (timeout) {
|
||||
this._unbindEvents();
|
||||
this._destroyPin();
|
||||
this._destroyInfowindow(timeout);
|
||||
},
|
||||
|
||||
_createInfowindow: function (position, address) {
|
||||
var infowindowModel = new InfowindowModel({
|
||||
template: infowindowTemplate,
|
||||
latlng: position,
|
||||
width: this.options.infowindowWidth,
|
||||
offset: this.options.infowindowOffset,
|
||||
content: {
|
||||
fields: [{
|
||||
title: 'address',
|
||||
value: address
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
this._searchInfowindow = new Infowindow({
|
||||
model: infowindowModel,
|
||||
mapView: this.mapView
|
||||
});
|
||||
|
||||
this.mapView.$el.append(this._searchInfowindow.el);
|
||||
infowindowModel.set('visibility', true);
|
||||
},
|
||||
|
||||
_destroyInfowindow: function (timeout) {
|
||||
if (this._searchInfowindow) {
|
||||
timeout = !_.isUndefined(timeout) && _.isNumber(timeout) ? timeout : 500;
|
||||
// Hide it and then destroy it (when animation ends)
|
||||
this._searchInfowindow.hide(true);
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
if (self._searchInfowindow) {
|
||||
self._searchInfowindow.clean();
|
||||
delete self._searchInfowindow;
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
},
|
||||
|
||||
_createPin: function (position, address) {
|
||||
this._searchPin = new Point({
|
||||
latlng: [position[0], position[1]],
|
||||
iconUrl: this.options.iconUrl,
|
||||
iconAnchor: this.options.iconAnchor
|
||||
});
|
||||
|
||||
this.map.addGeometry(this._searchPin);
|
||||
},
|
||||
|
||||
_toggleSearchInfowindow: function () {
|
||||
var infowindowVisibility = this._searchInfowindow.model.get('visibility');
|
||||
this._searchInfowindow.model.set('visibility', !infowindowVisibility);
|
||||
},
|
||||
|
||||
_destroyPin: function () {
|
||||
if (this._searchPin) {
|
||||
this.map.removeGeometry(this._searchPin);
|
||||
delete this._searchPin;
|
||||
}
|
||||
},
|
||||
|
||||
_bindEvents: function () {
|
||||
this._searchPin && this._searchPin.bind('click', this._toggleSearchInfowindow, this);
|
||||
this.mapView.bind('click', this._destroySearchPin, this);
|
||||
},
|
||||
|
||||
_unbindEvents: function () {
|
||||
this._searchPin && this._searchPin.unbind('click', this._toggleSearchInfowindow, this);
|
||||
this.mapView.unbind('click', this._destroySearchPin, this);
|
||||
},
|
||||
|
||||
_getGeocodingInfoFromConfig () {
|
||||
if (!window.geocoderConfiguration) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const provider = window.geocoderConfiguration.provider;
|
||||
let token;
|
||||
|
||||
if (provider) {
|
||||
token = window.geocoderConfiguration[provider] &&
|
||||
window.geocoderConfiguration[provider].search_bar_api_key;
|
||||
}
|
||||
|
||||
return { provider, token };
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._unbindEvents();
|
||||
this._destroySearchPin();
|
||||
View.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Search;
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="CDB-infowindow CDB-infowindow--light js-infowindow">
|
||||
<div class="CDB-infowindow-container">
|
||||
<div class="CDB-infowindow-inner">
|
||||
<div class="CDB-infowindow-list ">
|
||||
<div class="CDB-infowindow-listItem">
|
||||
<h4 class="CDB-infowindow-title"><%- address %></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="CDB-hook">
|
||||
<div class="CDB-hook-inner">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<form class="js-form CDB-Search-inner">
|
||||
<button type="button" class="CDB-Shape CDB-Search-actionButton js-toggle">
|
||||
<span class="CDB-Shape-magnify is-small"></span>
|
||||
</button>
|
||||
<input type="text" placeholder="Search Location" class="CDB-Search-text js-textInput" value="" />
|
||||
</form>
|
||||
@@ -0,0 +1,49 @@
|
||||
var View = require('../../core/view');
|
||||
|
||||
/**
|
||||
* Show or hide tiles loader
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* var tiles_loader = new TilesLoader();
|
||||
* mapWrapper.$el.append(tiles_loader.render().$el);
|
||||
*/
|
||||
var TilesLoader = View.extend({
|
||||
className: 'CDB-Loader',
|
||||
|
||||
options: {
|
||||
animationSpeed: 500
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.isVisible = 0;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function (ev) {
|
||||
if (this.isVisible) {
|
||||
return;
|
||||
}
|
||||
this.$el.addClass('is-visible');
|
||||
this.isVisible++;
|
||||
},
|
||||
|
||||
hide: function (ev) {
|
||||
this.isVisible--;
|
||||
if (this.isVisible > 0) {
|
||||
return;
|
||||
}
|
||||
this.isVisible = 0;
|
||||
this.$el.removeClass('is-visible');
|
||||
},
|
||||
|
||||
visible: function () {
|
||||
return this.isVisible > 0;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = TilesLoader;
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="CDB-Overlay CDB-Overlay--limits">
|
||||
<button type="button" class="CDB-Shape CDB-Limits-button js-button">
|
||||
<svg width="12px" height="12px" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-font_114_Warning</title>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard-1" transform="translate(-9001.000000, -11000.000000)" fill-rule="nonzero" fill="#636D72">
|
||||
<g id="icon-font_114_Warning" transform="translate(9001.000000, 11000.000000)">
|
||||
<path d="M47.3618699,443.171714 C40.4022689,456.721101 41.0006423,457.65748 56.75075,457.65748 L441.243856,457.65748 C456.985511,457.65748 457.59007,456.716689 450.632737,443.171714 L248.997303,50.6152213 L47.3618699,443.171714 Z M210.115762,31.2931921 C231.589444,-10.5131174 266.489496,-10.3489299 287.878844,31.2931921 L489.747111,424.302981 C511.220794,466.109288 489.302388,500 441.243856,500 L56.75075,500 C8.48945932,500 -13.1418536,465.945101 8.24749422,424.302981 L210.115762,31.2931921 Z M222.781864,372.97244 L222.781864,415.31496 L266.474263,415.31496 L266.474263,372.97244 L222.781864,372.97244 Z M222.781864,203.602361 L222.781864,330.62992 L266.474263,330.62992 L266.474263,203.602361 L222.781864,203.602361 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="CDB-Limits-text js-text u-ellipsis">
|
||||
<%= limits %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
var $ = require('jquery');
|
||||
var View = require('../../../core/view');
|
||||
var Model = require('../../../core/model');
|
||||
var template = require('./tiles-template.tpl');
|
||||
var Sanitize = require('../../../core/sanitize');
|
||||
|
||||
var ESC_KEY_CODE = 27;
|
||||
|
||||
/**
|
||||
* Limits overlay
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'CDB-Limits CDB-Limits--short',
|
||||
|
||||
events: {
|
||||
'click .js-button': '_toggleLimits',
|
||||
'dblclick': 'killEvent'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.model = new Model({
|
||||
visible: false
|
||||
});
|
||||
this.map = this.options.map;
|
||||
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this._onDocumentKeyDown = this._onDocumentKeyDown.bind(this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
limits: Sanitize.html('Some tiles might not be rendering correctly.')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.on('change:visible', this._onVisibleChanged, this);
|
||||
},
|
||||
|
||||
_enableDocumentBinds: function () {
|
||||
$(document).bind('keydown', this._onDocumentKeyDown);
|
||||
$(document).bind('click', this._onDocumentClick);
|
||||
},
|
||||
|
||||
_disableDocumentBinds: function () {
|
||||
$(document).unbind('keydown', this._onDocumentKeyDown);
|
||||
$(document).unbind('click', this._onDocumentClick);
|
||||
},
|
||||
|
||||
_onDocumentKeyDown: function (event) {
|
||||
if (event && event.keyCode === ESC_KEY_CODE) {
|
||||
this._toggleLimits();
|
||||
}
|
||||
},
|
||||
|
||||
_showLimits: function () {
|
||||
this.$el.addClass('is-active');
|
||||
this._enableDocumentBinds();
|
||||
},
|
||||
|
||||
_hideLimits: function () {
|
||||
this.$el.removeClass('is-active');
|
||||
this._disableDocumentBinds();
|
||||
},
|
||||
|
||||
_toggleLimits: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
_onDocumentClick: function (event) {
|
||||
if (!$(event.target).closest(this.el).length) {
|
||||
this._toggleLimits();
|
||||
}
|
||||
},
|
||||
|
||||
_onVisibleChanged: function (_model, isVisible) {
|
||||
isVisible ? this._showLimits() : this._hideLimits();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._disableDocumentBinds();
|
||||
View.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
var _ = require('underscore');
|
||||
var Model = require('../../core/model');
|
||||
var InfowindowModel = require('./infowindow-model');
|
||||
|
||||
var TooltipModel = Model.extend({
|
||||
defaults: {
|
||||
template: '<p>{{text}}</p>',
|
||||
fields: [],
|
||||
alternative_names: {},
|
||||
placement: 'bottom|right',
|
||||
position: { x: 0, y: 0 },
|
||||
offset: [0, 0],
|
||||
blacklisted_columns: ['cartodb_id']
|
||||
},
|
||||
|
||||
updateContent: function (attributes) {
|
||||
var data = attributes;
|
||||
var nonValidKeys = ['fields', 'content'];
|
||||
|
||||
if (this.get('blacklisted_columns')) {
|
||||
nonValidKeys = nonValidKeys.concat(this.get('blacklisted_columns'));
|
||||
}
|
||||
|
||||
var c = InfowindowModel.contentForFields(data, this.get('fields'), {
|
||||
show_empty_fields: false
|
||||
});
|
||||
|
||||
// Remove fields and content from data
|
||||
// and make them visible for custom templates
|
||||
data.content = _.omit(data, nonValidKeys);
|
||||
|
||||
// loop through content values
|
||||
data.fields = c.fields;
|
||||
|
||||
// alternamte names
|
||||
var names = this.get('alternative_names');
|
||||
if (names) {
|
||||
data.fields.forEach(function (field) {
|
||||
field.title = names[field.title] || field.title;
|
||||
});
|
||||
}
|
||||
|
||||
this.set('content', data);
|
||||
},
|
||||
|
||||
setTooltipTemplate: function (tooltipTemplate) {
|
||||
this.set({
|
||||
template: tooltipTemplate.get('template'),
|
||||
fields: tooltipTemplate.fields.toJSON(),
|
||||
alternative_names: tooltipTemplate.get('alternative_names')
|
||||
});
|
||||
},
|
||||
|
||||
setPosition: function (position) {
|
||||
this.set('position', position);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.set('visible', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.set('visible', false);
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return !!this.get('visible');
|
||||
},
|
||||
|
||||
getVerticalOffset: function () {
|
||||
var offset = this.get('offset');
|
||||
return (offset && offset[1]) || 0;
|
||||
},
|
||||
|
||||
getHorizontalOffset: function () {
|
||||
var offset = this.get('offset');
|
||||
return (offset && offset[0]) || 0;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TooltipModel;
|
||||
@@ -0,0 +1,149 @@
|
||||
var _ = require('underscore');
|
||||
var sanitize = require('../../core/sanitize');
|
||||
var Template = require('../../core/template');
|
||||
var View = require('../../core/view');
|
||||
|
||||
var FADE_IN_DURATION = 200;
|
||||
var FADE_OUT_DURATION = 100;
|
||||
var FADE_TIMEOUT = 50;
|
||||
|
||||
var TooltipView = View.extend({
|
||||
defaultTemplate: '<p>{{text}}</p>',
|
||||
className: 'CDB-Tooltip-wrapper',
|
||||
|
||||
initialize: function (options) {
|
||||
if (!options.mapView) {
|
||||
throw new Error('mapView should be present');
|
||||
}
|
||||
|
||||
this._mapView = options.mapView;
|
||||
|
||||
this.showing = false;
|
||||
this.showhideTimeout = null;
|
||||
|
||||
this.model.bind('change:visible', this._showOrHide, this);
|
||||
this.model.bind('change:position', this._updatePosition, this);
|
||||
this.model.bind('change:placement', this._updatePosition, this);
|
||||
this.model.bind('change:content change:alternative_names', this.render, this);
|
||||
},
|
||||
|
||||
template: function (data) {
|
||||
var compiledTemplate = Template.compile(this.model.get('template'), 'mustache');
|
||||
return compiledTemplate(data);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var content = this.model.get('content');
|
||||
var sanitizedOutput = sanitize.html(this.template(content));
|
||||
this.$el.html(sanitizedOutput);
|
||||
this._updatePosition();
|
||||
return this;
|
||||
},
|
||||
|
||||
_showOrHide: function () {
|
||||
if (this.model.isVisible()) {
|
||||
this._show();
|
||||
} else {
|
||||
this._hide();
|
||||
}
|
||||
},
|
||||
|
||||
_hide: function () {
|
||||
var self = this;
|
||||
var fadeOut = function () {
|
||||
self.$el.fadeOut(FADE_OUT_DURATION);
|
||||
};
|
||||
|
||||
clearTimeout(this.showhideTimeout);
|
||||
this.showhideTimeout = setTimeout(fadeOut, FADE_TIMEOUT);
|
||||
},
|
||||
|
||||
_show: function () {
|
||||
this.render();
|
||||
|
||||
var self = this;
|
||||
var fadeIn = function () {
|
||||
self.$el.fadeIn(FADE_IN_DURATION);
|
||||
};
|
||||
|
||||
clearTimeout(this.showhideTimeout);
|
||||
this.showhideTimeout = setTimeout(fadeIn, FADE_TIMEOUT);
|
||||
},
|
||||
|
||||
_updatePosition: function () {
|
||||
var position = this.model.get('position');
|
||||
var placement = this.model.get('placement');
|
||||
var height = this.$el.innerHeight();
|
||||
var width = this.$el.innerWidth();
|
||||
var mapViewSize = this._mapView.getSize();
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
var modifierClass = 'CDB-Tooltip-wrapper--';
|
||||
|
||||
// Remove any position modifier
|
||||
this._removePositionModifiers();
|
||||
|
||||
// Vertically
|
||||
if (placement.indexOf('top') !== -1) {
|
||||
top = position.y - height;
|
||||
} else if (placement.indexOf('middle') !== -1) {
|
||||
top = position.y - (height / 2);
|
||||
} else { // bottom
|
||||
top = position.y;
|
||||
}
|
||||
|
||||
// Fix vertical overflow
|
||||
if (top < 0) {
|
||||
top = position.y;
|
||||
modifierClass += 'top';
|
||||
} else if (top + height > mapViewSize.y) {
|
||||
top = position.y - height;
|
||||
modifierClass += 'bottom';
|
||||
} else {
|
||||
modifierClass += 'top';
|
||||
}
|
||||
|
||||
// Horizontally
|
||||
if (placement.indexOf('left') !== -1) {
|
||||
left = position.x - width;
|
||||
} else if (placement.indexOf('center') !== -1) {
|
||||
left = position.x - (width / 2);
|
||||
} else { // right
|
||||
left = position.x;
|
||||
}
|
||||
|
||||
// Fix horizontal overflow
|
||||
if (left < 0) {
|
||||
left = position.x;
|
||||
modifierClass += 'Left';
|
||||
} else if (left + width > mapViewSize.x) {
|
||||
left = position.x - width;
|
||||
modifierClass += 'Right';
|
||||
} else {
|
||||
modifierClass += 'Left';
|
||||
}
|
||||
|
||||
// Add offsets
|
||||
top += this.model.getVerticalOffset();
|
||||
left += this.model.getHorizontalOffset();
|
||||
|
||||
this.$el.css({
|
||||
top: top,
|
||||
left: left
|
||||
}).addClass(modifierClass);
|
||||
},
|
||||
|
||||
_removePositionModifiers: function () {
|
||||
var positions = [ 'topLeft', 'topRight', 'bottomRight', 'bottomLeft' ];
|
||||
var positionModifiers = _.map(positions, function (className) {
|
||||
return this._modifierClassName(className);
|
||||
}, this);
|
||||
this.$el.removeClass(positionModifiers.join(' '));
|
||||
},
|
||||
|
||||
_modifierClassName: function (className) {
|
||||
return this.className + '--' + className;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TooltipView;
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="CDB-Overlay">
|
||||
<button class="CDB-Zoom-action CDB-Zoom-action--out js-zoomOut"></button>
|
||||
<div class="CDB-Zoom-info js-zoomInfo">-</div>
|
||||
<button class="CDB-Zoom-action CDB-Zoom-action--in js-zoomIn"></button>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
var View = require('../../../core/view');
|
||||
var template = require('./zoom-template.tpl');
|
||||
|
||||
/**
|
||||
* View to control the zoom of the map.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* var zoomControl = new Zoom({ model: map, template: "" });
|
||||
* mapWrapper.$el.append(zoomControl.render().$el);
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'CDB-Zoom',
|
||||
|
||||
events: {
|
||||
'click .js-zoomIn': '_onZoomIn',
|
||||
'click .js-zoomOut': '_onZoomOut',
|
||||
'dblclick': 'killEvent'
|
||||
},
|
||||
|
||||
options: {
|
||||
timeout: 0,
|
||||
msg: ''
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.map = this.model;
|
||||
this.template = this.options.template || template;
|
||||
this.map.bind('change:zoom change:minZoom change:maxZoom', this._checkZoom, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template(this.options));
|
||||
this._checkZoom();
|
||||
return this;
|
||||
},
|
||||
|
||||
_checkZoom: function () {
|
||||
var zoom = this.map.get('zoom');
|
||||
this.$('.js-zoomIn')[ zoom < this.map.get('maxZoom') ? 'removeClass' : 'addClass' ]('is-disabled');
|
||||
this.$('.js-zoomOut')[ zoom > this.map.get('minZoom') ? 'removeClass' : 'addClass' ]('is-disabled');
|
||||
this.$('.js-zoomInfo').text(zoom);
|
||||
},
|
||||
|
||||
_onZoomIn: function (ev) {
|
||||
this.killEvent(ev);
|
||||
|
||||
if (this.map.get('maxZoom') > this.map.getZoom()) {
|
||||
this.map.setZoom(this.map.getZoom() + 1);
|
||||
}
|
||||
},
|
||||
|
||||
_onZoomOut: function (ev) {
|
||||
this.killEvent(ev);
|
||||
|
||||
if (this.map.get('minZoom') < this.map.getZoom()) {
|
||||
this.map.setZoom(this.map.getZoom() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user