Initial commit
This commit is contained in:
42
lib/assets/javascripts/builder/components/apply-button/apply-button-view.js
Executable file
42
lib/assets/javascripts/builder/components/apply-button/apply-button-view.js
Executable file
@@ -0,0 +1,42 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./apply-button.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'onApplyClick',
|
||||
'overlayModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-apply': '_onClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template({
|
||||
disabled: this._isDisabled()
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._overlayModel, 'change:visible', this.render);
|
||||
},
|
||||
|
||||
_isDisabled: function () {
|
||||
return this._overlayModel.get('visible');
|
||||
},
|
||||
|
||||
_onClick: function () {
|
||||
if (this._isDisabled()) return;
|
||||
|
||||
this._onApplyClick();
|
||||
}
|
||||
});
|
||||
3
lib/assets/javascripts/builder/components/apply-button/apply-button.tpl
Executable file
3
lib/assets/javascripts/builder/components/apply-button/apply-button.tpl
Executable file
@@ -0,0 +1,3 @@
|
||||
<button class="u-lSpace--xl CDB-Button CDB-Button--primary <%- disabled ? 'is-disabled' : '' %> js-apply">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small"><%- _t("editor.infowindow.apply") %></span>
|
||||
</button>
|
||||
@@ -0,0 +1,186 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var Notifier = require('builder/components/notifier/notifier');
|
||||
var UploadConfig = require('builder/config/upload-config');
|
||||
var ErrorDetailsView = require('./error-details-view');
|
||||
var WarningsDetailsView = require('./warnings-details-view');
|
||||
var TwitterImportDetailsDialog = require('./twitter-import-details-view');
|
||||
|
||||
/**
|
||||
* Import item within background importer
|
||||
*
|
||||
*/
|
||||
|
||||
var DELEGATIONS = {
|
||||
loading: require('./delegated-import-views/loading'),
|
||||
error: require('./delegated-import-views/failed'),
|
||||
warning: require('./delegated-import-views/warnings'),
|
||||
success: require('./delegated-import-views/success')
|
||||
};
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.modals) throw new Error('modals is required');
|
||||
if (!opts.importModel) throw new Error('importModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
this._modals = opts.modals;
|
||||
this._importModel = opts.importModel;
|
||||
this._showSuccessDetailsButton = opts.showSuccessDetailsButton;
|
||||
|
||||
this._notification = Notifier.addNotification({});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._importModel.on('change', this.updateNotification, this);
|
||||
this._importModel.on('remove', this.clean, this);
|
||||
this._notification.on('notification:close', this._closeHandler, this);
|
||||
this._notification.on('notification:action', this._actionHandler, this);
|
||||
this.add_related_model(this._importModel);
|
||||
this.add_related_model(this._notification);
|
||||
},
|
||||
|
||||
_closeHandler: function () {
|
||||
this.trigger('remove', this._importModel, this);
|
||||
this._importModel.pause();
|
||||
this.clean();
|
||||
},
|
||||
|
||||
_actionHandler: function (action) {
|
||||
if (action === 'show_errors') {
|
||||
this._showImportError();
|
||||
} else if (action === 'show_stats') {
|
||||
this._showImportStats();
|
||||
} else if (action === 'show_table') {
|
||||
this._showImportDataset();
|
||||
} else if (action === 'show_warnings') {
|
||||
this._showImportWarnings();
|
||||
}
|
||||
},
|
||||
|
||||
_getStatus: function () {
|
||||
var status = 'loading';
|
||||
var failed = this._importModel.hasFailed();
|
||||
var completed = this._importModel.hasCompleted();
|
||||
var warnings = this._importModel.getWarnings();
|
||||
|
||||
if (failed) {
|
||||
status = 'error';
|
||||
} else if (completed && !warnings) {
|
||||
status = 'success';
|
||||
} else if (completed && warnings) {
|
||||
status = 'warning';
|
||||
}
|
||||
|
||||
return status;
|
||||
},
|
||||
|
||||
_getInfo: function () {
|
||||
var upload = this._importModel.get('upload');
|
||||
var imp = this._importModel.get('import');
|
||||
|
||||
var d = {
|
||||
name: '',
|
||||
state: this._importModel.get('state'),
|
||||
progress: '',
|
||||
service: '',
|
||||
step: this._importModel.get('step'),
|
||||
failed: this._importModel.hasFailed(),
|
||||
completed: this._importModel.hasCompleted(),
|
||||
warnings: this._importModel.getWarnings(),
|
||||
showSuccessDetailsButton: this._showSuccessDetailsButton,
|
||||
tables_created_count: imp.tables_created_count
|
||||
};
|
||||
|
||||
// Name
|
||||
if (upload.type) {
|
||||
if (upload.type === 'file') {
|
||||
if (upload.value.length > 1) {
|
||||
d.name = upload.value.length + ' files';
|
||||
} else {
|
||||
d.name = upload.value.name;
|
||||
}
|
||||
}
|
||||
if (upload.type === 'url' || upload.type === 'remote') {
|
||||
d.name = upload.value;
|
||||
}
|
||||
if (upload.type === 'service') {
|
||||
d.name = upload.value && upload.value.filename || '';
|
||||
}
|
||||
if (upload.service_name === 'twitter_search') {
|
||||
d.name = 'Twitter import';
|
||||
}
|
||||
if (upload.type === 'sql') {
|
||||
d.name = 'SQL';
|
||||
}
|
||||
if (upload.type === 'duplication') {
|
||||
d.name = upload.table_name || upload.value;
|
||||
}
|
||||
} else {
|
||||
d.name = imp.display_name || imp.item_queue_id || 'import';
|
||||
}
|
||||
|
||||
// Service
|
||||
d.service = upload.service_name;
|
||||
|
||||
// Progress
|
||||
if (this._importModel.get('step') === 'upload') {
|
||||
d.progress = this._importModel.get('upload').progress;
|
||||
} else {
|
||||
d.progress = Math.max(0, (UploadConfig.uploadStates.indexOf(d.state) / UploadConfig.uploadStates.length) * 100);
|
||||
}
|
||||
|
||||
d.progress = d.progress.toFixed(0);
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
updateNotification: function () {
|
||||
var status = this._getStatus();
|
||||
var delegated = DELEGATIONS[status];
|
||||
delegated.call(this, this._importModel, this._notification, status, this._getInfo(), this._showSuccessDetailsButton);
|
||||
},
|
||||
|
||||
_showImportDataset: function () {
|
||||
var dataset = this._importModel.get('import').table_name;
|
||||
window.location = this._configModel.get('base_url') + '/dataset/' + dataset;
|
||||
},
|
||||
|
||||
_showImportStats: function () {
|
||||
var self = this;
|
||||
this._modals.create(function (modalModel) {
|
||||
return new TwitterImportDetailsDialog({
|
||||
modalModel: modalModel,
|
||||
userModel: self._userModel,
|
||||
model: self._importModel
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_showImportError: function () {
|
||||
var self = this;
|
||||
this._modals.create(function (modalModel) {
|
||||
return new ErrorDetailsView({
|
||||
configModel: self._configModel,
|
||||
modalModel: modalModel,
|
||||
error: self._importModel.getError(),
|
||||
userModel: self._userModel
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_showImportWarnings: function () {
|
||||
var self = this;
|
||||
this._modals.create(function (modalModel) {
|
||||
return new WarningsDetailsView({
|
||||
modalModel: modalModel,
|
||||
warnings: self._importModel.getWarnings(),
|
||||
userModel: self._userModel
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<% if (failed) { %>
|
||||
<%- _t('components.background-importer.background-importer-item.error-connecting', { name: name }) %> <% if (service) { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
<% } else if (completed && !warnings) { %>
|
||||
<%- name %> <% if (service && service != "twitter_search") { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %> <%- _t('components.background-importer.background-importer-item.completed') %>!
|
||||
<% } else if (completed && warnings) { %>
|
||||
Some warnings were produced for <%- name %> <% if (service) { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
<% } else { %>
|
||||
<%- progress %>% <%- state %> <%- name %> <% if (service && service != "twitter_search") { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,47 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./background-import-limit.tpl');
|
||||
var Notifier = require('builder/components/notifier/notifier');
|
||||
|
||||
/**
|
||||
* Import limit message within background importer
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._notification = Notifier.addNotification({
|
||||
status: 'error',
|
||||
closable: true,
|
||||
button: false,
|
||||
info: this._getInfo()
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._notification.on('notification:close', this._closeHandler, this);
|
||||
this.add_related_model(this._notification);
|
||||
},
|
||||
|
||||
_closeHandler: function () {
|
||||
this.clean();
|
||||
},
|
||||
|
||||
_getInfo: function () {
|
||||
var importQuota = this._userModel.getMaxConcurrentImports();
|
||||
var isUpgradeable = !this._configModel.get('cartodb_com_hosted') && importQuota === 1;
|
||||
|
||||
return template({
|
||||
upgradeUrl: window.upgrade_url,
|
||||
isUpgradeable: isUpgradeable,
|
||||
importQuota: importQuota
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<% if (isUpgradeable) { %>
|
||||
<%- _t('components.background-importer.background-import-limit.hurry', { upgradeUrl: upgradeUrl }) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.background-importer.background-import-limit.one-file', { importQuota: importQuota }) %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,161 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var ImportItemView = require('./background-import-item-view');
|
||||
var ImportLimitItemView = require('./background-import-limit-view');
|
||||
var ImportsModel = require('builder/data/background-importer/imports-model');
|
||||
|
||||
/**
|
||||
* Background polling manager
|
||||
*
|
||||
* It will pool all polling operations (imports) that happens
|
||||
* in the Builder
|
||||
*
|
||||
*/
|
||||
|
||||
var BackgroundImporter = function (options) {
|
||||
this.options = options || {};
|
||||
this._importers = {};
|
||||
this.initialize(this.options);
|
||||
};
|
||||
|
||||
BackgroundImporter.prototype = {
|
||||
initialize: function (opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.modals) throw new Error('modals is required');
|
||||
if (!opts.pollingModel) throw new Error('pollingModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
this._createVis = opts.createVis;
|
||||
this._modals = opts.modals;
|
||||
this._model = opts.pollingModel;
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
addView: function (view) {
|
||||
this._importers[view.cid] = view;
|
||||
},
|
||||
|
||||
removeView: function (view) {
|
||||
delete this._importers[view.cid];
|
||||
},
|
||||
|
||||
enable: function () {
|
||||
this._model.startPollings();
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
this._model.stopPollings();
|
||||
},
|
||||
|
||||
removeImport: function (model) {
|
||||
this._model.removeImportItem(model);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this._model.off(null, null, this);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._model.on('importAdded', this._addImport, this);
|
||||
this._model.on('importByUploadData', this._addDataset, this);
|
||||
},
|
||||
|
||||
_addImport: function (m) {
|
||||
var importItemView = new ImportItemView({
|
||||
configModel: this._configModel,
|
||||
showSuccessDetailsButton: this._model.get('showSuccessDetailsButton'),
|
||||
modals: this._modals,
|
||||
importModel: m,
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
importItemView.on('remove', this._removeImport, this);
|
||||
this.addView(importItemView);
|
||||
this.enable();
|
||||
},
|
||||
|
||||
_addDataset: function (d) {
|
||||
if (d) {
|
||||
this._addImportsItem(d);
|
||||
}
|
||||
},
|
||||
|
||||
_onDroppedFile: function (files) {
|
||||
if (files) {
|
||||
this._addImportsItem({
|
||||
type: 'file',
|
||||
value: files,
|
||||
create_vis: this._createVis
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_addImportsItem: function (uploadData) {
|
||||
if (this._model.canAddImport()) {
|
||||
this._removeLimitItem();
|
||||
} else {
|
||||
this._addLimitItem();
|
||||
return false;
|
||||
}
|
||||
|
||||
var imp = new ImportsModel({}, {
|
||||
upload: uploadData,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
this._model.addImportItem(imp);
|
||||
},
|
||||
|
||||
_addLimitItem: function () {
|
||||
if (!this._importLimit) {
|
||||
var view = new ImportLimitItemView({
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
this.addView(view);
|
||||
this._importLimit = view;
|
||||
}
|
||||
},
|
||||
|
||||
_removeLimitItem: function () {
|
||||
var view = this._importLimit;
|
||||
if (view) {
|
||||
view.clean();
|
||||
this.removeView(view);
|
||||
delete this._importLimit;
|
||||
}
|
||||
},
|
||||
|
||||
_removeImport: function (model, view) {
|
||||
this._model.removeImportItem(model);
|
||||
this.removeView(view);
|
||||
}
|
||||
};
|
||||
|
||||
// Supporting default Backbone events like on, off, trigger, listenTo etc
|
||||
_.extend(BackgroundImporter.prototype, Backbone.Events, {
|
||||
remove: function () {
|
||||
this.stopListening();
|
||||
}
|
||||
});
|
||||
|
||||
var manager = (function () {
|
||||
var initialized = false;
|
||||
var importer;
|
||||
|
||||
return {
|
||||
init: function (opts) {
|
||||
if (!initialized) {
|
||||
importer = new BackgroundImporter(opts);
|
||||
}
|
||||
return importer;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = manager;
|
||||
@@ -0,0 +1,34 @@
|
||||
var template = require('./failed.tpl');
|
||||
|
||||
function getAction () {
|
||||
return 'show_errors';
|
||||
}
|
||||
|
||||
function getButton () {
|
||||
return _t('components.background-importer.background-importer-item.show');
|
||||
}
|
||||
|
||||
function getClosable () {
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateNotification (info) {
|
||||
var data = {
|
||||
info: info,
|
||||
status: _status,
|
||||
closable: getClosable(),
|
||||
button: getButton(),
|
||||
action: getAction()
|
||||
};
|
||||
|
||||
_notification.update(data);
|
||||
}
|
||||
|
||||
var _notification;
|
||||
var _status;
|
||||
|
||||
module.exports = function (model, notification, status, info, showDetails) {
|
||||
_notification = notification;
|
||||
_status = status;
|
||||
updateNotification(template(info));
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<%= _t('components.background-importer.background-importer-item.error-connecting', { name: name }) %> <% if (service) { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
@@ -0,0 +1,38 @@
|
||||
var template = require('./loading.tpl');
|
||||
|
||||
function getAction () {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getButton () {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getClosable () {
|
||||
var state = _model.get('state');
|
||||
var step = _model.get('step');
|
||||
return state === 'uploading' && step === 'upload';
|
||||
}
|
||||
|
||||
function updateNotification (info) {
|
||||
var data = {
|
||||
info: info,
|
||||
status: _status,
|
||||
closable: getClosable(),
|
||||
button: getButton(),
|
||||
action: getAction()
|
||||
};
|
||||
|
||||
_notification.update(data);
|
||||
}
|
||||
|
||||
var _model;
|
||||
var _notification;
|
||||
var _status;
|
||||
|
||||
module.exports = function (model, notification, status, info, showDetails) {
|
||||
_model = model;
|
||||
_notification = notification;
|
||||
_status = status;
|
||||
updateNotification(template(info));
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<span class='CDB-Text is-semibold'><%- progress %>% <%- state %></span> <%- name %> <% if (service && service != "twitter_search") { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
@@ -0,0 +1,60 @@
|
||||
var template = require('./success.tpl');
|
||||
|
||||
function getAction () {
|
||||
var service = _model.get('upload').service_name;
|
||||
var tables_created_count = _model.get('import').tables_created_count;
|
||||
|
||||
if (_showDetails) {
|
||||
if (service && service === 'twitter_search') {
|
||||
return 'show_stats';
|
||||
} else if (tables_created_count === 1) {
|
||||
return 'show_table';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getButton () {
|
||||
var service = _model.get('upload').service_name;
|
||||
var tables_created_count = _model.get('import').tables_created_count;
|
||||
|
||||
if (_showDetails) {
|
||||
if (service && service === 'twitter_search') {
|
||||
return _t('components.background-importer.background-importer-item.show');
|
||||
} else if (tables_created_count === 1) {
|
||||
return _t('components.background-importer.background-importer-item.show');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getClosable () {
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateNotification (info) {
|
||||
var data = {
|
||||
info: info,
|
||||
status: _status,
|
||||
closable: getClosable(),
|
||||
button: getButton(),
|
||||
action: getAction()
|
||||
};
|
||||
|
||||
_notification.update(data);
|
||||
}
|
||||
|
||||
var _model;
|
||||
var _notification;
|
||||
var _status;
|
||||
var _showDetails;
|
||||
|
||||
module.exports = function (model, notification, status, info, showDetails) {
|
||||
_model = model;
|
||||
_notification = notification;
|
||||
_status = status;
|
||||
_showDetails = showDetails;
|
||||
updateNotification(template(info));
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<%- name %> <% if (service && service != "twitter_search") { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %> <span class='CDB-Text is-semibold'><%- _t('components.background-importer.background-importer-item.completed') %></span>
|
||||
@@ -0,0 +1,34 @@
|
||||
var template = require('./warnings.tpl');
|
||||
|
||||
function getAction () {
|
||||
return 'show_warnings';
|
||||
}
|
||||
|
||||
function getButton () {
|
||||
return _t('components.background-importer.background-importer-item.show');
|
||||
}
|
||||
|
||||
function getClosable () {
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateNotification (info) {
|
||||
var data = {
|
||||
info: info,
|
||||
status: _status,
|
||||
closable: getClosable(),
|
||||
button: getButton(),
|
||||
action: getAction()
|
||||
};
|
||||
|
||||
_notification.update(data);
|
||||
}
|
||||
|
||||
var _notification;
|
||||
var _status;
|
||||
|
||||
module.exports = function (model, notification, status, info, showDetails) {
|
||||
_notification = notification;
|
||||
_status = status;
|
||||
updateNotification(template(info));
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<span class='CDB-Text is-semibold'>Some warnings</span> were produced for <%- name %> <% if (service) { %> <%- _t('components.background-importer.background-importer-item.from') %> <%- service %> <% } %>
|
||||
@@ -0,0 +1,55 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var defaultTemplate = require('./error-details.tpl');
|
||||
var upgradeErrorTemplate = require('./upgrade-errors.tpl');
|
||||
var UPGRADE_ERROR_CODES = [8001, 8002, 8005, 8007];
|
||||
|
||||
/**
|
||||
* Error details view, to be used together with an error object from an import model.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.error) throw new Error('error is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
this._error = opts.error;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var isUpgradeError = _.contains(UPGRADE_ERROR_CODES, this._error.errorCode);
|
||||
var upgradeUrl = this._configModel.get('upgrade_url');
|
||||
var userCanUpgrade = upgradeUrl && !this._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
|
||||
var template = this._getTemplate(isUpgradeError);
|
||||
|
||||
var html = template({
|
||||
errorCode: this._error.errorCode,
|
||||
title: this._error.title,
|
||||
text: this._error.what_about,
|
||||
itemQueueId: this._error.item_queue_id,
|
||||
originalUrl: this._error.originalUrl,
|
||||
httpResponseCode: this._error.httpResponseCode,
|
||||
httpResponseCodeMessage: this._error.httpRresponseCodeMessage,
|
||||
userCanUpgrade: userCanUpgrade,
|
||||
showTrial: this._userModel.canStartTrial(),
|
||||
upgradeUrl: upgradeUrl
|
||||
});
|
||||
|
||||
this.$el.html(html);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_getTemplate: function (isUpgradeError) {
|
||||
var template = defaultTemplate;
|
||||
|
||||
if (isUpgradeError) {
|
||||
template = upgradeErrorTemplate;
|
||||
}
|
||||
return template;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="Dialog-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--negative u-flex u-alignCenter u-justifyCenter">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloud"></i>
|
||||
</div>
|
||||
<h2 class="CDB-Text CDB-Size-large u-bSpace u-errorTextColor">
|
||||
<%- title %> <% if (errorCode) { %>(<%- errorCode %>)<% } %>
|
||||
</h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-secondaryTextColor">
|
||||
<% if (itemQueueId) { %>
|
||||
<%- _t('components.background-importer.error-details.dont-panic') %>
|
||||
<% } else { %>
|
||||
<%- _t('components.background-importer.error-details.check-errors') %>
|
||||
<% } %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-body ErrorDetails-body">
|
||||
<ul class="Modal-containerList">
|
||||
<% if (httpResponseCode) { %>
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemStep CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-justifyCenter">1</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-medium">
|
||||
<%= _t('components.background-importer.error-details.remote-server-code', { httpResponseCode: httpResponseCode}) %> <%- httpResponseCodeMessage %>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemStep CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-justifyCenter">2</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-medium">
|
||||
<%- _t('components.background-importer.error-details.check-url') %>:<br/>
|
||||
</p>
|
||||
<span class='CDB-Text CDB-Size-medium ErrorDetails-itemTextStrong'><a href="<%- originalUrl %>"><%- originalUrl %></a></span>
|
||||
</div>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemStep CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-justifyCenter">1</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-medium">
|
||||
<% if (text) { %>
|
||||
<%= cdb.core.sanitize.html(text) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.background-importer.error-details.unknown-error') %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
<% if (itemQueueId) { %>
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemStep CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-justifyCenter">!</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-medium">
|
||||
<%= _t('components.background-importer.error-details.send-us-the-error-code') %>:<br/>
|
||||
</p>
|
||||
<span class="CDB-Text CDB-Size-medium ErrorDetails-itemTextStrong"><%- itemQueueId %></span>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Button CDB-Button--error u-tSpace--m js-close">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
|
||||
<%- _t('components.background-importer.error-details.close') %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
var template = require('./twitter-import-details.tpl');
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* When a Twitter import finishes, this dialog displays
|
||||
* all the info about the price/cost etc.
|
||||
*
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'TwitterImportDetails',
|
||||
|
||||
events: {
|
||||
'click .js-close': '_onCloseClick'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var imp = this.model.get('import');
|
||||
var userTwitterValues = this._userModel.get('twitter');
|
||||
var availableTweets = userTwitterValues.quota - userTwitterValues.monthly_use;
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
datasetTotalRows: imp.tweets_georeferenced,
|
||||
datasetTotalRowsFormatted: Utils.formatNumber(imp.tweets_georeferenced),
|
||||
tweetsCost: imp.tweets_cost,
|
||||
tweetsCostFormatted: Utils.formatNumber(imp.tweets_cost),
|
||||
availableTweets: availableTweets,
|
||||
availableTweetsFormatted: Utils.formatNumber(availableTweets),
|
||||
tweetsOverquota: imp.tweets_overquota,
|
||||
tweetsOverquotaFormatted: Utils.formatNumber(imp.tweets_overquota),
|
||||
blockSizeFormatted: Utils.formatNumber(userTwitterValues.block_size),
|
||||
blockPriceFormatted: Utils.formatNumber(userTwitterValues.block_price)
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onCloseClick: function () {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="Dialog-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--twitter u-flex u-alignCenter u-justifyCenter">
|
||||
<i class="CDB-IconFont CDB-IconFont-twitter"></i>
|
||||
</div>
|
||||
<h2 class="CDB-Text CDB-Size-large u-bSpace Dialog-headerIcon--twitter">
|
||||
<%- _t('components.background-importer.twitter-import-details.twitter-import-title') %>
|
||||
</h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<% if (datasetTotalRows === 0) { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.errors.no-results') %>
|
||||
<% } else { %>
|
||||
<%= _t('components.background-importer.twitter-import-details.new-type-created', { datasetTotalRowsFormatted: datasetTotalRowsFormatted, tweetPlural: datasetTotalRows != 1 ? 's' : '' }) %>
|
||||
<% } %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-body ErrorDetails-body">
|
||||
<ul class="Modal-containerList">
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemIcon ErrorDetails-itemIcon--success CDB-Size-big u-flex u-alignCenter u-justifyCenter u-rSpace--xl">
|
||||
<i class="CDB-IconFont CDB-IconFont-dollar"></i>
|
||||
</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-large u-secondaryTextColor">
|
||||
<% if (tweetsCost > 0) { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.tweet-cost.paid', { tweetsCostFormatted: tweetsCostFormatted }) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.tweet-cost.free', { tweetsCostFormatted: tweetsCostFormatted }) %>
|
||||
<% } %>
|
||||
</p>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<% if (tweetsCost > 0 || availableTweets <= 0) { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.no-more-credits', { blockPriceFormatted: blockPriceFormatted, blockSizeFormatted: blockSizeFormatted }) %>
|
||||
<% } else { %>
|
||||
<% if (availableTweets != 1) { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.credits-left', { availableTweetsFormatted: availableTweetsFormatted }) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.background-importer.twitter-import-details.credit-left', { availableTweetsFormatted: availableTweetsFormatted }) %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Button CDB-Button--primary u-tSpace--m">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase js-close">
|
||||
<%- _t('components.background-importer.error-details.close') %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="Dialog-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--negative u-flex u-alignCenter u-justifyCenter">
|
||||
<i class="CDB-IconFont CDB-IconFont-barometer"></i>
|
||||
</div>
|
||||
<h2 class="CDB-Text CDB-Size-large u-bSpace u-errorTextColor">
|
||||
<%- _t('components.background-importer.upgrade-errors.' + errorCode + '.title') %>
|
||||
</h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-secondaryTextColor">
|
||||
<%- _t('components.background-importer.upgrade-errors.' + errorCode + '.description') %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-body ErrorDetails-body">
|
||||
<ul class="Modal-containerList">
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemIcon ErrorDetails-itemIcon--success CDB-Size-big u-flex u-alignCenter u-justifyCenter u-rSpace--xl">
|
||||
<i class="CDB-IconFont CDB-IconFont-rocket"></i>
|
||||
</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<p class="CDB-Text CDB-Size-medium">
|
||||
<%- _t('components.background-importer.upgrade-errors.' + errorCode + '.info') %>
|
||||
<% if (showTrial) { %>
|
||||
<br/>
|
||||
<a href="<%= upgradeUrl %>"><%- _t('components.background-importer.free-trial', { days: 14 }) %></a>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer--simple u-inner">
|
||||
<a href="<%- upgradeUrl %>" class="CDB-Button CDB-Button--primary u-tSpace--m">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
|
||||
<%- _t('components.background-importer.upgrade-errors.upgrade') %>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="Dialog-header ErrorDetails-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--alert">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloud"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle--warning">
|
||||
<%- _t('components.background-importer.partial-import-details.unable-to-import-as-layers') %>
|
||||
</p>
|
||||
<p class="Dialog-headerText">
|
||||
<%- _t('components.background-importer.partial-import-details.find-connected-datasets') %></br>
|
||||
<%- _t('components.background-importer.partial-import-details.upgrade-your-account', { userMaxLayers: userMaxLayers }) %></br>
|
||||
</p>
|
||||
</div>
|
||||
<% if (maxTablesPerImport) { %>
|
||||
<div class="Dialog-body ErrorDetails-body">
|
||||
<ul class="ErrorDetails-list">
|
||||
<li class="ErrorDetails-item">
|
||||
<div class="ErrorDetails-itemStep">!</div>
|
||||
<div class="ErrorDetails-itemText">
|
||||
<%- _t('components.background-importer.partial-import-details.too-many-datasets', { maxTablesPerImport: maxTablesPerImport }) %></br>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="Dialog-footer ErrorDetails-footer">
|
||||
<button class="Button Button--secondary ErrorDetails-footerButton js-close">
|
||||
<span><%- _t('components.background-importer.partial-import-details.continue-btn') %></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="Dialog-header ErrorDetails-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--alert">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloud"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle--warning">
|
||||
<%- _t('components.background-importer.warnings-details.unable-to-import-datasets') %>
|
||||
</p>
|
||||
<p class="Dialog-headerText">
|
||||
<%- _t('components.background-importer.warnings-details.no-more-datasets', { maxTablesPerImport: maxTablesPerImport}) %><br />
|
||||
<%- _t('components.background-importer.warnings-details.find-connected-datasets') %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="Dialog-footer ErrorDetails-footer">
|
||||
<button class="Button Button--secondary ErrorDetails-footerButton u-upperCase js-close">
|
||||
<span><%- _t('components.background-importer.warnings-details.continue-btn') %></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="Dialog-header ErrorDetails-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--alert">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloud"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle--warning">
|
||||
<%- _t('components.background-importer.connector-warning-details.too-many-rows') %>
|
||||
</p>
|
||||
<p class="Dialog-headerText">
|
||||
<%- _t('components.background-importer.connector-warning-details.unable-to-import-all-rows', { maxRowsPerConnectorImport: maxRowsPerConnectorImport}) %><br />
|
||||
</p>
|
||||
</div>
|
||||
<div class="Dialog-footer ErrorDetails-footer">
|
||||
<button class="Button Button--secondary ErrorDetails-footerButton u-upperCase js-close">
|
||||
<span><%- _t('components.background-importer.connector-warning-details.continue-btn') %></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var partialImportDetailsTemplate = require('./warning-partial-import-details.tpl');
|
||||
var tooManyFilesDetailsTemplate = require('./warning-too-many-files-details.tpl');
|
||||
var tooManyRowsConnectorTemplate = require('./warning-too-many-rows-connector-details.tpl');
|
||||
|
||||
/**
|
||||
* Error details view, to be used together with an error object from an import model.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.warnings) throw new Error('warnings is required');
|
||||
|
||||
this._userModel = this.options.userModel;
|
||||
this._warnings = this.options.warnings;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var warnings = this._warnings;
|
||||
var template = this._getTemplate(warnings);
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
userMaxLayers: warnings.user_max_layers,
|
||||
maxTablesPerImport: warnings.max_tables_per_import,
|
||||
maxRowsPerConnectorImport: warnings.max_rows_per_connection
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_getTemplate: function (warnings) {
|
||||
if (warnings.user_max_layers && warnings.max_tables_per_import) {
|
||||
return (warnings.user_max_layers < warnings.max_tables_per_import) ? partialImportDetailsTemplate : tooManyFilesDetailsTemplate;
|
||||
} else if (warnings.user_max_layers) {
|
||||
return partialImportDetailsTemplate;
|
||||
} else if (warnings.max_tables_per_import) {
|
||||
return tooManyFilesDetailsTemplate;
|
||||
} else if (warnings.max_rows_per_connection) {
|
||||
return tooManyRowsConnectorTemplate;
|
||||
}
|
||||
}
|
||||
});
|
||||
59
lib/assets/javascripts/builder/components/carousel-form-view.js
Executable file
59
lib/assets/javascripts/builder/components/carousel-form-view.js
Executable file
@@ -0,0 +1,59 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CarouselView = require('./custom-carousel/custom-carousel-view');
|
||||
|
||||
/**
|
||||
* Carousel form view
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components:carousel-form-view',
|
||||
|
||||
className: 'js-aggregationTypes',
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.collection) throw new Error('Carousel collection is required');
|
||||
if (!opts.template) throw new Error('template is required');
|
||||
this.template = opts.template;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var selectedItem = this.collection.getSelected();
|
||||
var selectedName = selectedItem && selectedItem.getName();
|
||||
this.$el.html(
|
||||
this.template({
|
||||
name: selectedName
|
||||
})
|
||||
);
|
||||
this._initViews();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.collection.bind('change:highlighted', this._onChangeHighlighted, this);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var carousel = new CarouselView(_.extend(this.options, {
|
||||
collection: this.collection
|
||||
}));
|
||||
|
||||
if (!this.$('.js-selector').length) throw new Error('HTML element with js-selector class is required');
|
||||
|
||||
this.$('.js-selector').append(carousel.render().el);
|
||||
carousel.initScroll();
|
||||
this.addView(carousel);
|
||||
},
|
||||
|
||||
_onChangeHighlighted: function () {
|
||||
var item = this.collection.getHighlighted() || this.collection.getSelected();
|
||||
if (item) {
|
||||
var $el = this.$('.js-highlight');
|
||||
if ($el) {
|
||||
$el.text(item.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
315
lib/assets/javascripts/builder/components/code-mirror/cartocss.code-mirror.js
Executable file
315
lib/assets/javascripts/builder/components/code-mirror/cartocss.code-mirror.js
Executable file
@@ -0,0 +1,315 @@
|
||||
var COLOR_KEYWORDS = require('builder/helpers/color-keywords');
|
||||
|
||||
/*
|
||||
LESS mode - http://www.lesscss.org/
|
||||
Ported to CodeMirror by Peter Kroon <plakroon@gmail.com>
|
||||
Report bugs/issues here: https://github.com/marijnh/CodeMirror/issues GitHub: @peterkroon
|
||||
*/
|
||||
|
||||
module.exports = function (CodeMirror) {
|
||||
CodeMirror.defineMode('cartocss', function (config) {
|
||||
var indentUnit = config.indentUnit;
|
||||
var type;
|
||||
|
||||
function ret (style, tp) {
|
||||
type = tp;
|
||||
return style;
|
||||
}
|
||||
// html tags
|
||||
var tags = 'a abbr acronym address applet area article aside audio b base basefont bdi bdo big blockquote body br button canvas caption cite code col colgroup command datalist dd del details dfn dir div dl dt em embed fieldset figcaption figure font footer form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins keygen kbd label legend li link map mark menu meta meter nav noframes noscript object ol optgroup option output p param pre progress q rp rt ruby s samp script section select small source span strike strong style sub summary sup table tbody td textarea tfoot th thead time title tr track tt u ul var video wbr'.split(' ');
|
||||
var colorKeywords = keySet(COLOR_KEYWORDS);
|
||||
|
||||
function inTagsArray (val) {
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
if (val === tags[i]) return true;
|
||||
}
|
||||
}
|
||||
|
||||
var selectors = /(^\:root$|^\:nth\-child$|^\:nth\-last\-child$|^\:nth\-of\-type$|^\:nth\-last\-of\-type$|^\:first\-child$|^\:last\-child$|^\:first\-of\-type$|^\:last\-of\-type$|^\:only\-child$|^\:only\-of\-type$|^\:empty$|^\:link|^\:visited$|^\:active$|^\:hover$|^\:focus$|^\:target$|^\:lang$|^\:enabled^\:disabled$|^\:checked$|^\:first\-line$|^\:first\-letter$|^\:before$|^\:after$|^\:not$|^\:required$|^\:invalid$)/;
|
||||
|
||||
function tokenBase (stream, state) {
|
||||
var ch = stream.next();
|
||||
|
||||
if (ch === '@') {
|
||||
stream.eatWhile(/[\w\-]/);
|
||||
return ret('meta', stream.current());
|
||||
} else if (ch === '/' && stream.eat('*')) {
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
} else if (ch === '<' && stream.eat('!')) {
|
||||
state.tokenize = tokenSGMLComment;
|
||||
return tokenSGMLComment(stream, state);
|
||||
} else if (ch === '=') ret(null, 'compare');
|
||||
else if (ch === '|' && stream.eat('=')) return ret(null, 'compare');
|
||||
else if (ch === '\'' || ch === '\'') {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
} else if (ch === '/') { // e.g.: .png will not be parsed as a class
|
||||
if (stream.eat('/')) {
|
||||
state.tokenize = tokenSComment;
|
||||
return tokenSComment(stream, state);
|
||||
} else {
|
||||
if (type === 'string' || type === '(') {
|
||||
return ret('string', 'string');
|
||||
}
|
||||
if (state.stack[state.stack.length - 1] !== undefined) {
|
||||
return ret(null, ch);
|
||||
}
|
||||
stream.eatWhile(/[\a-zA-Z0-9\-_.\s]/);
|
||||
if (/\/|\)|#/.test(stream.peek() || (stream.eatSpace() && stream.peek() === ')')) || stream.eol()) {
|
||||
return ret('string', 'string'); // let url(/images/logo.png) without quotes return as string
|
||||
}
|
||||
}
|
||||
} else if (ch === '!') {
|
||||
stream.match(/^\s*\w*/);
|
||||
return ret('keyword', 'important');
|
||||
} else if (/\d/.test(ch)) {
|
||||
stream.eatWhile(/[\w.%]/);
|
||||
return ret('number', 'unit');
|
||||
} else if (/[,+<>*\/]/.test(ch)) {
|
||||
if (stream.peek() === '=' || type === 'a') {
|
||||
return ret('string', 'string');
|
||||
}
|
||||
return ret(null, 'select-op');
|
||||
} else if (/[;{}:\[\]()~\|]/.test(ch)) {
|
||||
if (ch === ':') {
|
||||
stream.eatWhile(/[a-z\\\-]/);
|
||||
|
||||
if (selectors.test(stream.current())) {
|
||||
return ret('tag', 'tag');
|
||||
} else if (stream.peek() === ':') { // ::-webkit-search-decoration
|
||||
stream.next();
|
||||
stream.eatWhile(/[a-z\\\-]/);
|
||||
if (stream.current().match(/\:\:\-(o|ms|moz|webkit)\-/)) {
|
||||
return ret('string', 'string');
|
||||
}
|
||||
if (selectors.test(stream.current().substring(1))) {
|
||||
return ret('tag', 'tag');
|
||||
}
|
||||
return ret(null, ch);
|
||||
} else {
|
||||
return ret(null, ch);
|
||||
}
|
||||
} else if (ch === '~') {
|
||||
if (type === 'r') {
|
||||
return ret('string', 'string');
|
||||
}
|
||||
} else {
|
||||
return ret(null, ch);
|
||||
}
|
||||
} else if (ch === '.') {
|
||||
if (type === '(' || type === 'string') {
|
||||
return ret('string', 'string'); // allow url(../image.png)
|
||||
}
|
||||
stream.eatWhile(/[\a-zA-Z0-9\-_]/);
|
||||
if (stream.peek() === ' ') {
|
||||
stream.eatSpace();
|
||||
}
|
||||
if (stream.peek() === ')') {
|
||||
return ret('number', 'unit'); // rgba(0,0,0,.25);
|
||||
}
|
||||
return ret('tag', 'tag');
|
||||
} else if (ch === '#') {
|
||||
// we don't eat white-space, we want the hex color and or id only
|
||||
stream.eatWhile(/[A-Za-z0-9]/);
|
||||
// check if there is a proper hex color length e.g. #eee || #eeeEEE
|
||||
if (stream.current().length === 4 || stream.current().length === 7) {
|
||||
if (stream.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/, false) != null) { // is there a valid hex color value present in the current stream
|
||||
// when not a valid hex value, parse as id
|
||||
if (stream.current().substring(1) !== stream.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/, false)[0]) {
|
||||
return ret('atom', 'tag');
|
||||
}
|
||||
// eat white-space
|
||||
stream.eatSpace();
|
||||
// when hex value declaration doesn't end with [;,] but is does with a slash/cc comment treat it as an id, just like the other hex values that don't end with[;,]
|
||||
if (/[\/<>.({!$%^&*_\-\\?=+\|#'~`]/.test(stream.peek())) {
|
||||
return ret('atom', 'tag');
|
||||
} else if (stream.peek() === '}') {
|
||||
// #time { color: #aaa }
|
||||
return ret('color', 'unit');
|
||||
} else if (/[a-zA-Z\\]/.test(stream.peek())) {
|
||||
// we have a valid hex color value, parse as id whenever an element/class is defined after the hex(id) value e.g. #eee aaa || #eee .aaa
|
||||
return ret('color', 'unit');
|
||||
} else if (stream.eol()) {
|
||||
// when a hex value is on the end of a line, parse as id
|
||||
return ret('color', 'unit');
|
||||
} else {
|
||||
// default
|
||||
return ret('color', 'unit');
|
||||
}
|
||||
} else { // when not a valid hexvalue in the current stream e.g. #footer
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret('atom', 'tag');
|
||||
}
|
||||
} else { // when not a valid hexvalue length
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret('atom', 'tag');
|
||||
}
|
||||
} else if (ch === '&') {
|
||||
stream.eatWhile(/[\w\-]/);
|
||||
return ret(null, ch);
|
||||
} else {
|
||||
stream.eatWhile(/[\w\\\-_%.{]/);
|
||||
if (type === 'string') {
|
||||
return ret('string', 'string');
|
||||
} else if (stream.current().match(/(^http$|^https$)/) != null) {
|
||||
stream.eatWhile(/[\w\\\-_%.{:\/]/);
|
||||
return ret('string', 'string');
|
||||
} else if (stream.peek() === '<' || stream.peek() === '>') {
|
||||
return ret('tag', 'tag');
|
||||
} else if (/\(/.test(stream.peek())) {
|
||||
return ret(null, ch);
|
||||
} else if (stream.peek() === '/' && state.stack[state.stack.length - 1] !== undefined) { // url(dir/center/image.png)
|
||||
return ret('string', 'string');
|
||||
} else if (stream.current().match(/\-\d|\-.\d/)) { // match e.g.: -5px -0.4 etc... only colorize the minus sign
|
||||
// commment out these 2 comment if you want the minus sign to be parsed as null -500px
|
||||
// stream.backUp(stream.current().length-1);
|
||||
// return ret(null, ch); //console.log( stream.current() );
|
||||
return ret('number', 'unit');
|
||||
} else if (inTagsArray(stream.current().toLowerCase())) { // match html tags
|
||||
return ret('tag', 'tag');
|
||||
} else if (/\/|[\s\)]/.test(stream.peek() || stream.eol() || (stream.eatSpace() && stream.peek() === '/')) && stream.current().indexOf('.') !== -1) {
|
||||
if (stream.current().substring(stream.current().length - 1, stream.current().length) === '{') {
|
||||
stream.backUp(1);
|
||||
return ret('tag', 'tag');
|
||||
} // end if
|
||||
stream.eatSpace();
|
||||
if (/[{<>.a-zA-Z\/]/.test(stream.peek()) || stream.eol()) return ret('tag', 'tag'); // e.g. button.icon-plus
|
||||
return ret('string', 'string'); // let url(/images/logo.png) without quotes return as string
|
||||
} else if (stream.eol() || stream.peek() === '[' || stream.peek() === '#' || type === 'tag') {
|
||||
if (stream.current().substring(stream.current().length - 1, stream.current().length) === '{') stream.backUp(1);
|
||||
return ret('tag', 'tag');
|
||||
} else if (type === 'compare' || type === 'a' || type === '(') {
|
||||
return ret('string', 'string');
|
||||
} else if (type === '|' || stream.current() === '-' || type === '[') {
|
||||
return ret(null, ch);
|
||||
} else if (stream.peek() === ':') {
|
||||
stream.next();
|
||||
var t_v = stream.peek() === ':';
|
||||
if (!t_v) {
|
||||
var old_pos = stream.pos;
|
||||
var sc = stream.current().length;
|
||||
stream.eatWhile(/[a-z\\\-]/);
|
||||
var new_pos = stream.pos;
|
||||
if (stream.current().substring(sc - 1).match(selectors) != null) {
|
||||
stream.backUp(new_pos - (old_pos - 1));
|
||||
return ret('tag', 'tag');
|
||||
} else stream.backUp(new_pos - (old_pos - 1));
|
||||
} else {
|
||||
stream.backUp(1);
|
||||
}
|
||||
if (t_v) return ret('tag', 'tag');
|
||||
else return ret('variable', 'variable');
|
||||
|
||||
// It is a color variable?
|
||||
} else if (colorKeywords.hasOwnProperty(stream.current())) {
|
||||
return ret('color', 'unit');
|
||||
} else {
|
||||
return ret('variable', 'variable');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keySet (array) {
|
||||
var keys = {};
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
keys[array[i]] = true;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function tokenSComment (stream, state) { // SComment = Slash comment
|
||||
stream.skipToEnd();
|
||||
state.tokenize = tokenBase;
|
||||
return ret('comment', 'comment');
|
||||
}
|
||||
|
||||
function tokenCComment (stream, state) {
|
||||
var maybeEnd = false;
|
||||
var ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (maybeEnd && ch === '/') {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch === '*');
|
||||
}
|
||||
return ret('comment', 'comment');
|
||||
}
|
||||
|
||||
function tokenSGMLComment (stream, state) {
|
||||
var dashes = 0;
|
||||
var ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (dashes >= 2 && ch === '>') {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
dashes = (ch === '-') ? dashes + 1 : 0;
|
||||
}
|
||||
return ret('comment', 'comment');
|
||||
}
|
||||
|
||||
function tokenString (quote) {
|
||||
return function (stream, state) {
|
||||
var escaped = false;
|
||||
var ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch === quote && !escaped) {
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && ch === '\\';
|
||||
}
|
||||
if (!escaped) state.tokenize = tokenBase;
|
||||
return ret('string', 'string');
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
startState: function (base) {
|
||||
return {
|
||||
tokenize: tokenBase,
|
||||
baseIndent: base || 0,
|
||||
stack: []
|
||||
};
|
||||
},
|
||||
|
||||
token: function (stream, state) {
|
||||
if (stream.eatSpace()) return null;
|
||||
var style = state.tokenize(stream, state);
|
||||
|
||||
var context = state.stack[state.stack.length - 1];
|
||||
if (type === 'hash' && context === 'rule') style = 'atom';
|
||||
else if (style === 'variable') {
|
||||
if (context === 'rule') style = null; // 'tag'
|
||||
else if (!context || context === '@media{') {
|
||||
style = stream.current() === 'when' ? 'variable' : /[\s,|\s\)|\s]/.test(stream.peek()) ? 'tag' : type;
|
||||
}
|
||||
}
|
||||
|
||||
if (context === 'rule' && /^[\{\};]$/.test(type)) {
|
||||
state.stack.pop();
|
||||
}
|
||||
if (type === '{') {
|
||||
if (context === '@media') state.stack[state.stack.length - 1] = '@media{';
|
||||
else state.stack.push('{');
|
||||
} else if (type === '}') state.stack.pop();
|
||||
else if (type === '@media') state.stack.push('@media');
|
||||
else if (context === '{' && type !== 'comment') state.stack.push('rule');
|
||||
return style;
|
||||
},
|
||||
|
||||
indent: function (state, textAfter) {
|
||||
var n = state.stack.length;
|
||||
if (/^\}/.test(textAfter)) {
|
||||
n -= state.stack[state.stack.length - 1] === 'rule' ? 2 : 1;
|
||||
}
|
||||
return state.baseIndent + n * indentUnit;
|
||||
},
|
||||
|
||||
electricChars: '}'
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME('text/x-carto', 'cartocss');
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<i class="CodeMirror-bullet"></i>
|
||||
@@ -0,0 +1,5 @@
|
||||
<ul class="CodeMirror-error">
|
||||
<li class="CodeMirror-errorMessage u-lSpace--xl u-rSpace--xl">
|
||||
<%- _t('components.codemirror.syntax-error') %>. <%- _t('components.codemirror.line') %> <%- line %>: <span><%- message %></span>
|
||||
</li>
|
||||
</ul>
|
||||
365
lib/assets/javascripts/builder/components/code-mirror/code-mirror-view.js
Executable file
365
lib/assets/javascripts/builder/components/code-mirror/code-mirror-view.js
Executable file
@@ -0,0 +1,365 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var CodeMirror = require('codemirror');
|
||||
var ColorPicker = require('./colorpicker.code-mirror');
|
||||
var template = require('./code-mirror.tpl');
|
||||
var bulletTemplate = require('./code-mirror-bullet.tpl');
|
||||
var errorTemplate = require('./code-mirror-error.tpl');
|
||||
var warningTemplate = require('./code-mirror-warning.tpl');
|
||||
var DATA_SERVICES = require('./data-services');
|
||||
|
||||
require('./mode/sql')(CodeMirror);
|
||||
require('./mode/mustache')(CodeMirror);
|
||||
require('./cartocss.code-mirror')(CodeMirror);
|
||||
require('./scroll.code-mirror')(CodeMirror);
|
||||
require('./show-hint.code-mirror')(CodeMirror);
|
||||
require('./hint/custom-list-hint')(CodeMirror);
|
||||
require('./searchcursor.code-mirror')(CodeMirror);
|
||||
require('./placeholder.code-mirror')(CodeMirror);
|
||||
|
||||
var ESCAPE_KEY_CODE = 27;
|
||||
var RETURN_KEY_CODE = 13;
|
||||
|
||||
var NOHINT = [ESCAPE_KEY_CODE, RETURN_KEY_CODE];
|
||||
|
||||
var ADDONS = {
|
||||
'color-picker': ColorPicker
|
||||
};
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components:code-mirror:code-mirror-view',
|
||||
|
||||
className: 'Editor-content',
|
||||
|
||||
options: {
|
||||
readonly: false,
|
||||
lineNumbers: true,
|
||||
autocompleteChars: 3
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts) throw new Error('options for codemirror are required.');
|
||||
if (!opts.model) throw new Error('Model for codemirror is required.');
|
||||
if (opts.model.get('content') === void 0 &&
|
||||
opts.placeholder === void 0) throw new Error('Content property or placeholder for codemirror is required.');
|
||||
if (!opts.tips) throw new Error('tip messages are required');
|
||||
|
||||
this._autocompleteChars = opts.autocompleteChars || this.options.autocompleteChars;
|
||||
this._mode = opts.mode || 'cartocss';
|
||||
this._addons = opts.addons;
|
||||
this._hints = opts.hints;
|
||||
this._autocompletePrefix = opts.autocompletePrefix;
|
||||
this._autocompleteTriggers = opts.autocompleteTriggers;
|
||||
this._autocompleteSuffix = opts.autocompleteSuffix;
|
||||
this._errorTemplate = opts.errorTemplate || errorTemplate;
|
||||
this._warningTemplate = opts.warningTemplate || warningTemplate;
|
||||
this._warnings = null;
|
||||
this._tips = opts.tips;
|
||||
this._lineWithErrors = [];
|
||||
this._onInputRead = _.bind(this._onKeyUpEditor, this);
|
||||
this._placeholder = opts.placeholder;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
content: this.model.get('content'),
|
||||
tips: this._tips.join(' '),
|
||||
warnings: this._warnings
|
||||
})
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
this._bindEvents();
|
||||
this._showErrors();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var options = _.defaults(_.extend({}, this.model.toJSON()), this.options);
|
||||
|
||||
var isReadOnly = options.readonly;
|
||||
var hasLineNumbers = options.lineNumbers;
|
||||
|
||||
var extraKeys = {
|
||||
'Ctrl-S': this.triggerApplyEvent.bind(this),
|
||||
'Cmd-S': this.triggerApplyEvent.bind(this),
|
||||
'Ctrl-Space': this._completeIfAfterCtrlSpace.bind(this)
|
||||
};
|
||||
|
||||
this.editor = CodeMirror.fromTextArea(this.$('.js-editor').get(0), {
|
||||
lineNumbers: hasLineNumbers,
|
||||
theme: 'material',
|
||||
mode: this._mode,
|
||||
scrollbarStyle: 'simple',
|
||||
lineWrapping: true,
|
||||
readOnly: isReadOnly,
|
||||
extraKeys: extraKeys,
|
||||
placeholder: this._placeholder
|
||||
});
|
||||
this.editor.on('change', _.debounce(this._onCodeMirrorChange.bind(this), 150), this);
|
||||
|
||||
if (!_.isEmpty(this._addons)) {
|
||||
_.each(this._addons, function (addon) {
|
||||
var Class = ADDONS[addon];
|
||||
var addonView = new Class({
|
||||
editor: this.editor
|
||||
});
|
||||
addonView.bind('codeSaved', this.triggerApplyEvent, this);
|
||||
this.$el.append(addonView.el);
|
||||
this.addView(addonView);
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (this._hints) {
|
||||
this.editor.on('keyup', this._onInputRead);
|
||||
}
|
||||
|
||||
this._toggleReadOnly();
|
||||
|
||||
setTimeout(function () {
|
||||
this.editor && this.editor.refresh();
|
||||
}.bind(this), 0);
|
||||
},
|
||||
|
||||
_completeIfAfterCtrlSpace: function (cm) {
|
||||
var autocompletePrefix = this._autocompletePrefix;
|
||||
var opts = {};
|
||||
var cur = cm.getCursor();
|
||||
|
||||
if (autocompletePrefix &&
|
||||
cm.getRange(CodeMirror.Pos(cur.line, cur.ch - autocompletePrefix.length), cur) !== autocompletePrefix) {
|
||||
opts = { autocompletePrefix: autocompletePrefix };
|
||||
}
|
||||
|
||||
return this._completeAfter(cm, opts);
|
||||
},
|
||||
|
||||
updateHints: function (hints) {
|
||||
this._hints = hints;
|
||||
},
|
||||
|
||||
_onKeyUpEditor: function (cm, event) {
|
||||
var code = event.keyCode;
|
||||
var hints = this._hints;
|
||||
var autocompleteChars = this._autocompleteChars - 1;
|
||||
var autocompletePrefix = this._autocompletePrefix;
|
||||
|
||||
if (NOHINT.indexOf(code) === -1) {
|
||||
var self = this;
|
||||
|
||||
if (this._autocompleteTimeout) clearTimeout(this._autocompleteTimeout);
|
||||
|
||||
this._autocompleteTimeout = setTimeout(function () {
|
||||
var opts = {};
|
||||
var cur = cm.getCursor();
|
||||
var str = cm.getTokenAt(cur).string;
|
||||
str = str.toLowerCase();
|
||||
|
||||
if (autocompletePrefix &&
|
||||
cm.getRange(CodeMirror.Pos(cur.line, cur.ch - autocompletePrefix.length), cur) !== autocompletePrefix) {
|
||||
opts = { autocompletePrefix: autocompletePrefix };
|
||||
}
|
||||
|
||||
return self._completeAfter(cm, opts, function () {
|
||||
var autocompleteHandler = function (listItem) {
|
||||
// every list can be an array of strings or an array of objects {text, type}
|
||||
var hit = _.isObject(listItem) ? listItem.text : listItem;
|
||||
hit = hit.toLowerCase();
|
||||
return hit.indexOf(str) !== -1;
|
||||
};
|
||||
|
||||
if (str.length > autocompleteChars) {
|
||||
var listHints = _.filter(hints, autocompleteHandler);
|
||||
|
||||
return listHints.length > 0 || autocompletePrefix && autocompletePrefix === str;
|
||||
}
|
||||
});
|
||||
}, 150);
|
||||
}
|
||||
},
|
||||
|
||||
_onCodeMirrorChange: function () {
|
||||
this.trigger('codeChanged');
|
||||
},
|
||||
|
||||
_completeAfter: function (cm, opts, pred) {
|
||||
if (!pred || pred()) {
|
||||
if (!cm.state.completionActive) {
|
||||
this._showAutocomplete(cm, _.extend({}, opts));
|
||||
}
|
||||
}
|
||||
|
||||
return CodeMirror.Pass;
|
||||
},
|
||||
|
||||
_showAutocomplete: function (cm, opts) {
|
||||
var autocompletePrefix = opts && opts.autocompletePrefix;
|
||||
|
||||
CodeMirror.showHint(cm, CodeMirror.hint['custom-list'], {
|
||||
completeSingle: false,
|
||||
list: this._hints,
|
||||
autocompletePrefix: autocompletePrefix,
|
||||
autocompleteSuffix: this._autocompleteSuffix
|
||||
});
|
||||
},
|
||||
|
||||
_showWarning: function (warnings) {
|
||||
var $warning = this._getWarning();
|
||||
var hasNodes = $warning.children().length;
|
||||
|
||||
if (warnings && !hasNodes) {
|
||||
$warning.append(this._warningTemplate(warnings));
|
||||
}
|
||||
},
|
||||
|
||||
_hideWarning: function () {
|
||||
var $warning = this._getWarning();
|
||||
var hasNodes = $warning.children().length;
|
||||
|
||||
if (hasNodes) {
|
||||
$warning.children()[0].remove();
|
||||
}
|
||||
},
|
||||
|
||||
_bindEvents: function () {
|
||||
var self = this;
|
||||
this.editor.on('change', function (editor, changed) {
|
||||
var content = self.getContent();
|
||||
var dataService = self._containsDataService(content);
|
||||
|
||||
if (dataService) {
|
||||
self._showWarning('Quota error ' + dataService);
|
||||
} else {
|
||||
self._hideWarning();
|
||||
}
|
||||
|
||||
self.model.set('content', content, { silent: true });
|
||||
});
|
||||
|
||||
this.model.on('change:content', function () {
|
||||
this.setContent(this.model.get('content'));
|
||||
}, this);
|
||||
|
||||
this.model.on('change:readonly', this._toggleReadOnly, this);
|
||||
|
||||
this.model.on('change:errors', function () {
|
||||
this._showErrors();
|
||||
}, this);
|
||||
|
||||
this.model.on('undo redo', function () {
|
||||
this.setContent(this.model.get('content'));
|
||||
}, this);
|
||||
},
|
||||
|
||||
_toggleReadOnly: function () {
|
||||
var isReadOnly = !!this.model.get('readonly');
|
||||
this.editor.setOption('readOnly', isReadOnly);
|
||||
if (isReadOnly) {
|
||||
this.editor.setOption('theme', '');
|
||||
this._getInfo().hide();
|
||||
} else {
|
||||
this.editor.setOption('theme', 'material');
|
||||
this._getInfo().show();
|
||||
}
|
||||
},
|
||||
|
||||
search: function (query, caseInsensitive) {
|
||||
var cursor = this.editor.getSearchCursor(query, null, true);
|
||||
cursor.find();
|
||||
return cursor.pos;
|
||||
},
|
||||
|
||||
markReadOnly: function (from, to) {
|
||||
var options = {readOnly: true, inclusiveLeft: true};
|
||||
this.editor.markText(from, to, options);
|
||||
|
||||
for (var i = from.line; i <= to.line; i++) {
|
||||
this.editor.addLineClass(i, 'background', 'CodeMirror-readonlyLine');
|
||||
}
|
||||
},
|
||||
|
||||
setContent: function (value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
getContent: function () {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
triggerApplyEvent: function () {
|
||||
this.trigger('codeSaved', this.getContent(), this);
|
||||
},
|
||||
|
||||
destroyEditor: function () {
|
||||
this.editor.off('change');
|
||||
var el = this.editor.getWrapperElement();
|
||||
var parent = el.parentNode;
|
||||
parent && parent.removeChild(el);
|
||||
this.editor = null;
|
||||
},
|
||||
|
||||
_getInfo: function () {
|
||||
return this.$('.js-console');
|
||||
},
|
||||
|
||||
_getConsole: function () {
|
||||
return this.$('.js-console-error');
|
||||
},
|
||||
|
||||
_getWarning: function () {
|
||||
return this.$('.js-warning');
|
||||
},
|
||||
|
||||
_getCode: function () {
|
||||
return this.$('.CodeMirror-code');
|
||||
},
|
||||
|
||||
_containsDataService: function (content) {
|
||||
return _.find(DATA_SERVICES, function (dataService) {
|
||||
return content.indexOf(dataService) !== -1;
|
||||
});
|
||||
},
|
||||
|
||||
_removeErrors: function () {
|
||||
this._getConsole().empty();
|
||||
_.each(this._lineWithErrors, function ($line) {
|
||||
$line.find('.CodeMirror-bullet').remove();
|
||||
$line.find('.CodeMirror-linenumber').removeClass('has-error');
|
||||
});
|
||||
|
||||
this._lineWithErrors = [];
|
||||
},
|
||||
|
||||
_showErrors: function () {
|
||||
var errors = this.model.get('errors');
|
||||
this._removeErrors();
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
_.each(errors, function (err) {
|
||||
this._renderError(err);
|
||||
this._renderBullet(err);
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
_renderBullet: function (error) {
|
||||
var line = error.line;
|
||||
var $line;
|
||||
if (line) {
|
||||
$line = this._getCode().children().eq(+line - 1);
|
||||
$line.append(bulletTemplate);
|
||||
$line.find('.CodeMirror-linenumber').addClass('has-error');
|
||||
this._lineWithErrors.push($line);
|
||||
}
|
||||
},
|
||||
|
||||
_renderError: function (error) {
|
||||
this._getConsole().append(this._errorTemplate(error));
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.destroyEditor();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="CodeMirror-warning CDB-Text CDB-Size-medium u-flex u-lSpace--xl u-warningTextColor">
|
||||
<div class="CodeMirror-warning-icon__wrapper">
|
||||
<svg class="CodeMirror-warning-icon" width="100%" 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="#FEB100">
|
||||
<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>
|
||||
</div>
|
||||
<div><%= _t('editor.data.code-mirror.quota-data-services-warning') %> <%= _t('editor.data.code-mirror.quota-data-services-warning-link') %></div>
|
||||
</div>
|
||||
12
lib/assets/javascripts/builder/components/code-mirror/code-mirror.tpl
Executable file
12
lib/assets/javascripts/builder/components/code-mirror/code-mirror.tpl
Executable file
@@ -0,0 +1,12 @@
|
||||
<div class="CodeMirror-editor">
|
||||
<textarea class="js-editor"><%- content %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="js-warning"></div>
|
||||
|
||||
<% if (tips) { %>
|
||||
<div class="CodeMirror-console js-console">
|
||||
<%- tips %>
|
||||
<div class="js-console-error"></div>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -0,0 +1,225 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var ColorPicker = require('builder/components/color-picker/color-picker.js');
|
||||
var COLOR_KEYWORDS = require('builder/helpers/color-keywords');
|
||||
|
||||
/**
|
||||
* Show color picker when user clicks over
|
||||
* a color in the Codemirror editor.
|
||||
*
|
||||
* new CodemirrorColorPicker({
|
||||
* editor: codemirror-editor...
|
||||
* })
|
||||
*/
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'editor'
|
||||
];
|
||||
|
||||
var STYLE = _.template('1px solid <%- color %>');
|
||||
var COLORPICKER_HEIGHT = 220;
|
||||
|
||||
var stopPropagation = function (e) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
_.each(REQUIRED_OPTS, function (item) {
|
||||
if (opts[item] === undefined) throw new Error(item + ' is required');
|
||||
this['_' + item] = opts[item];
|
||||
}, this);
|
||||
|
||||
this._updateColors = _.debounce(this._updateColors, 5).bind(this);
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this._editor = opts.editor;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
var self = this;
|
||||
var destroyPicker = function () {
|
||||
this._destroyPicker();
|
||||
}.bind(this);
|
||||
|
||||
this._enableUpdateBind();
|
||||
|
||||
this._editor.on('mousedown', function (cm, ev) {
|
||||
_.delay(self._onClick.bind(self, cm, ev), 50);
|
||||
});
|
||||
|
||||
this._editor.on('keydown', destroyPicker);
|
||||
this._editor.on('viewportChange', destroyPicker);
|
||||
this._editor.on('scroll', destroyPicker);
|
||||
|
||||
var wrapper = this._editor.getWrapperElement();
|
||||
wrapper.addEventListener('click', stopPropagation);
|
||||
},
|
||||
|
||||
_disableBinds: function () {
|
||||
var wrapper = this._editor.getWrapperElement();
|
||||
wrapper.removeEventListener('click', stopPropagation);
|
||||
this._editor.off(null, null, this);
|
||||
},
|
||||
|
||||
_enableUpdateBind: function () {
|
||||
this._editor.on('update', this._updateColors);
|
||||
},
|
||||
|
||||
_disableUpdateBind: function () {
|
||||
this._editor.off('update', this._updateColors);
|
||||
},
|
||||
|
||||
_onClick: function (cm, ev) {
|
||||
var cursor = this._editor.getCursor(true);
|
||||
var token = this._editor.getTokenAt(cursor);
|
||||
|
||||
if (token.type === 'color') {
|
||||
this._createPicker(ev, cursor, token);
|
||||
} else {
|
||||
this._destroyPicker();
|
||||
}
|
||||
},
|
||||
|
||||
_updateColors: function (cm) {
|
||||
var wrapper = cm.getWrapperElement();
|
||||
_.each(wrapper.querySelectorAll('.cm-color'), function (node) {
|
||||
this._paintColor(node.textContent, node);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_replaceColor: function (color, target) {
|
||||
var cursor = this._editor.getCursor();
|
||||
var nameMatch = this._getMatch(cursor, 'name');
|
||||
var hexMatch = this._getMatch(cursor, 'hex');
|
||||
var match = nameMatch || hexMatch;
|
||||
var start;
|
||||
var end;
|
||||
|
||||
if (match) {
|
||||
start = {
|
||||
line: cursor.line,
|
||||
ch: match.start
|
||||
};
|
||||
end = {
|
||||
line: cursor.line,
|
||||
ch: match.end
|
||||
};
|
||||
|
||||
this._editor.replaceRange(color, start, end, 'paste');
|
||||
|
||||
var wrapper = this._editor.getWrapperElement();
|
||||
_.each(wrapper.querySelectorAll('.cm-color'), function (node) {
|
||||
var nodeStyle = node.style;
|
||||
var nodeColor = node.innerText;
|
||||
if (!nodeStyle || !nodeStyle.borderBottom) {
|
||||
this._paintColor(nodeColor, node);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
_paintColor: function (color, target) {
|
||||
target.style.borderBottom = STYLE({color: color});
|
||||
},
|
||||
|
||||
_getMatch: function (cursor, type) {
|
||||
if (!type) return;
|
||||
var re;
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'name':
|
||||
re = new RegExp(COLOR_KEYWORDS.join('|'), 'g');
|
||||
break;
|
||||
case 'hsl':
|
||||
re = /hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)/g;
|
||||
break;
|
||||
case 'rgb':
|
||||
re = /rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/;
|
||||
break;
|
||||
case 'hex':
|
||||
re = /#[a-fA-F0-9]{3,6}/g;
|
||||
break;
|
||||
default:
|
||||
console.log('Invalid color match selection');
|
||||
return;
|
||||
}
|
||||
|
||||
var line = this._editor.getLine(cursor.line);
|
||||
var match = re.exec(line);
|
||||
|
||||
while (match) {
|
||||
var val = match[0];
|
||||
var len = val.length;
|
||||
var start = match.index;
|
||||
var end = match.index + len;
|
||||
if (cursor.ch >= start && cursor.ch <= end) {
|
||||
match = null;
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
string: val
|
||||
};
|
||||
}
|
||||
match = re.exec(line);
|
||||
}
|
||||
},
|
||||
|
||||
_createPicker: function (ev, cursor, token) {
|
||||
var cursorCoords = this._editor.cursorCoords();
|
||||
this._destroyPicker();
|
||||
|
||||
this._colorPicker = new ColorPicker({
|
||||
className: 'Editor-boxModal ColorPicker--cm Editor-boxModal--darked Editor-FormDialog CDB-Text',
|
||||
value: token.string,
|
||||
disableOpacity: true
|
||||
});
|
||||
this._colorPicker.$el.attr('data-colorpicker-cid', this.cid);
|
||||
this._colorPicker.bind('change', _.debounce(this._onColorPickerChange.bind(this, ev), 5), this);
|
||||
|
||||
var top = cursorCoords.top + 20;
|
||||
var maxTop = $(window).outerHeight();
|
||||
|
||||
if (top + COLORPICKER_HEIGHT > maxTop) {
|
||||
top = cursorCoords.top - COLORPICKER_HEIGHT - 20;
|
||||
}
|
||||
|
||||
this._colorPicker.$el.css({
|
||||
left: cursorCoords.left,
|
||||
top: top
|
||||
});
|
||||
|
||||
document.body.appendChild(this._colorPicker.render().el);
|
||||
document.addEventListener('click', this._onDocumentClick);
|
||||
},
|
||||
|
||||
_onDocumentClick: function (e) {
|
||||
var $el = $(e.target);
|
||||
if ($el.closest('[data-colorpicker-cid="' + this.cid + '"]').length === 0) {
|
||||
this._destroyPicker();
|
||||
}
|
||||
},
|
||||
|
||||
_onColorPickerChange: function (ev, values) {
|
||||
this._disableUpdateBind();
|
||||
this._replaceColor(values.hex, ev.target);
|
||||
this.trigger('codeSaved');
|
||||
this._enableUpdateBind();
|
||||
},
|
||||
|
||||
_destroyPicker: function () {
|
||||
if (this._colorPicker) {
|
||||
this.removeView(this._colorPicker);
|
||||
this._colorPicker.clean();
|
||||
delete this._colorPicker;
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this._onDocumentClick);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._disableBinds();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = [
|
||||
'cdb_geocode_street_point',
|
||||
'cdb_isodistance',
|
||||
'cdb_isochrone',
|
||||
'OBS_GetDemographicSnapshot',
|
||||
'cdb_route_point_to_point',
|
||||
'cdb_route_with_waypoints'
|
||||
];
|
||||
@@ -0,0 +1,95 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = function (CodeMirror) {
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function arrayContains (arr, item) {
|
||||
return arr.indexOf(item) !== -1;
|
||||
}
|
||||
|
||||
function scriptHint (editor, keywords, getToken, options) {
|
||||
// Find the token at the cursor
|
||||
var cur = editor.getCursor();
|
||||
var token = getToken(editor, cur);
|
||||
var tprop = token;
|
||||
var context = [];
|
||||
token.state = CodeMirror.innerMode(editor.getMode(), token.state).state;
|
||||
|
||||
// If it's not a 'word-style' token, ignore the token.
|
||||
if (!/^[\w$_-]*$/.test(token.string)) {
|
||||
token = tprop = {
|
||||
start: cur.ch,
|
||||
end: cur.ch,
|
||||
string: '',
|
||||
state: token.state,
|
||||
type: token.string === '.' ? 'property' : null
|
||||
};
|
||||
}
|
||||
// If it is a property, find out what it is a property of.
|
||||
while (tprop.type === 'property') {
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.string !== '.') return;
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.string === ')') {
|
||||
var level = 1;
|
||||
do {
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
switch (tprop.string) {
|
||||
case ')':
|
||||
level++;
|
||||
break;
|
||||
case '(':
|
||||
level--;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} while (level > 0);
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.type.indexOf('variable') === 0) tprop.type = 'function';
|
||||
else return; // no clue
|
||||
}
|
||||
context.push(tprop);
|
||||
}
|
||||
|
||||
return {
|
||||
list: getCompletions(token, context, keywords, options),
|
||||
from: Pos(cur.line, token.start),
|
||||
to: Pos(cur.line, token.end)
|
||||
};
|
||||
}
|
||||
|
||||
function columnsHint (editor, options) {
|
||||
return scriptHint(editor, [], /* javascriptKeywords */
|
||||
function (e, cur) {
|
||||
return e.getTokenAt(cur);
|
||||
},
|
||||
options);
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper('hint', 'custom-list', columnsHint);
|
||||
|
||||
function getCompletions (token, context, keywords, options) {
|
||||
var found = [];
|
||||
var start = token.string.toLowerCase();
|
||||
|
||||
function maybeAdd (str) {
|
||||
var hit = _.isObject(str) ? str.text : str;
|
||||
hit = hit.toLowerCase();
|
||||
if (hit.indexOf(start) === 0 && start !== hit && !arrayContains(found, hit)) {
|
||||
found.push(str);
|
||||
}
|
||||
}
|
||||
|
||||
function gatherCompletions (obj) {
|
||||
for (var name in obj) {
|
||||
maybeAdd(obj[name]);
|
||||
}
|
||||
}
|
||||
|
||||
gatherCompletions(options.list);
|
||||
_.each(keywords, maybeAdd);
|
||||
|
||||
return found;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
module.exports = function (CodeMirror) {
|
||||
CodeMirror.defineMode('mustache', function () {
|
||||
return {
|
||||
token: function (stream, state) {
|
||||
var ch;
|
||||
|
||||
if (stream.match('{{')) {
|
||||
ch = stream.peek();
|
||||
if (ch == null || ch != null && ch.match(/[{]{1,}/)) {
|
||||
stream.next();
|
||||
return 'mustache-error';
|
||||
} else if (ch != null && ch.match(/[a-zA-Z\u00C0-\u024F_]+/)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.match('}}')) {
|
||||
ch = stream.peek();
|
||||
if (ch != null && ch.match(/[}]{1,}/)) {
|
||||
stream.next();
|
||||
return 'mustache-error';
|
||||
} else if (ch == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.match('}')) {
|
||||
ch = stream.peek();
|
||||
if (ch == null || ch != null && ch.match(/[}]{1,}/)) {
|
||||
stream.next();
|
||||
return 'mustache-error';
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.match('{')) {
|
||||
ch = stream.peek();
|
||||
if (ch == null || ch != null && ch.match(/[{]{1,}/)) {
|
||||
stream.next();
|
||||
return 'mustache-error';
|
||||
}
|
||||
}
|
||||
|
||||
// Delimiter character
|
||||
if (stream.match(/[a-zA-Z\u00C0-\u024F_]+/, true)) {
|
||||
return 'mustache-text';
|
||||
}
|
||||
|
||||
// jump to the next item, needed OR CRASH
|
||||
stream.next();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
(function () {
|
||||
var sqlKeywords = '{{ }}';
|
||||
|
||||
function set (str) {
|
||||
var obj = {};
|
||||
var words = str.split(' ');
|
||||
for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
|
||||
return obj;
|
||||
}
|
||||
|
||||
CodeMirror.defineMIME('text/mustache', {
|
||||
name: 'mustache',
|
||||
client: set('source'),
|
||||
keywords: set(sqlKeywords)
|
||||
});
|
||||
}());
|
||||
};
|
||||
279
lib/assets/javascripts/builder/components/code-mirror/mode/sql.js
Executable file
279
lib/assets/javascripts/builder/components/code-mirror/mode/sql.js
Executable file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
/* eslint-disable */
|
||||
module.exports = function (CodeMirror) {
|
||||
CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
|
||||
var prev = old && old != CodeMirror.Init;
|
||||
if (val && !prev) {
|
||||
cm.on("blur", onBlur);
|
||||
cm.on("change", onChange);
|
||||
cm.on("swapDoc", onChange);
|
||||
onChange(cm);
|
||||
} else if (!val && prev) {
|
||||
cm.off("blur", onBlur);
|
||||
cm.off("change", onChange);
|
||||
cm.off("swapDoc", onChange);
|
||||
clearPlaceholder(cm);
|
||||
var wrapper = cm.getWrapperElement();
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
|
||||
}
|
||||
|
||||
if (val && !cm.hasFocus()) onBlur(cm);
|
||||
});
|
||||
|
||||
function clearPlaceholder(cm) {
|
||||
if (cm.state.placeholder) {
|
||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
|
||||
cm.state.placeholder = null;
|
||||
}
|
||||
}
|
||||
function setPlaceholder(cm) {
|
||||
clearPlaceholder(cm);
|
||||
var elt = cm.state.placeholder = document.createElement("pre");
|
||||
elt.style.cssText = "height: 0; overflow: visible";
|
||||
elt.className = "CodeMirror-placeholder";
|
||||
var placeHolder = cm.getOption("placeholder")
|
||||
if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
|
||||
elt.appendChild(placeHolder)
|
||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
|
||||
}
|
||||
|
||||
function onBlur(cm) {
|
||||
if (isEmpty(cm)) setPlaceholder(cm);
|
||||
}
|
||||
function onChange(cm) {
|
||||
var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
|
||||
|
||||
if (empty) setPlaceholder(cm);
|
||||
else clearPlaceholder(cm);
|
||||
}
|
||||
|
||||
function isEmpty(cm) {
|
||||
return (cm.lineCount() === 1) && (cm.getLine(0) === "");
|
||||
}
|
||||
};
|
||||
/* eslint-enable */
|
||||
151
lib/assets/javascripts/builder/components/code-mirror/scroll.code-mirror.js
Executable file
151
lib/assets/javascripts/builder/components/code-mirror/scroll.code-mirror.js
Executable file
@@ -0,0 +1,151 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
module.exports = function (CodeMirror) {
|
||||
function Bar (cls, orientation, scroll) {
|
||||
this.orientation = orientation;
|
||||
this.scroll = scroll;
|
||||
this.screen = this.total = this.size = 1;
|
||||
this.pos = 0;
|
||||
|
||||
this.node = document.createElement('div');
|
||||
this.node.className = cls + '-' + orientation;
|
||||
this.inner = this.node.appendChild(document.createElement('div'));
|
||||
|
||||
var self = this;
|
||||
CodeMirror.on(this.inner, 'mousedown', function (e) {
|
||||
if (e.which !== 1) return;
|
||||
CodeMirror.e_preventDefault(e);
|
||||
var axis = self.orientation === 'horizontal' ? 'pageX' : 'pageY';
|
||||
var start = e[axis];
|
||||
var startpos = self.pos;
|
||||
function done () {
|
||||
CodeMirror.off(document, 'mousemove', move);
|
||||
CodeMirror.off(document, 'mouseup', done);
|
||||
}
|
||||
function move (e) {
|
||||
if (e.which !== 1) return done();
|
||||
self.moveTo(startpos + (e[axis] - start) * (self.total / self.size));
|
||||
}
|
||||
CodeMirror.on(document, 'mousemove', move);
|
||||
CodeMirror.on(document, 'mouseup', done);
|
||||
});
|
||||
|
||||
CodeMirror.on(this.node, 'click', function (e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
var innerBox = self.inner.getBoundingClientRect();
|
||||
var where;
|
||||
if (self.orientation === 'horizontal') {
|
||||
where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0;
|
||||
} else {
|
||||
where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0;
|
||||
}
|
||||
self.moveTo(self.pos + where * self.screen);
|
||||
});
|
||||
|
||||
function onWheel (e) {
|
||||
var moved = CodeMirror.wheelEventPixels(e)[self.orientation === 'horizontal' ? 'x' : 'y'];
|
||||
var oldPos = self.pos;
|
||||
self.moveTo(self.pos + moved);
|
||||
if (self.pos !== oldPos) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
}
|
||||
}
|
||||
CodeMirror.on(this.node, 'mousewheel', onWheel);
|
||||
CodeMirror.on(this.node, 'DOMMouseScroll', onWheel);
|
||||
}
|
||||
|
||||
Bar.prototype.setPos = function (pos, force) {
|
||||
if (pos < 0) pos = 0;
|
||||
if (pos > this.total - this.screen) pos = this.total - this.screen;
|
||||
if (!force && pos === this.pos) return false;
|
||||
this.pos = pos;
|
||||
this.inner.style[this.orientation === 'horizontal' ? 'left' : 'top'] =
|
||||
(pos * (this.size / this.total)) + 'px';
|
||||
return true;
|
||||
};
|
||||
|
||||
Bar.prototype.moveTo = function (pos) {
|
||||
if (this.setPos(pos)) this.scroll(pos, this.orientation);
|
||||
};
|
||||
|
||||
var minButtonSize = 10;
|
||||
|
||||
Bar.prototype.update = function (scrollSize, clientSize, barSize) {
|
||||
var sizeChanged = this.screen !== clientSize || this.total !== scrollSize || this.size !== barSize;
|
||||
if (sizeChanged) {
|
||||
this.screen = clientSize;
|
||||
this.total = scrollSize;
|
||||
this.size = barSize;
|
||||
}
|
||||
|
||||
var buttonSize = this.screen * (this.size / this.total);
|
||||
if (buttonSize < minButtonSize) {
|
||||
this.size -= minButtonSize - buttonSize;
|
||||
buttonSize = minButtonSize;
|
||||
}
|
||||
this.inner.style[this.orientation === 'horizontal' ? 'width' : 'height'] =
|
||||
buttonSize + 'px';
|
||||
this.setPos(this.pos, sizeChanged);
|
||||
};
|
||||
|
||||
function SimpleScrollbars (cls, place, scroll) {
|
||||
this.addClass = cls;
|
||||
this.horiz = new Bar(cls, 'horizontal', scroll);
|
||||
place(this.horiz.node);
|
||||
this.vert = new Bar(cls, 'vertical', scroll);
|
||||
place(this.vert.node);
|
||||
this.width = null;
|
||||
}
|
||||
|
||||
SimpleScrollbars.prototype.update = function (measure) {
|
||||
if (this.width == null) {
|
||||
var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle;
|
||||
if (style) {
|
||||
this.width = parseInt(style.height, 10);
|
||||
}
|
||||
}
|
||||
var width = this.width || 0;
|
||||
|
||||
var needsH = measure.scrollWidth > measure.clientWidth + 1;
|
||||
var needsV = measure.scrollHeight > measure.clientHeight + 1;
|
||||
this.vert.node.style.display = needsV ? 'block' : 'none';
|
||||
this.horiz.node.style.display = needsH ? 'block' : 'none';
|
||||
|
||||
if (needsV) {
|
||||
this.vert.update(measure.scrollHeight, measure.clientHeight,
|
||||
measure.viewHeight - (needsH ? width : 0));
|
||||
this.vert.node.style.bottom = needsH ? width + 'px' : '0';
|
||||
}
|
||||
if (needsH) {
|
||||
this.horiz.update(measure.scrollWidth, measure.clientWidth,
|
||||
measure.viewWidth - (needsV ? width : 0) - measure.barLeft);
|
||||
this.horiz.node.style.right = needsV ? width + 'px' : '0';
|
||||
this.horiz.node.style.left = measure.barLeft + 'px';
|
||||
}
|
||||
|
||||
return {right: needsV ? width : 0, bottom: needsH ? width : 0};
|
||||
};
|
||||
|
||||
SimpleScrollbars.prototype.setScrollTop = function (pos) {
|
||||
this.vert.setPos(pos);
|
||||
};
|
||||
|
||||
SimpleScrollbars.prototype.setScrollLeft = function (pos) {
|
||||
this.horiz.setPos(pos);
|
||||
};
|
||||
|
||||
SimpleScrollbars.prototype.clear = function () {
|
||||
var parent = this.horiz.node.parentNode;
|
||||
parent.removeChild(this.horiz.node);
|
||||
parent.removeChild(this.vert.node);
|
||||
};
|
||||
|
||||
CodeMirror.scrollbarModel.simple = function (place, scroll) {
|
||||
return new SimpleScrollbars('CodeMirror-simplescroll', place, scroll);
|
||||
};
|
||||
|
||||
CodeMirror.scrollbarModel.overlay = function (place, scroll) {
|
||||
return new SimpleScrollbars('CodeMirror-overlayscroll', place, scroll);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
module.exports = function (CodeMirror) {
|
||||
/*eslint-disable */
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function SearchCursor(doc, query, pos, caseFold) {
|
||||
this.atOccurrence = false; this.doc = doc;
|
||||
if (caseFold == null && typeof query == "string") caseFold = false;
|
||||
|
||||
pos = pos ? doc.clipPos(pos) : Pos(0, 0);
|
||||
this.pos = {from: pos, to: pos};
|
||||
|
||||
// The matches method is filled in based on the type of query.
|
||||
// It takes a position and a direction, and returns an object
|
||||
// describing the next occurrence of the query, or null if no
|
||||
// more matches were found.
|
||||
if (typeof query != "string") { // Regexp match
|
||||
if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
|
||||
this.matches = function(reverse, pos) {
|
||||
if (reverse) {
|
||||
query.lastIndex = 0;
|
||||
var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
|
||||
for (;;) {
|
||||
query.lastIndex = cutOff;
|
||||
var newMatch = query.exec(line);
|
||||
if (!newMatch) break;
|
||||
match = newMatch;
|
||||
start = match.index;
|
||||
cutOff = match.index + (match[0].length || 1);
|
||||
if (cutOff == line.length) break;
|
||||
}
|
||||
var matchLen = (match && match[0].length) || 0;
|
||||
if (!matchLen) {
|
||||
if (start == 0 && line.length == 0) {match = undefined;}
|
||||
else if (start != doc.getLine(pos.line).length) {
|
||||
matchLen++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query.lastIndex = pos.ch;
|
||||
var line = doc.getLine(pos.line), match = query.exec(line);
|
||||
var matchLen = (match && match[0].length) || 0;
|
||||
var start = match && match.index;
|
||||
if (start + matchLen != line.length && !matchLen) matchLen = 1;
|
||||
}
|
||||
if (match && matchLen)
|
||||
return {from: Pos(pos.line, start),
|
||||
to: Pos(pos.line, start + matchLen),
|
||||
match: match};
|
||||
};
|
||||
} else { // String query
|
||||
var origQuery = query;
|
||||
if (caseFold) query = query.toLowerCase();
|
||||
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
|
||||
var target = query.split("\n");
|
||||
// Different methods for single-line and multi-line queries
|
||||
if (target.length == 1) {
|
||||
if (!query.length) {
|
||||
// Empty string would match anything and never progress, so
|
||||
// we define it to match nothing instead.
|
||||
this.matches = function() {};
|
||||
} else {
|
||||
this.matches = function(reverse, pos) {
|
||||
if (reverse) {
|
||||
var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
|
||||
var match = line.lastIndexOf(query);
|
||||
if (match > -1) {
|
||||
match = adjustPos(orig, line, match);
|
||||
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
|
||||
}
|
||||
} else {
|
||||
var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
|
||||
var match = line.indexOf(query);
|
||||
if (match > -1) {
|
||||
match = adjustPos(orig, line, match) + pos.ch;
|
||||
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
var origTarget = origQuery.split("\n");
|
||||
this.matches = function(reverse, pos) {
|
||||
var last = target.length - 1;
|
||||
if (reverse) {
|
||||
if (pos.line - (target.length - 1) < doc.firstLine()) return;
|
||||
if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
|
||||
var to = Pos(pos.line, origTarget[last].length);
|
||||
for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
|
||||
if (target[i] != fold(doc.getLine(ln))) return;
|
||||
var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
|
||||
if (fold(line.slice(cut)) != target[0]) return;
|
||||
return {from: Pos(ln, cut), to: to};
|
||||
} else {
|
||||
if (pos.line + (target.length - 1) > doc.lastLine()) return;
|
||||
var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
|
||||
if (fold(line.slice(cut)) != target[0]) return;
|
||||
var from = Pos(pos.line, cut);
|
||||
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
|
||||
if (target[i] != fold(doc.getLine(ln))) return;
|
||||
if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
|
||||
return {from: from, to: Pos(ln, origTarget[last].length)};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchCursor.prototype = {
|
||||
findNext: function() {return this.find(false);},
|
||||
findPrevious: function() {return this.find(true);},
|
||||
|
||||
find: function(reverse) {
|
||||
var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
|
||||
function savePosAndFail(line) {
|
||||
var pos = Pos(line, 0);
|
||||
self.pos = {from: pos, to: pos};
|
||||
self.atOccurrence = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
if (this.pos = this.matches(reverse, pos)) {
|
||||
this.atOccurrence = true;
|
||||
return this.pos.match || true;
|
||||
}
|
||||
if (reverse) {
|
||||
if (!pos.line) return savePosAndFail(0);
|
||||
pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
|
||||
}
|
||||
else {
|
||||
var maxLine = this.doc.lineCount();
|
||||
if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
|
||||
pos = Pos(pos.line + 1, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
from: function() {if (this.atOccurrence) return this.pos.from;},
|
||||
to: function() {if (this.atOccurrence) return this.pos.to;},
|
||||
|
||||
replace: function(newText, origin) {
|
||||
if (!this.atOccurrence) return;
|
||||
var lines = CodeMirror.splitLines(newText);
|
||||
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
|
||||
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
|
||||
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
|
||||
}
|
||||
};
|
||||
|
||||
// Maps a position in a case-folded line back to a position in the original line
|
||||
// (compensating for codepoints increasing in number during folding)
|
||||
function adjustPos(orig, folded, pos) {
|
||||
if (orig.length == folded.length) return pos;
|
||||
for (var pos1 = Math.min(pos, orig.length);;) {
|
||||
var len1 = orig.slice(0, pos1).toLowerCase().length;
|
||||
if (len1 < pos) ++pos1;
|
||||
else if (len1 > pos) --pos1;
|
||||
else return pos1;
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
|
||||
return new SearchCursor(this.doc, query, pos, caseFold);
|
||||
});
|
||||
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
|
||||
return new SearchCursor(this, query, pos, caseFold);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
|
||||
var ranges = [];
|
||||
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
|
||||
while (cur.findNext()) {
|
||||
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
|
||||
ranges.push({anchor: cur.from(), head: cur.to()});
|
||||
}
|
||||
if (ranges.length)
|
||||
this.setSelections(ranges, 0);
|
||||
});
|
||||
/* eslint-enable */
|
||||
};
|
||||
460
lib/assets/javascripts/builder/components/code-mirror/show-hint.code-mirror.js
Executable file
460
lib/assets/javascripts/builder/components/code-mirror/show-hint.code-mirror.js
Executable file
@@ -0,0 +1,460 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
/* eslint-disable */
|
||||
module.exports = function (CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
|
||||
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
|
||||
|
||||
// This is the old interface, kept around for now to stay
|
||||
// backwards-compatible.
|
||||
CodeMirror.showHint = function(cm, getHints, options) {
|
||||
if (!getHints) return cm.showHint(options);
|
||||
if (options && options.async) getHints.async = true;
|
||||
var newOpts = {hint: getHints};
|
||||
if (options) for (var prop in options) newOpts[prop] = options[prop];
|
||||
return cm.showHint(newOpts);
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("showHint", function(options) {
|
||||
options = parseOptions(this, this.getCursor("start"), options);
|
||||
var selections = this.listSelections()
|
||||
if (selections.length > 1) return;
|
||||
// By default, don't allow completion when something is selected.
|
||||
// A hint function can have a `supportsSelection` property to
|
||||
// indicate that it can handle selections.
|
||||
if (this.somethingSelected()) {
|
||||
if (!options.hint.supportsSelection) return;
|
||||
// Don't try with cross-line selections
|
||||
for (var i = 0; i < selections.length; i++)
|
||||
if (selections[i].head.line != selections[i].anchor.line) return;
|
||||
}
|
||||
|
||||
if (this.state.completionActive) this.state.completionActive.close();
|
||||
var completion = this.state.completionActive = new Completion(this, options);
|
||||
if (!completion.options.hint) return;
|
||||
|
||||
CodeMirror.signal(this, "startCompletion", this);
|
||||
completion.update(true);
|
||||
});
|
||||
|
||||
function Completion(cm, options) {
|
||||
this.cm = cm;
|
||||
this.options = options;
|
||||
this.widget = null;
|
||||
this.debounce = 0;
|
||||
this.tick = 0;
|
||||
this.startPos = this.cm.getCursor("start");
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
|
||||
|
||||
var self = this;
|
||||
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
|
||||
}
|
||||
|
||||
var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
|
||||
return setTimeout(fn, 1000/60);
|
||||
};
|
||||
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
|
||||
|
||||
Completion.prototype = {
|
||||
close: function() {
|
||||
if (!this.active()) return;
|
||||
this.cm.state.completionActive = null;
|
||||
this.tick = null;
|
||||
this.cm.off("cursorActivity", this.activityFunc);
|
||||
|
||||
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
|
||||
if (this.widget) this.widget.close();
|
||||
CodeMirror.signal(this.cm, "endCompletion", this.cm);
|
||||
},
|
||||
|
||||
active: function() {
|
||||
return this.cm.state.completionActive == this;
|
||||
},
|
||||
|
||||
pick: function(data, i) {
|
||||
var completion = data.list[i];
|
||||
if (completion.hint) completion.hint(this.cm, data, completion);
|
||||
else this.cm.replaceRange(getText(completion, this.options), completion.from || data.from,
|
||||
completion.to || data.to, "complete");
|
||||
CodeMirror.signal(data, "pick", completion);
|
||||
this.close();
|
||||
},
|
||||
|
||||
cursorActivity: function() {
|
||||
if (this.debounce) {
|
||||
cancelAnimationFrame(this.debounce);
|
||||
this.debounce = 0;
|
||||
}
|
||||
|
||||
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
|
||||
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
|
||||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
|
||||
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
|
||||
this.close();
|
||||
} else {
|
||||
var self = this;
|
||||
this.debounce = requestAnimationFrame(function() {self.update();});
|
||||
if (this.widget) this.widget.disable();
|
||||
}
|
||||
},
|
||||
|
||||
update: function(first) {
|
||||
if (this.tick == null) return
|
||||
var self = this, myTick = ++this.tick
|
||||
fetchHints(this.options.hint, this.cm, this.options, function(data) {
|
||||
if (self.tick == myTick) self.finishUpdate(data, first)
|
||||
})
|
||||
},
|
||||
|
||||
finishUpdate: function(data, first) {
|
||||
if (this.data) CodeMirror.signal(this.data, "update");
|
||||
|
||||
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
|
||||
if (this.widget) this.widget.close();
|
||||
|
||||
if (data && this.data && isNewCompletion(this.data, data)) return;
|
||||
this.data = data;
|
||||
|
||||
if (data && data.list.length) {
|
||||
if (picked && data.list.length == 1) {
|
||||
this.pick(data, 0);
|
||||
} else {
|
||||
this.widget = new Widget(this, data);
|
||||
CodeMirror.signal(data, "shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function isNewCompletion(old, nw) {
|
||||
var moved = CodeMirror.cmpPos(nw.from, old.from)
|
||||
return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
|
||||
}
|
||||
|
||||
function parseOptions(cm, pos, options) {
|
||||
var editor = cm.options.hintOptions;
|
||||
var out = {};
|
||||
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
|
||||
if (editor) for (var prop in editor)
|
||||
if (editor[prop] !== undefined) out[prop] = editor[prop];
|
||||
if (options) for (var prop in options)
|
||||
if (options[prop] !== undefined) out[prop] = options[prop];
|
||||
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
|
||||
return out;
|
||||
}
|
||||
|
||||
function getText(completion, options) {
|
||||
var autocompleteSuffix = options && options.autocompleteSuffix ? options.autocompleteSuffix : '';
|
||||
var autocompletePrefix = options && options.autocompletePrefix ? options.autocompletePrefix : '';
|
||||
|
||||
if (typeof completion !== "string") {
|
||||
completion = completion.text
|
||||
}
|
||||
|
||||
return autocompletePrefix + completion + autocompleteSuffix;
|
||||
}
|
||||
|
||||
function getType(completion) {
|
||||
if (typeof completion == "string") return '';
|
||||
else return completion.type;
|
||||
}
|
||||
|
||||
function buildKeyMap(completion, handle) {
|
||||
var baseMap = {
|
||||
Up: function() {handle.moveFocus(-1);},
|
||||
Down: function() {handle.moveFocus(1);},
|
||||
PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
|
||||
PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
|
||||
Home: function() {handle.setFocus(0);},
|
||||
End: function() {handle.setFocus(handle.length - 1);},
|
||||
Enter: handle.pick,
|
||||
Tab: handle.pick,
|
||||
Esc: handle.close
|
||||
};
|
||||
var custom = completion.options.customKeys;
|
||||
var ourMap = custom ? {} : baseMap;
|
||||
function addBinding(key, val) {
|
||||
var bound;
|
||||
if (typeof val != "string")
|
||||
bound = function(cm) { return val(cm, handle); };
|
||||
// This mechanism is deprecated
|
||||
else if (baseMap.hasOwnProperty(val))
|
||||
bound = baseMap[val];
|
||||
else
|
||||
bound = val;
|
||||
ourMap[key] = bound;
|
||||
}
|
||||
if (custom)
|
||||
for (var key in custom) if (custom.hasOwnProperty(key))
|
||||
addBinding(key, custom[key]);
|
||||
var extra = completion.options.extraKeys;
|
||||
if (extra)
|
||||
for (var key in extra) if (extra.hasOwnProperty(key))
|
||||
addBinding(key, extra[key]);
|
||||
return ourMap;
|
||||
}
|
||||
|
||||
function getHintElement(hintsElement, el) {
|
||||
while (el && el != hintsElement) {
|
||||
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
|
||||
el = el.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function styleHint(hint) {
|
||||
var element = document.createElement(hint.displayText || getText(hint));
|
||||
element = document.createElement("span");
|
||||
element.className = 'CDB-Size-small has-letter';
|
||||
element.innerHTML = hint.displayText || getText(hint);
|
||||
|
||||
var type = getType(hint);
|
||||
if (type) {
|
||||
element.className = 'CDB-Size-small has-letter';
|
||||
element.dataset.type = type;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function Widget(completion, data) {
|
||||
this.completion = completion;
|
||||
this.data = data;
|
||||
this.picked = false;
|
||||
var widget = this, cm = completion.cm;
|
||||
|
||||
var hints = this.hints = document.createElement("ul");
|
||||
hints.className = "CodeMirror-hints";
|
||||
this.selectedHint = data.selectedHint || 0;
|
||||
|
||||
var element;
|
||||
var completions = data.list;
|
||||
for (var i = 0; i < completions.length; ++i) {
|
||||
var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
|
||||
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
|
||||
if (cur.className != null) className = cur.className + " " + className;
|
||||
elt.className = className;
|
||||
if (cur.render) cur.render(elt, data, cur);
|
||||
else {
|
||||
element = styleHint(cur);
|
||||
elt.appendChild(element);
|
||||
}
|
||||
|
||||
elt.hintId = i;
|
||||
}
|
||||
|
||||
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
|
||||
var left = pos.left, top = pos.bottom, below = true;
|
||||
hints.style.left = left + "px";
|
||||
hints.style.top = top + "px";
|
||||
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
|
||||
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
|
||||
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
|
||||
(completion.options.container || document.body).appendChild(hints);
|
||||
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
|
||||
if (overlapY > 0) {
|
||||
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
|
||||
if (curTop - height > 0) { // Fits above cursor
|
||||
hints.style.top = (top = pos.top - height) + "px";
|
||||
below = false;
|
||||
} else if (height > winH) {
|
||||
hints.style.height = (winH - 5) + "px";
|
||||
hints.style.top = (top = pos.bottom - box.top) + "px";
|
||||
var cursor = cm.getCursor();
|
||||
if (data.from.ch != cursor.ch) {
|
||||
pos = cm.cursorCoords(cursor);
|
||||
hints.style.left = (left = pos.left) + "px";
|
||||
box = hints.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
var overlapX = box.right - winW;
|
||||
if (overlapX > 0) {
|
||||
if (box.right - box.left > winW) {
|
||||
hints.style.width = (winW - 5) + "px";
|
||||
overlapX -= (box.right - box.left) - winW;
|
||||
}
|
||||
hints.style.left = (left = pos.left - overlapX) + "px";
|
||||
}
|
||||
|
||||
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
|
||||
moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
|
||||
setFocus: function(n) { widget.changeActive(n); },
|
||||
menuSize: function() { return widget.screenAmount(); },
|
||||
length: completions.length,
|
||||
close: function() { completion.close(); },
|
||||
pick: function() { widget.pick(); },
|
||||
data: data
|
||||
}));
|
||||
|
||||
if (completion.options.closeOnUnfocus) {
|
||||
var closingOnBlur;
|
||||
cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
|
||||
cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
|
||||
}
|
||||
|
||||
var startScroll = cm.getScrollInfo();
|
||||
cm.on("scroll", this.onScroll = function() {
|
||||
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
|
||||
var newTop = top + startScroll.top - curScroll.top;
|
||||
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
|
||||
if (!below) point += hints.offsetHeight;
|
||||
if (point <= editor.top || point >= editor.bottom) return completion.close();
|
||||
hints.style.top = newTop + "px";
|
||||
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "dblclick", function(e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "click", function(e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {
|
||||
widget.changeActive(t.hintId);
|
||||
if (completion.options.completeOnSingleClick) widget.pick();
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "mousedown", function() {
|
||||
setTimeout(function(){cm.focus();}, 20);
|
||||
});
|
||||
|
||||
CodeMirror.signal(data, "select", completions[0], hints.firstChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget.prototype = {
|
||||
close: function() {
|
||||
if (this.completion.widget != this) return;
|
||||
this.completion.widget = null;
|
||||
this.hints.parentNode.removeChild(this.hints);
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
|
||||
var cm = this.completion.cm;
|
||||
if (this.completion.options.closeOnUnfocus) {
|
||||
cm.off("blur", this.onBlur);
|
||||
cm.off("focus", this.onFocus);
|
||||
}
|
||||
cm.off("scroll", this.onScroll);
|
||||
},
|
||||
|
||||
disable: function() {
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
var widget = this;
|
||||
this.keyMap = {Enter: function() { widget.picked = true; }};
|
||||
this.completion.cm.addKeyMap(this.keyMap);
|
||||
},
|
||||
|
||||
pick: function() {
|
||||
this.completion.pick(this.data, this.selectedHint);
|
||||
},
|
||||
|
||||
changeActive: function(i, avoidWrap) {
|
||||
if (i >= this.data.list.length)
|
||||
i = avoidWrap ? this.data.list.length - 1 : 0;
|
||||
else if (i < 0)
|
||||
i = avoidWrap ? 0 : this.data.list.length - 1;
|
||||
if (this.selectedHint == i) return;
|
||||
var node = this.hints.childNodes[this.selectedHint];
|
||||
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
|
||||
node = this.hints.childNodes[this.selectedHint = i];
|
||||
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
|
||||
if (node.offsetTop < this.hints.scrollTop)
|
||||
this.hints.scrollTop = node.offsetTop - 3;
|
||||
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
|
||||
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
|
||||
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
|
||||
},
|
||||
|
||||
screenAmount: function() {
|
||||
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
|
||||
}
|
||||
};
|
||||
|
||||
function applicableHelpers(cm, helpers) {
|
||||
if (!cm.somethingSelected()) return helpers
|
||||
var result = []
|
||||
for (var i = 0; i < helpers.length; i++)
|
||||
if (helpers[i].supportsSelection) result.push(helpers[i])
|
||||
return result
|
||||
}
|
||||
|
||||
function fetchHints(hint, cm, options, callback) {
|
||||
if (hint.async) {
|
||||
hint(cm, callback, options)
|
||||
} else {
|
||||
var result = hint(cm, options)
|
||||
if (result && result.then) result.then(callback)
|
||||
else callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAutoHints(cm, pos) {
|
||||
var helpers = cm.getHelpers(pos, "hint"), words
|
||||
if (helpers.length) {
|
||||
var resolved = function(cm, callback, options) {
|
||||
var app = applicableHelpers(cm, helpers);
|
||||
function run(i) {
|
||||
if (i == app.length) return callback(null)
|
||||
fetchHints(app[i], cm, options, function(result) {
|
||||
if (result && result.list.length > 0) callback(result)
|
||||
else run(i + 1)
|
||||
})
|
||||
}
|
||||
run(0)
|
||||
}
|
||||
resolved.async = true
|
||||
resolved.supportsSelection = true
|
||||
return resolved
|
||||
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
|
||||
return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
|
||||
} else if (CodeMirror.hint.anyword) {
|
||||
return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
|
||||
} else {
|
||||
return function() {}
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("hint", "auto", {
|
||||
resolve: resolveAutoHints
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
|
||||
var cur = cm.getCursor(), token = cm.getTokenAt(cur);
|
||||
var to = CodeMirror.Pos(cur.line, token.end);
|
||||
if (token.string && /\w/.test(token.string[token.string.length - 1])) {
|
||||
var term = token.string, from = CodeMirror.Pos(cur.line, token.start);
|
||||
} else {
|
||||
var term = "", from = to;
|
||||
}
|
||||
var found = [];
|
||||
for (var i = 0; i < options.words.length; i++) {
|
||||
var word = options.words[i];
|
||||
if (word.slice(0, term.length) == term)
|
||||
found.push(word);
|
||||
}
|
||||
|
||||
if (found.length) return {list: found, from: from, to: to};
|
||||
});
|
||||
|
||||
CodeMirror.commands.autocomplete = CodeMirror.showHint;
|
||||
|
||||
var defaultOptions = {
|
||||
hint: CodeMirror.hint.auto,
|
||||
completeSingle: true,
|
||||
alignWithWord: true,
|
||||
closeCharacters: /[\s()\[\]{};:>,]/,
|
||||
closeOnUnfocus: true,
|
||||
completeOnSingleClick: true,
|
||||
container: null,
|
||||
customKeys: null,
|
||||
extraKeys: null
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("hintOptions", null);
|
||||
};
|
||||
/* eslint-enable */
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="colorpicker dropdown-menu">
|
||||
<div class="colorpicker-saturation">
|
||||
<i><b></b></i>
|
||||
</div>
|
||||
<div class="colorpicker-hue">
|
||||
<i class="ColorPicker-ball"></i>
|
||||
</div>
|
||||
<div class="colorpicker-alpha js-alpha">
|
||||
<i class="ColorPicker-ball"></i>
|
||||
</div>
|
||||
<div class="ColorPicker-colorWrapper">
|
||||
<div class="ColorPicker-color colorpicker-color js-color"></div>
|
||||
</div>
|
||||
</div>
|
||||
179
lib/assets/javascripts/builder/components/color-picker/color-picker.js
Executable file
179
lib/assets/javascripts/builder/components/color-picker/color-picker.js
Executable file
@@ -0,0 +1,179 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./template.tpl');
|
||||
var colorPickerTemplate = require('./color-picker-template.tpl');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
|
||||
require('bootstrap-colorpicker');
|
||||
|
||||
var ESCAPE_KEY_CODE = 27;
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'blur .js-hex': '_onChangeHex',
|
||||
'blur .js-inputColor': '_onChangeColorValue',
|
||||
'blur .js-a': '_onChangeOpacity'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
var opacity = opts.opacity != null ? opts.opacity : 1;
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
hex: opts.value,
|
||||
opacity: opacity
|
||||
});
|
||||
|
||||
this._onEscape = this._onEscapePressed.bind(this);
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var hex = this.model.get('hex');
|
||||
var rgb = Utils.hexToRGB(this._sanitizeHex());
|
||||
var color = _.extend(rgb, {
|
||||
hex,
|
||||
opacity: this.model.get('opacity'),
|
||||
opacityDisabled: this.options.disableOpacity
|
||||
});
|
||||
|
||||
this.$el.append(template(color));
|
||||
|
||||
var rgbaTemplate = _.template('rgba(<%- r %>, <%- g %>, <%- b %>, <%- opacity %>)');
|
||||
|
||||
this.$('.js-colorPicker').colorpicker({
|
||||
color: rgbaTemplate(color),
|
||||
format: 'rgba',
|
||||
container: true,
|
||||
horizontal: true,
|
||||
inline: true,
|
||||
customClass: 'ColorPicker',
|
||||
template: colorPickerTemplate(),
|
||||
slidersHorz: {
|
||||
saturation: {
|
||||
maxLeft: 206,
|
||||
maxTop: 95,
|
||||
callLeft: 'setSaturation',
|
||||
callTop: 'setBrightness'
|
||||
},
|
||||
hue: {
|
||||
maxLeft: 180,
|
||||
maxTop: 0,
|
||||
callLeft: 'setHue',
|
||||
callTop: false
|
||||
},
|
||||
alpha: {
|
||||
maxLeft: 180,
|
||||
maxTop: 0,
|
||||
callLeft: 'setAlpha',
|
||||
callTop: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.disableOpacity) {
|
||||
this.$('.js-colorPicker .js-alpha').addClass('is-hidden');
|
||||
}
|
||||
|
||||
var setNewColor = function (e) {
|
||||
var rgb = e.color.toRGB();
|
||||
var hex = e.color.toHex();
|
||||
this.model.set({ hex: hex, r: rgb.r, g: rgb.g, b: rgb.b, opacity: rgb.a });
|
||||
this.$('.js-color').css('opacity', rgb.a);
|
||||
}.bind(this);
|
||||
|
||||
this.$('.js-colorPicker').colorpicker().on('changeColor', setNewColor);
|
||||
|
||||
this._initDocumentBinds();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:hex', this._onChangeColor, this);
|
||||
this.model.bind('change:r', this._onChangeColor, this);
|
||||
this.model.bind('change:g', this._onChangeColor, this);
|
||||
this.model.bind('change:b', this._onChangeColor, this);
|
||||
this.model.bind('change:opacity', this._onChangeColor, this);
|
||||
},
|
||||
|
||||
_onEscapePressed: function (ev) {
|
||||
if (ev.which === ESCAPE_KEY_CODE) {
|
||||
this.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_initDocumentBinds: function () {
|
||||
$(document).on('keydown', this._onEscape);
|
||||
},
|
||||
|
||||
_destroyDocumentBinds: function () {
|
||||
$(document).off('keydown', this._onEscape);
|
||||
},
|
||||
|
||||
_sanitizeHex: function () {
|
||||
return Utils.sanitizeHex(this.model.get('hex'));
|
||||
},
|
||||
|
||||
_onChangeColor: function () {
|
||||
this.trigger('change', {
|
||||
opacity: this.model.get('opacity'),
|
||||
hex: this.model.get('hex')
|
||||
}, this);
|
||||
|
||||
this.$('.js-hex').val(this.model.get('hex'));
|
||||
this.$('.js-r').val(this.model.get('r'));
|
||||
this.$('.js-g').val(this.model.get('g'));
|
||||
this.$('.js-b').val(this.model.get('b'));
|
||||
this.$('.js-a').val(this.model.get('opacity'));
|
||||
},
|
||||
|
||||
_onChangeOpacity: function () {
|
||||
var opacity = +this.$('.js-a').val();
|
||||
if (_.isNumber(opacity) && opacity >= 0 && opacity <= 1) {
|
||||
this.setColor(this.model.get('hex'), opacity);
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeColorValue: function (e) {
|
||||
this.$(e.target).removeClass('has-error');
|
||||
|
||||
var r = +this.$('.js-r').val();
|
||||
var g = +this.$('.js-g').val();
|
||||
var b = +this.$('.js-b').val();
|
||||
|
||||
var hex = Utils.rgbToHex(r, g, b);
|
||||
|
||||
if (Utils.isValidHex(hex)) {
|
||||
this.setColor(hex);
|
||||
} else {
|
||||
this.$(e.target).addClass('has-error');
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeHex: function (e) {
|
||||
this.$(e.target).removeClass('has-error');
|
||||
|
||||
var hex = this.$('.js-hex').val();
|
||||
|
||||
if (Utils.isValidHex(hex)) {
|
||||
this.setColor(hex);
|
||||
} else {
|
||||
this.$(e.target).addClass('has-error');
|
||||
}
|
||||
},
|
||||
|
||||
setColor: function (hex, opacity) {
|
||||
if (_.isUndefined(opacity)) {
|
||||
opacity = this.model.get('opacity') || 1;
|
||||
}
|
||||
|
||||
this.$('.js-colorPicker').colorpicker('setValue', Utils.hexToRGBA(hex, opacity));
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._destroyDocumentBinds();
|
||||
this.trigger('onClean', this);
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
25
lib/assets/javascripts/builder/components/color-picker/template.tpl
Executable file
25
lib/assets/javascripts/builder/components/color-picker/template.tpl
Executable file
@@ -0,0 +1,25 @@
|
||||
<div class="Editor-boxModalContent">
|
||||
<div class="ColorPicker-pickerWrapper js-colorPicker"></div>
|
||||
<div class="ColorPicker-inputs">
|
||||
<div class="ColorPicker-inputWrapper CDB-Text">
|
||||
<input type="text" class="CDB-Text CDB-InputText ColorPicker-input is-color js-hex" value="<%- hex %>"/>
|
||||
<span class="ColorPicker-inputLabel u-upperCase">HEX</span>
|
||||
</div>
|
||||
<div class="ColorPicker-inputWrapper CDB-Text">
|
||||
<input type="text" class="CDB-Text CDB-InputText ColorPicker-input js-inputColor js-r" value="<%- r %>"/>
|
||||
<span class="ColorPicker-inputLabel u-upperCase">R</span>
|
||||
</div>
|
||||
<div class="ColorPicker-inputWrapper CDB-Text">
|
||||
<input type="text" class="CDB-Text CDB-InputText ColorPicker-input js-inputColor js-g" value="<%- g %>" />
|
||||
<span class="ColorPicker-inputLabel u-upperCase">G</span>
|
||||
</div>
|
||||
<div class="ColorPicker-inputWrapper CDB-Text">
|
||||
<input type="text" class="CDB-Text CDB-InputText ColorPicker-input js-inputColor js-b" value="<%- b %>" />
|
||||
<span class="ColorPicker-inputLabel u-upperCase">B</span>
|
||||
</div>
|
||||
<div class="ColorPicker-inputWrapper CDB-Text">
|
||||
<input type="text" class="CDB-Text CDB-InputText ColorPicker-input js-a<% if (opacityDisabled) { %> is-disabled<% } %>" value="<%- opacity %>" <% if (opacityDisabled) { %>disabled<% } %> />
|
||||
<span class="ColorPicker-inputLabel u-upperCase">A</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
111
lib/assets/javascripts/builder/components/context-menu-factory-view.js
Executable file
111
lib/assets/javascripts/builder/components/context-menu-factory-view.js
Executable file
@@ -0,0 +1,111 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var ContextMenuView = require('./context-menu/context-menu-view');
|
||||
var CustomListCollection = require('./custom-list/custom-list-collection');
|
||||
var template = require('./context-menu-factory.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var TipsyTooltipView = require('./tipsy-tooltip-view');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'menuItems'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-toggle-menu': '_onToggleContextMenuClicked'
|
||||
},
|
||||
|
||||
className: 'CDB-Shape',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this.template = opts.template || template;
|
||||
this._initContextMenu(this._menuItems);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(this.template());
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var tooltip = new TipsyTooltipView({
|
||||
el: this.$el,
|
||||
title: function () {
|
||||
return _t('more-options');
|
||||
},
|
||||
gravity: 'w'
|
||||
});
|
||||
|
||||
this.addView(tooltip);
|
||||
},
|
||||
|
||||
_initContextMenu: function (items) {
|
||||
this._menuItems = new CustomListCollection(items);
|
||||
this._menuItems.on('change:selected', this._onContextMenuSelect, this);
|
||||
this.add_related_model(this._menuItems);
|
||||
},
|
||||
|
||||
_onContextMenuSelect: function (menuItem) {
|
||||
var action = menuItem.get('action');
|
||||
action && action.call(this);
|
||||
},
|
||||
|
||||
_showContextMenu: function (position) {
|
||||
var triggerElementID = 'context-menu-trigger-' + this.cid;
|
||||
this.$('.js-toggle-menu').attr('id', triggerElementID);
|
||||
|
||||
var menuItems = this._menuItems;
|
||||
this._resetContextMenuItems();
|
||||
this._menuView = new ContextMenuView({
|
||||
collection: menuItems,
|
||||
triggerElementID: triggerElementID,
|
||||
position: position
|
||||
});
|
||||
|
||||
this._menuView.model.on('change:visible', function (model, isContextMenuVisible) {
|
||||
if (this._hasContextMenu() && !isContextMenuVisible) {
|
||||
this._hideContextMenu();
|
||||
}
|
||||
}, this);
|
||||
|
||||
this._menuView.show();
|
||||
this.addView(this._menuView);
|
||||
},
|
||||
|
||||
_resetContextMenuItems: function () {
|
||||
var selected = this._menuItems.getSelectedItem();
|
||||
selected && selected.set({selected: false}, {silent: true});
|
||||
},
|
||||
|
||||
_hasContextMenu: function () {
|
||||
return this._menuView != null;
|
||||
},
|
||||
|
||||
_hideContextMenu: function () {
|
||||
this.removeView(this._menuView);
|
||||
this._menuView.clean();
|
||||
delete this._menuView;
|
||||
},
|
||||
|
||||
_onToggleContextMenuClicked: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this._hasContextMenu()) {
|
||||
this._hideContextMenu();
|
||||
} else {
|
||||
this._showContextMenu({
|
||||
x: event.pageX,
|
||||
y: event.pageY
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getContextMenu: function () {
|
||||
return this._menuView;
|
||||
}
|
||||
});
|
||||
5
lib/assets/javascripts/builder/components/context-menu-factory.tpl
Executable file
5
lib/assets/javascripts/builder/components/context-menu-factory.tpl
Executable file
@@ -0,0 +1,5 @@
|
||||
<button class="CDB-Shape-threePoints is-blue is-small js-toggle-menu">
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
</button>
|
||||
139
lib/assets/javascripts/builder/components/context-menu/context-menu-view.js
Executable file
139
lib/assets/javascripts/builder/components/context-menu/context-menu-view.js
Executable file
@@ -0,0 +1,139 @@
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CustomListView = require('builder/components/custom-list/custom-list-view');
|
||||
var CustomListItemView = require('builder/components/custom-list/custom-list-item-view');
|
||||
var itemTemplate = require('builder/components/custom-list/custom-list-item.tpl');
|
||||
var magicPositioner = require('builder/helpers/magic-positioner');
|
||||
var DropdownOverlayView = require('builder/components/dropdown-overlay/dropdown-overlay-view');
|
||||
|
||||
var ESCAPE_KEY_CODE = 27;
|
||||
|
||||
/*
|
||||
* A context menu
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
options: {
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 15
|
||||
},
|
||||
itemTemplate: itemTemplate,
|
||||
itemView: CustomListItemView
|
||||
},
|
||||
|
||||
className: 'CDB-Box-modal CDB-SelectItem CustomList CustomList--small',
|
||||
tagName: 'div',
|
||||
|
||||
initialize: function (opts) {
|
||||
opts = opts || {};
|
||||
if (!opts.collection) throw new Error('collection option is required');
|
||||
if (!opts.triggerElementID) throw new Error('element id is required');
|
||||
|
||||
this._triggerElementID = opts.triggerElementID;
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
visible: false
|
||||
});
|
||||
|
||||
this._onEscapePressed = this._onEscapePressed.bind(this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.on('change:visible', function (mdl, isVisible) {
|
||||
this.render();
|
||||
if (isVisible) {
|
||||
this._initDocumentBinds();
|
||||
} else {
|
||||
this._destroyDocumentBinds();
|
||||
}
|
||||
}, this);
|
||||
this.collection.on('change:selected', this.hide, this);
|
||||
this.add_related_model(this.collection);
|
||||
},
|
||||
|
||||
_initDocumentBinds: function () {
|
||||
$(document).on('keydown', this._onEscapePressed);
|
||||
},
|
||||
|
||||
_destroyDocumentBinds: function () {
|
||||
$(document).off('keydown', this._onEscapePressed);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var $body = $('body');
|
||||
var posX = (this.options.position.x || 0) + this.options.offset.x;
|
||||
var posY = (this.options.position.y || 0) + this.options.offset.y;
|
||||
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this._renderList();
|
||||
|
||||
this.$el.toggle(this.isVisible());
|
||||
|
||||
$body.append(this.el);
|
||||
|
||||
this.$el.css(
|
||||
magicPositioner({
|
||||
parentView: $body,
|
||||
posX: posX,
|
||||
posY: posY
|
||||
})
|
||||
);
|
||||
|
||||
this.dropdownOverlay = new DropdownOverlayView({
|
||||
visible: this.isVisible(),
|
||||
onClickAction: this.hide.bind(this)
|
||||
});
|
||||
this.addView(this.dropdownOverlay);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onEscapePressed: function (ev) {
|
||||
if (ev.which === ESCAPE_KEY_CODE) {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_renderList: function () {
|
||||
this._listView = new CustomListView({
|
||||
model: this.model,
|
||||
collection: this.collection,
|
||||
typeLabel: '',
|
||||
itemView: this.options.itemView,
|
||||
itemTemplate: this.options.itemTemplate,
|
||||
size: 5
|
||||
});
|
||||
this.$el.append(this._listView.render().el);
|
||||
this.addView(this._listView);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.model.set('visible', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.model.set('visible', false);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return this.model.get('visible');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._destroyDocumentBinds();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
var Backbone = require('backbone');
|
||||
var CustomCarouselModel = require('./custom-carousel-item-model');
|
||||
|
||||
/*
|
||||
* Custom list collection, it parses pairs like:
|
||||
*
|
||||
* [{ val, label }]
|
||||
* ["string"]
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var d = {};
|
||||
if (typeof attrs === 'string') {
|
||||
d.val = attrs;
|
||||
} else {
|
||||
d = attrs;
|
||||
}
|
||||
return new CustomCarouselModel(d);
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:selected', this._onSelectedChange, this);
|
||||
},
|
||||
|
||||
_onSelectedChange: function (changedModel, isSelected) {
|
||||
if (isSelected) {
|
||||
this.each(function (m) {
|
||||
if (m.cid !== changedModel.cid && m.get('selected')) {
|
||||
m.set('selected', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getSelected: function () {
|
||||
return this.findWhere({ selected: true });
|
||||
},
|
||||
|
||||
getSelectedValue: function () {
|
||||
var selectedModel = this.getSelected();
|
||||
return selectedModel && selectedModel.get('val');
|
||||
},
|
||||
|
||||
getHighlighted: function () {
|
||||
return this.findWhere({ highlighted: true });
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/*
|
||||
* List item model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
selected: false,
|
||||
label: '',
|
||||
template: function () {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
getName: function () {
|
||||
return this.get('label') || this.getValue();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.get('val');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./custom-carousel-item.tpl');
|
||||
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'Carousel-item',
|
||||
tagName: 'li',
|
||||
|
||||
events: {
|
||||
'mouseenter': '_onMouseEnter',
|
||||
'mouseleave': '_onMouseLeave',
|
||||
'click': '_onClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this._itemClassName = opts && opts.itemOptions ? opts.itemOptions.className : '';
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// NOTE: this function calls destroyTipsy for each tooltip, which calls:
|
||||
// this.$el.unbind('mouseleave mouseenter');
|
||||
// So we need to bind the events again after calling clearSubViews
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.bind('mouseenter', this._onMouseEnter.bind(this));
|
||||
this.$el.bind('mouseleave', this._onMouseLeave.bind(this));
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
name: this.model.getName(),
|
||||
className: this._itemClassName,
|
||||
template: this.model.get('template')()
|
||||
})
|
||||
);
|
||||
|
||||
if (this.model.getValue()) {
|
||||
this.$el.addClass('js-' + this.model.getValue());
|
||||
}
|
||||
this.$el.toggleClass('is-selected', !!this.model.get('selected'));
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
if (this.model.get('tooltip')) {
|
||||
var tooltip = new TipsyTooltipView({
|
||||
el: this.$el,
|
||||
gravity: 's',
|
||||
title: function () {
|
||||
return this.model.get('tooltip');
|
||||
}.bind(this)
|
||||
});
|
||||
this.addView(tooltip);
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:selected', this.render, this);
|
||||
},
|
||||
|
||||
_onMouseEnter: function () {
|
||||
this.model.set('highlighted', true);
|
||||
},
|
||||
|
||||
_onMouseLeave: function () {
|
||||
this.model.set('highlighted', false);
|
||||
},
|
||||
|
||||
_onClick: function (e) {
|
||||
this.killEvent(e);
|
||||
this.model.set('selected', true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<button<% if (className) { %> class="<%- className %>"<% } %>>
|
||||
<%= template %>
|
||||
</button>
|
||||
@@ -0,0 +1,137 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
var template = require('./custom-carousel.tpl');
|
||||
var CarouselCollection = require('./custom-carousel-collection');
|
||||
var CarouselItemView = require('./custom-carousel-item-view');
|
||||
var _ = require('underscore');
|
||||
/*
|
||||
* A custom carousel selector
|
||||
*
|
||||
* It accepts a collection of (val, label) model attributes or a values array
|
||||
* with the same content or only strings.
|
||||
*
|
||||
* new CustomCarousel({
|
||||
* options: [
|
||||
* {
|
||||
* val: 'hello',
|
||||
* label: 'hi'
|
||||
* }
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'Carousel',
|
||||
tagName: 'div',
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.collection) {
|
||||
if (!opts.options) { throw new Error('options array {value, label} is required'); }
|
||||
this.collection = new CarouselCollection(opts.options);
|
||||
this.options = opts;
|
||||
}
|
||||
this._bindedCheckShadows = this._checkShadows.bind(this);
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(template());
|
||||
this._renderList();
|
||||
this._applyCustomScroll();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.collection.bind('change:selected', this._checkScroll, this);
|
||||
},
|
||||
|
||||
_renderList: function () {
|
||||
this.collection.each(function (model) {
|
||||
this._createItem(model);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_createItem: function (model) {
|
||||
var opts = this.options;
|
||||
var className = opts && opts.listItemOptions ? opts.listItemOptions.className : 'Carousel-item';
|
||||
var view = new CarouselItemView({
|
||||
className: className,
|
||||
model: model,
|
||||
itemOptions: this.options.itemOptions
|
||||
});
|
||||
|
||||
this._listContainer().append(view.render().el);
|
||||
this.addView(view);
|
||||
},
|
||||
|
||||
_applyCustomScroll: function () {
|
||||
Ps.initialize(this._listContainer().get(0), {
|
||||
wheelSpeed: 1,
|
||||
wheelPropagation: false,
|
||||
swipePropagation: true,
|
||||
suppressScrollY: true,
|
||||
stopPropagationOnClick: false,
|
||||
minScrollbarLength: 120,
|
||||
useBothWheelAxes: true
|
||||
});
|
||||
this._checkScroll();
|
||||
this._bindScroll();
|
||||
},
|
||||
|
||||
_destroyCustomScroll: function () {
|
||||
this._unbindScroll();
|
||||
Ps.destroy(this._listContainer().get(0));
|
||||
},
|
||||
|
||||
_bindScroll: function () {
|
||||
this._listContainer()
|
||||
.on('ps-x-reach-start', this._bindedCheckShadows)
|
||||
.on('ps-x-reach-end', this._bindedCheckShadows)
|
||||
.on('ps-scroll-x', this._bindedCheckShadows);
|
||||
},
|
||||
|
||||
_unbindScroll: function () {
|
||||
this._listContainer()
|
||||
.off('ps-x-reach-start', this._bindedCheckShadows)
|
||||
.off('ps-x-reach-end', this._bindedCheckShadows)
|
||||
.off('ps-scroll-x', this._bindedCheckShadows);
|
||||
},
|
||||
|
||||
_checkScroll: function () {
|
||||
var position = this.$('.is-selected').position();
|
||||
if (position) {
|
||||
this._listContainer().scrollLeft(position.left - 20);
|
||||
this._bindedCheckShadows();
|
||||
}
|
||||
},
|
||||
|
||||
_listContainer: function () {
|
||||
return this.$('.js-list');
|
||||
},
|
||||
|
||||
_checkShadows: function () {
|
||||
var currentPos = this._listContainer().scrollLeft();
|
||||
var max = this._listContainer().get(0).scrollWidth;
|
||||
var width = this._listContainer().outerWidth();
|
||||
var maxPos = max - width;
|
||||
|
||||
this.$('.js-leftShadow').toggleClass('is-visible', currentPos > 0);
|
||||
this.$('.js-rightShadow').toggleClass('is-visible', currentPos < maxPos);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._destroyCustomScroll();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
},
|
||||
|
||||
initScroll: function () {
|
||||
setTimeout(_.bind(function () {
|
||||
this._checkShadows();
|
||||
this._checkScroll();
|
||||
Ps.update(this._listContainer().get(0));
|
||||
}, this), 0);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="Carousel-shadow Carousel-shadow--left js-leftShadow"></div>
|
||||
<div class="Carousel-shadow Carousel-shadow--right is-visible js-rightShadow"></div>
|
||||
<ul class="Carousel-list js-list"></ul>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="CDB-ListDecoration-itemLink u-flex u-justifySpace u-alignCenter u-actionTextColor <% if (isSelected) { %> is-selected <% } %>">
|
||||
<%- _t('form-components.editors.fill.quantification.methods.' + name) %>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CustomListView = require('builder/components/custom-list/custom-view');
|
||||
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
|
||||
var ColumnListViewTemplate = require('builder/components/custom-list/column-list/column-list-view.tpl');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
defaults: {
|
||||
headerTitle: ''
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-back': '_onClickBack'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.stackLayoutModel) throw new Error('stackLayoutModel is required');
|
||||
if (!opts.columns) throw new Error('columns param is required');
|
||||
|
||||
this._stackLayoutModel = opts.stackLayoutModel;
|
||||
this._columns = opts.columns;
|
||||
this._showSearch = opts.showSearch || false;
|
||||
this._typeLabel = opts.typeLabel;
|
||||
this._itemTemplate = opts.itemTemplate;
|
||||
|
||||
this.collection = new CustomListCollection(this._columns);
|
||||
this.listenTo(this.collection, 'change:selected', this._onSelectItem);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this.$el.append(
|
||||
ColumnListViewTemplate({
|
||||
headerTitle: this.options.headerTitle
|
||||
})
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onClickBack: function (e) {
|
||||
this.killEvent(e);
|
||||
this.trigger('back', this);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._listView = new CustomListView({
|
||||
collection: this.collection,
|
||||
showSearch: this._showSearch,
|
||||
typeLabel: this._typeLabel,
|
||||
itemTemplate: this._itemTemplate
|
||||
});
|
||||
this.$('.js-content').append(this._listView.render().$el);
|
||||
this.addView(this._listView);
|
||||
},
|
||||
|
||||
_onSelectItem: function (item) {
|
||||
this.trigger('selectItem', item, this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<% if (headerTitle) { %>
|
||||
<div class="CDB-Box-modalHeader">
|
||||
<ul class="CDB-Box-modalHeaderItem CDB-Box-modalHeaderItem--block CDB-Box-modalHeaderItem--paddingHorizontal">
|
||||
<li class="CDB-ListDecoration-item CDB-ListDecoration-itemPadding--vertical CDB-Text CDB-Size-medium u-secondaryTextColor">
|
||||
<button class="u-actionTextColor js-back u-rSpace">
|
||||
<i class="CDB-IconFont CDB-IconFont-arrowPrev Size-large"></i>
|
||||
</button>
|
||||
<%- headerTitle%>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="js-content"></div>
|
||||
@@ -0,0 +1,32 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./custom-list-action.tpl');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'CDB-Text CDB-Size-small is-semibold u-upperCase u-actionTextColor u-lSpace--xl',
|
||||
|
||||
tagName: 'button',
|
||||
|
||||
events: {
|
||||
'click': '_onClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.options = opts;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.empty();
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.append(template({
|
||||
label: this.options.label
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onClick: function () {
|
||||
this.options.action && this.options.action();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<%- label %>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="CDB-ListDecoration-item CustomList-item js-listItem">
|
||||
<button type="button" class="CDB-ListDecoration-itemLink CustomList--full js-add-custom-value u-ellipsis" data-val="<%- query %>" title="<%- query %>">
|
||||
<p class="CDB-Text CDB-FontSize-small u-altTextColor"><%- _t('components.custom-list.add-custom-result') %></p>
|
||||
“<%- query %>”
|
||||
</button>
|
||||
</div>
|
||||
122
lib/assets/javascripts/builder/components/custom-list/custom-list-collection.js
Executable file
122
lib/assets/javascripts/builder/components/custom-list/custom-list-collection.js
Executable file
@@ -0,0 +1,122 @@
|
||||
var Backbone = require('backbone');
|
||||
var CustomListItemModel = require('./custom-list-item-model');
|
||||
var _ = require('underscore');
|
||||
|
||||
/*
|
||||
* Custom list collection, it parses pairs like:
|
||||
*
|
||||
* [{ val, label }]
|
||||
* ["string"]
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
sort_key: 'id', // default sort key
|
||||
|
||||
initialize: function (models, options) {
|
||||
this.options = _.extend({ silent: true }, options);
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
comparator: function (item) {
|
||||
var name = item.get(this.sort_key);
|
||||
|
||||
return this._nameToLowerCase(name);
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var d = {};
|
||||
if (typeof attrs === 'string') {
|
||||
d.val = attrs;
|
||||
} else {
|
||||
d = attrs;
|
||||
}
|
||||
return new CustomListItemModel(d, opts);
|
||||
},
|
||||
|
||||
sortByKey: function (key) {
|
||||
this.sort_key = key;
|
||||
this.sort();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:selected', this._onSelectedChange, this);
|
||||
},
|
||||
|
||||
_nameToLowerCase: function (name) {
|
||||
/*
|
||||
* It could still be evaluated like true if it is a boolean/number
|
||||
* in that case, we convert it to a string
|
||||
*/
|
||||
|
||||
if (_.isUndefined(name) || _.isNull(name)) return name;
|
||||
|
||||
return _.isString(name) ? name.toLowerCase() : name.toString().toLowerCase();
|
||||
},
|
||||
|
||||
search: function (query) {
|
||||
if (!query) return this;
|
||||
query = query.toLowerCase();
|
||||
|
||||
return _(this.filter(function (model) {
|
||||
var name = model.getName();
|
||||
var val = this._nameToLowerCase(name);
|
||||
|
||||
return val ? ~val.indexOf(query) : -1;
|
||||
}, this));
|
||||
},
|
||||
|
||||
_onSelectedChange: function (changedModel, isSelected) {
|
||||
if (isSelected) {
|
||||
this.each(function (m) {
|
||||
if (m.cid !== changedModel.cid) {
|
||||
m.set({
|
||||
selected: false
|
||||
}, {
|
||||
silent: this.options.silent
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
getSelectedItem: function () {
|
||||
return this.findWhere({ selected: true });
|
||||
},
|
||||
|
||||
containsValue: function (value) {
|
||||
return this.find(function (mdl) {
|
||||
return mdl.getValue() === value;
|
||||
});
|
||||
},
|
||||
|
||||
setSelected: function (value) {
|
||||
var selectedModel;
|
||||
var silent = { silent: this.options.silent };
|
||||
|
||||
this.each(function (mdl) {
|
||||
if (mdl.getValue() === value) {
|
||||
mdl.set({
|
||||
selected: true
|
||||
}, silent);
|
||||
selectedModel = mdl;
|
||||
} else {
|
||||
mdl.set({
|
||||
selected: false
|
||||
}, silent);
|
||||
}
|
||||
});
|
||||
return selectedModel;
|
||||
},
|
||||
|
||||
removeSelected: function () {
|
||||
this.each(function (mdl) {
|
||||
mdl.set({
|
||||
selected: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
isAsync: function () {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="CustomList-message">
|
||||
<p class="CustomList-messageText CDB-Text CDB-Size-medium u-secondaryTextColor">
|
||||
<% if (query && query.length) { %>
|
||||
<%- _t('components.custom-list.no-results', { typeLabel: typeLabel, query: query }) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.custom-list.no-items', { typeLabel: typeLabel }) %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="CDB-Box-modalHeader">
|
||||
<div class="CDB-Box-modalHeaderItem js-header u-alignCenter u-justifySpace">
|
||||
<div class="u-flex js-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/*
|
||||
* List item model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
selected: false
|
||||
},
|
||||
|
||||
getName: function () {
|
||||
return (this.get('label') == null) ? this.getValue() : this.get('label'); // eslint-disable-line
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.get('val');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
options: {
|
||||
template: require('./custom-list-item.tpl')
|
||||
},
|
||||
|
||||
className: 'CDB-ListDecoration-item CustomList-item js-listItem',
|
||||
tagName: 'li',
|
||||
|
||||
events: {
|
||||
'mouseenter': '_onMouseEnter',
|
||||
'mouseleave': '_onMouseLeave',
|
||||
'click': '_onClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.options = _.extend({}, this.options, opts);
|
||||
this.model.on('change', this.render, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.empty();
|
||||
this.clearSubViews();
|
||||
|
||||
var name = this.model.getName() == null ? 'null' : this.model.getName();
|
||||
|
||||
this.$el.append(
|
||||
this.options.template(
|
||||
_.extend({
|
||||
typeLabel: this.options.typeLabel,
|
||||
isSelected: this.model.get('selected'),
|
||||
isDisabled: this.model.get('disabled'),
|
||||
isDestructive: this.model.get('destructive'),
|
||||
name: name,
|
||||
val: this.model.getValue(),
|
||||
options: this.model.get('renderOptions')
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.$el
|
||||
.attr('data-val', this.model.getValue())
|
||||
.toggleClass('is-disabled', !!this.model.get('disabled'));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onMouseLeave: function () {
|
||||
this.$el.removeClass('is-highlighted');
|
||||
},
|
||||
|
||||
_onMouseEnter: function () {
|
||||
this.$el.addClass('is-highlighted');
|
||||
},
|
||||
|
||||
_onClick: function (ev) {
|
||||
this.killEvent(ev);
|
||||
this.model.set({
|
||||
selectedClass: ev.target.classList,
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<button type="button" class="CDB-ListDecoration-itemLink u-ellipsis
|
||||
<% if (isSelected) { %> is-selected <% } %>
|
||||
<% if (isDestructive) { %>
|
||||
u-errorTextColor
|
||||
<% } else if (isDisabled) { %>
|
||||
u-hintTextColor
|
||||
<% } else { %>
|
||||
u-actionTextColor
|
||||
<% } %>
|
||||
" title="<%- name %>">
|
||||
|
||||
<div class="u-iBlock u-rSpace">
|
||||
<input class="CDB-Checkbox js-input" type="checkbox" name="" value="" <% if (isSelected) { %>checked<% } %> <% if (isDisabled) { %>disabled<% } %> />
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</div>
|
||||
<%- name %>
|
||||
</button>
|
||||
12
lib/assets/javascripts/builder/components/custom-list/custom-list-item.tpl
Executable file
12
lib/assets/javascripts/builder/components/custom-list/custom-list-item.tpl
Executable file
@@ -0,0 +1,12 @@
|
||||
<button type="button" class="CDB-ListDecoration-itemLink u-ellipsis
|
||||
<% if (isSelected) { %> is-selected <% } %>
|
||||
<% if (isDestructive) { %>
|
||||
u-errorTextColor
|
||||
<% } else if (isDisabled) { %>
|
||||
u-hintTextColor
|
||||
<% } else { %>
|
||||
u-actionTextColor
|
||||
<% } %>
|
||||
" title="<%- name %>">
|
||||
<%- name %>
|
||||
</button>
|
||||
@@ -0,0 +1,40 @@
|
||||
var _ = require('underscore');
|
||||
var CustomListCollection = require('./custom-list-collection');
|
||||
|
||||
module.exports = CustomListCollection.extend({
|
||||
_initBinds: function () { },
|
||||
|
||||
setSelected: function (value) {
|
||||
var selectedModel;
|
||||
var silentTrue = { silent: true };
|
||||
|
||||
if (_.isArray(value)) {
|
||||
this.each(function (mdl) {
|
||||
if (_.contains(value, mdl.getValue())) {
|
||||
mdl.set({
|
||||
selected: true
|
||||
}, silentTrue);
|
||||
selectedModel = mdl;
|
||||
} else {
|
||||
mdl.set({
|
||||
selected: false
|
||||
}, silentTrue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.each(function (mdl) {
|
||||
if (mdl.getValue() === value) {
|
||||
mdl.set({
|
||||
selected: true
|
||||
}, silentTrue);
|
||||
selectedModel = mdl;
|
||||
} else {
|
||||
mdl.set({
|
||||
selected: false
|
||||
}, silentTrue);
|
||||
}
|
||||
});
|
||||
}
|
||||
return selectedModel;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
var CustomListItemView = require('./custom-list-item-view');
|
||||
|
||||
module.exports = CustomListItemView.extend({
|
||||
_onClick: function (ev) {
|
||||
this.killEvent(ev);
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./custom-list-search.tpl');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'CustomList--full',
|
||||
tagName: 'form',
|
||||
|
||||
events: {
|
||||
'keyup .js-search': '_onSearchType',
|
||||
'click .js-clear': '_onClickClear',
|
||||
'submit': '_submit'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.template = this.options.template || template;
|
||||
this.searchPlaceholder = this.options.searchPlaceholder || _t('components.custom-list.placeholder', { typeLabel: this.options.typeLabel });
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el
|
||||
.empty()
|
||||
.append(
|
||||
this.template({
|
||||
query: this.model.get('query'),
|
||||
searchPlaceholder: this.searchPlaceholder
|
||||
})
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.on('change:query', this._checkButtons, this);
|
||||
},
|
||||
|
||||
_checkButtons: function () {
|
||||
var query = this.model.get('query');
|
||||
this.$('.js-clear').toggleClass('u-transparent', query.length === 0);
|
||||
},
|
||||
|
||||
_onSearchType: function (e) {
|
||||
this._submit();
|
||||
},
|
||||
|
||||
_onClickClear: function (e) {
|
||||
e.stopPropagation();
|
||||
this.model.set('query', '');
|
||||
this.render();
|
||||
this.focus();
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
this.$('.js-search').focus();
|
||||
},
|
||||
|
||||
_submit: function (ev) {
|
||||
this.killEvent(ev);
|
||||
var query = this.$('.js-search').val();
|
||||
this.model.set('query', query);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="u-flex">
|
||||
<input type="text" name="text" autocomplete="off" value="<%- query %>" placeholder="<%- searchPlaceholder %>" class="CDB-InputTextPlain CDB-Text js-search">
|
||||
<button type="button" class="u-lSpace--xl u-transparent js-clear">
|
||||
<div class="CDB-Shape">
|
||||
<div class="CDB-Shape-close is-blue is-large"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
234
lib/assets/javascripts/builder/components/custom-list/custom-list-view.js
Executable file
234
lib/assets/javascripts/builder/components/custom-list/custom-list-view.js
Executable file
@@ -0,0 +1,234 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
var emptyTemplate = require('./custom-list-empty.tpl');
|
||||
var addTemplate = require('./custom-list-add.tpl');
|
||||
var template = require('./custom-list.tpl');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
|
||||
var ARROW_DOWN_KEY_CODE = 40;
|
||||
var ARROW_UP_KEY_CODE = 38;
|
||||
var ENTER_KEY_CODE = 13;
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components:custom-list:custom-list-view',
|
||||
|
||||
options: {
|
||||
size: 3
|
||||
},
|
||||
|
||||
className: 'CDB-Text CDB-Size-medium CustomList-listWrapper',
|
||||
|
||||
tagName: 'div',
|
||||
|
||||
events: {
|
||||
'click .js-add-custom-value': '_onClickAddCustomValue',
|
||||
'mouseover': '_onMouseOver',
|
||||
'mouseout': '_onMouseOut'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.options = _.extend({}, this.options, opts);
|
||||
|
||||
this._onKeyDownBinded = this._onKeyDown.bind(this);
|
||||
this._needsMaxSize = true;
|
||||
|
||||
if (this.options.mouseOverAction) {
|
||||
this._mouseOverAction = this.options.mouseOverAction;
|
||||
}
|
||||
|
||||
if (this.options.mouseOutAction) {
|
||||
this._mouseOutAction = this.options.mouseOutAction;
|
||||
}
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this._removeArrowBinds();
|
||||
this._destroyCustomScroll();
|
||||
this.$el.empty();
|
||||
|
||||
var query = this.model.get('query');
|
||||
var items = this.collection.search(query);
|
||||
|
||||
this.$el.append(template());
|
||||
|
||||
var allowFreeTextInput = this.options.allowFreeTextInput;
|
||||
|
||||
if (allowFreeTextInput && query && !Utils.isBlank(query)) {
|
||||
if (!this.collection.containsValue(query)) {
|
||||
this.$el.prepend(
|
||||
addTemplate({
|
||||
query: query,
|
||||
typeLabel: this.options.typeLabel
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.size() > 0) {
|
||||
items.each(this._renderItem, this);
|
||||
this._applyArrowBinds();
|
||||
// Perfect-scroll needs to have the element in the DOM in order to
|
||||
// style/positionate the scroll properly, small trick
|
||||
setTimeout(this._applyCustomScroll.bind(this), 0);
|
||||
} else if (!allowFreeTextInput || Utils.isBlank(query)) {
|
||||
this.$el.append(
|
||||
emptyTemplate({
|
||||
query: query,
|
||||
typeLabel: this.options.typeLabel
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderItem: function (model) {
|
||||
if (model.get('hidden')) return;
|
||||
var ItemViewClass = this.options.itemView;
|
||||
|
||||
var itemView = new ItemViewClass({
|
||||
model: model,
|
||||
typeLabel: this.options.typeLabel,
|
||||
template: this.options.itemTemplate
|
||||
});
|
||||
this.$('.js-list').append(itemView.render().el);
|
||||
this.addView(itemView);
|
||||
|
||||
itemView.bind('customEvent', function (eventName, item) {
|
||||
this.trigger('customEvent', eventName, item, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:query', this._onQueryChanged, this);
|
||||
},
|
||||
|
||||
_applyArrowBinds: function () {
|
||||
document.addEventListener('keydown', this._onKeyDownBinded);
|
||||
},
|
||||
|
||||
_removeArrowBinds: function () {
|
||||
document.removeEventListener('keydown', this._onKeyDownBinded);
|
||||
},
|
||||
|
||||
_getSelected: function () {
|
||||
var selectedModel = this.collection.getSelectedItem();
|
||||
var selectedValue;
|
||||
|
||||
if (selectedModel) {
|
||||
selectedValue = selectedModel.getValue();
|
||||
return this.$("[data-val='" + selectedValue + "']");
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
_highlightSelected: function (item) {
|
||||
var itemHeight = item.outerHeight();
|
||||
item.addClass('is-highlighted');
|
||||
this.$('.js-list').scrollTop(item.index() * itemHeight);
|
||||
},
|
||||
|
||||
_onClickAddCustomValue: function (event) {
|
||||
this.killEvent(event);
|
||||
|
||||
var query = this.model.get('query');
|
||||
|
||||
var model = this.collection.add({
|
||||
val: query,
|
||||
label: '“' + query + '”',
|
||||
dirty: true
|
||||
});
|
||||
|
||||
this.collection.sortByKey('val');
|
||||
model.set('selected', true);
|
||||
},
|
||||
|
||||
_onKeyDown: function (event) {
|
||||
if (this.model.get('visible') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var key = event.which;
|
||||
var $listItems = this.$('.js-listItem');
|
||||
var $highlighted = $listItems.filter('.is-highlighted');
|
||||
var $current;
|
||||
var model;
|
||||
|
||||
$highlighted.removeClass('is-highlighted');
|
||||
|
||||
if (key === ARROW_DOWN_KEY_CODE) {
|
||||
if (!$highlighted.length || $highlighted[0] === $listItems.last()[0]) {
|
||||
$current = $listItems.eq(0);
|
||||
} else {
|
||||
$current = $highlighted.next();
|
||||
}
|
||||
} else if (key === ARROW_UP_KEY_CODE) {
|
||||
if (!$highlighted.length || $highlighted[0] === $listItems.first()[0]) {
|
||||
$current = $listItems.last();
|
||||
} else {
|
||||
$current = $highlighted.prev();
|
||||
}
|
||||
} else if (key === ENTER_KEY_CODE) {
|
||||
event.preventDefault();
|
||||
if ($highlighted && $highlighted.length) {
|
||||
model = _.first(this.collection.where({ val: $highlighted.data('val') }));
|
||||
if (model) {
|
||||
model.set('selected', true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$current && this._highlightSelected($current);
|
||||
},
|
||||
|
||||
_onQueryChanged: function () {
|
||||
var prevQuery = this.collection.findWhere({ val: this.model.previous('query'), dirty: true });
|
||||
this.collection.remove(prevQuery);
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
_applyCustomScroll: function () {
|
||||
Ps.initialize(this._wrapperContainer().get(0), {
|
||||
wheelSpeed: 2,
|
||||
wheelPropagation: true,
|
||||
stopPropagationOnClick: false,
|
||||
minScrollbarLength: 20
|
||||
});
|
||||
},
|
||||
|
||||
_destroyCustomScroll: function () {
|
||||
if (this._wrapperContainer().length > 0) {
|
||||
Ps.destroy(this._wrapperContainer().get(0));
|
||||
}
|
||||
},
|
||||
|
||||
_wrapperContainer: function () {
|
||||
return this.$('.js-list');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._removeArrowBinds();
|
||||
this._destroyCustomScroll();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
},
|
||||
|
||||
highlight: function () {
|
||||
var selected = this._getSelected();
|
||||
selected && this._highlightSelected(selected);
|
||||
},
|
||||
|
||||
_onMouseOver: function () {
|
||||
this._mouseOverAction && this._mouseOverAction();
|
||||
},
|
||||
|
||||
_onMouseOut: function () {
|
||||
this._mouseOutAction && this._mouseOutAction();
|
||||
}
|
||||
});
|
||||
1
lib/assets/javascripts/builder/components/custom-list/custom-list.tpl
Executable file
1
lib/assets/javascripts/builder/components/custom-list/custom-list.tpl
Executable file
@@ -0,0 +1 @@
|
||||
<ul class="CustomList-list js-list"></ul>
|
||||
221
lib/assets/javascripts/builder/components/custom-list/custom-view.js
Executable file
221
lib/assets/javascripts/builder/components/custom-list/custom-view.js
Executable file
@@ -0,0 +1,221 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CustomListCollection = require('./custom-list-collection');
|
||||
var SearchView = require('./custom-list-search-view');
|
||||
var CustomListView = require('./custom-list-view');
|
||||
var headerTemplate = require('./custom-list-header.tpl');
|
||||
var CustomListAction = require('./custom-list-action-view');
|
||||
var itemTemplate = require('./custom-list-item.tpl');
|
||||
var CustomListItemView = require('./custom-list-item-view');
|
||||
|
||||
/*
|
||||
* A custom list with possibility to search within values.
|
||||
*
|
||||
* It accepts a collection of (val, label) model attributes or a values array
|
||||
* with the same content or only strings.
|
||||
*
|
||||
* new CustomList({
|
||||
* showSearch: false,
|
||||
* itemTemplate: itemTemplate,
|
||||
* values: [
|
||||
* {
|
||||
* val: 'hello',
|
||||
* label: 'hi'
|
||||
* }
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components:custom-list:custom-view',
|
||||
|
||||
options: {
|
||||
showSearch: true,
|
||||
allowFreeTextInput: false,
|
||||
typeLabel: 'column',
|
||||
itemTemplate: itemTemplate,
|
||||
itemView: CustomListItemView
|
||||
},
|
||||
|
||||
className: 'CDB-Box-modal CustomList',
|
||||
tagName: 'div',
|
||||
|
||||
events: {
|
||||
'mouseover': '_onMouseOver',
|
||||
'mouseout': '_onMouseOut'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.collection) {
|
||||
if (!opts.options) { throw new Error('options array {value, label} is required'); }
|
||||
this.collection = new CustomListCollection(opts.options);
|
||||
}
|
||||
|
||||
if (opts.position) {
|
||||
this.$el.css(opts.position);
|
||||
}
|
||||
|
||||
this.options = _.extend({}, this.options, opts);
|
||||
this._selectModel = this.options.selectModel;
|
||||
|
||||
if (this.options.mouseOverAction) {
|
||||
this._mouseOverAction = this.options.mouseOverAction;
|
||||
}
|
||||
|
||||
if (this.options.mouseOutAction) {
|
||||
this._mouseOutAction = this.options.mouseOutAction;
|
||||
}
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
query: '',
|
||||
visible: false
|
||||
});
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.empty();
|
||||
this.clearSubViews();
|
||||
|
||||
if (this.options.showSearch || this.options.actions) {
|
||||
this._renderHeader();
|
||||
}
|
||||
|
||||
if (this.options.showSearch) {
|
||||
this._renderSearch();
|
||||
}
|
||||
|
||||
if (this.options.actions) {
|
||||
this._renderActions();
|
||||
}
|
||||
|
||||
this._renderList();
|
||||
|
||||
if (this.options.showSearch) {
|
||||
this._focusSearch();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:visible', this._onVisibleChanged, this);
|
||||
this.model.bind('change:query', this._setActionsVisibility, this);
|
||||
},
|
||||
|
||||
_renderHeader: function () {
|
||||
this.$el.prepend(headerTemplate());
|
||||
},
|
||||
|
||||
_renderSearch: function () {
|
||||
this._searchView = new SearchView({
|
||||
template: this.options.searchTemplate,
|
||||
typeLabel: this.options.typeLabel,
|
||||
searchPlaceholder: this.options.searchPlaceholder,
|
||||
model: this.model
|
||||
});
|
||||
this.$('.js-header').prepend(this._searchView.render().el);
|
||||
this.addView(this._searchView);
|
||||
},
|
||||
|
||||
_renderActions: function () {
|
||||
_.each(this.options.actions, function (action) {
|
||||
var view = new CustomListAction(action);
|
||||
this.$('.js-actions').append(view.render().el);
|
||||
this.addView(view);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_setActionsVisibility: function () {
|
||||
this.$('.js-actions').toggleClass('u-hide', this.model.get('query') !== '');
|
||||
},
|
||||
|
||||
_focusSearch: function () {
|
||||
setTimeout(function () {
|
||||
if (this._searchView) {
|
||||
this._searchView.focus();
|
||||
var input = this._searchView.$('input');
|
||||
var $initialVal = input.val();
|
||||
input.val($initialVal + ' ');
|
||||
input.val($initialVal);
|
||||
}
|
||||
}.bind(this), 0);
|
||||
},
|
||||
|
||||
_renderList: function () {
|
||||
this._listView = new CustomListView({
|
||||
model: this.model,
|
||||
allowFreeTextInput: this.options.allowFreeTextInput,
|
||||
collection: this.collection,
|
||||
typeLabel: this.options.typeLabel,
|
||||
itemView: this.options.itemView,
|
||||
itemTemplate: this.options.itemTemplate,
|
||||
size: this.options.size,
|
||||
mouseOverAction: this._mouseOverAction,
|
||||
mouseOutAction: this._mouseOutAction
|
||||
});
|
||||
this.$el.append(this._listView.render().el);
|
||||
|
||||
this._listView.highlight();
|
||||
this.addView(this._listView);
|
||||
|
||||
this._listView.bind('customEvent', function (eventName, item) {
|
||||
this.trigger(eventName, item, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
highlight: function () {
|
||||
this._listView.highlight();
|
||||
},
|
||||
|
||||
_onVisibleChanged: function (_model, isVisible) {
|
||||
this._resetQuery();
|
||||
this._toggleVisibility();
|
||||
|
||||
isVisible ? this.render() : this.clearSubViews();
|
||||
},
|
||||
|
||||
_resetQuery: function () {
|
||||
var query = this._selectModel && this._selectModel.get(this.options.typeLabel) || '';
|
||||
this.model.set('query', query);
|
||||
|
||||
var isInCollection = this.collection.findWhere({ val: query });
|
||||
if (query && !isInCollection) {
|
||||
this.collection.add({ label: query, val: query });
|
||||
}
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.model.set('visible', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.trigger('hidden', this);
|
||||
this.model.set('visible', false);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
_toggleVisibility: function () {
|
||||
this.$el.toggleClass('is-visible', !!this.model.get('visible'));
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return this.model.get('visible');
|
||||
},
|
||||
|
||||
_onMouseOver: function () {
|
||||
this._mouseOverAction && this._mouseOverAction();
|
||||
},
|
||||
|
||||
_onMouseOut: function () {
|
||||
this._mouseOutAction && this._mouseOutAction();
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this._listView && this._listView.clean();
|
||||
CoreView.prototype.remove.call(this);
|
||||
}
|
||||
});
|
||||
167
lib/assets/javascripts/builder/components/dataset/dataset-base-view.js
Executable file
167
lib/assets/javascripts/builder/components/dataset/dataset-base-view.js
Executable file
@@ -0,0 +1,167 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var SQLNotifications = require('builder/sql-notifications');
|
||||
var SQLUtils = require('builder/helpers/sql-utils');
|
||||
var Notifier = require('builder/components/notifier/notifier');
|
||||
var cdb = require('internal-carto.js');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'editorModel',
|
||||
'layerDefinitionModel',
|
||||
'querySchemaModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._clearSQLModel = new Backbone.Model({
|
||||
visible: false
|
||||
});
|
||||
|
||||
this._sqlModel = this._layerDefinitionModel.sqlModel;
|
||||
|
||||
this._SQL = new cdb.SQL({
|
||||
user: this._configModel.get('user_name'),
|
||||
sql_api_template: this._configModel.get('sql_api_template'),
|
||||
api_key: this._configModel.get('api_key')
|
||||
});
|
||||
|
||||
this._codemirrorModel = new Backbone.Model({
|
||||
content: this._querySchemaModel.get('query'),
|
||||
readonly: false
|
||||
});
|
||||
|
||||
this._applyButtonStatusModel = new Backbone.Model({
|
||||
loading: false
|
||||
});
|
||||
|
||||
SQLNotifications.track(this);
|
||||
},
|
||||
|
||||
_internalParseSQL: function (callbackAfterAlterSuccess) {
|
||||
var appliedQuery = this._querySchemaModel.get('query');
|
||||
var currentQuery = this._codemirrorModel.get('content');
|
||||
|
||||
// Remove last character if it has ';'
|
||||
if (currentQuery && currentQuery.slice(-1) === ';') {
|
||||
currentQuery = currentQuery.slice(0, currentQuery.length - 1);
|
||||
this._codemirrorModel.set('content', currentQuery);
|
||||
}
|
||||
|
||||
var isSameQuery = SQLUtils.isSameQuery(currentQuery, appliedQuery);
|
||||
var altersData = SQLUtils.altersData(currentQuery);
|
||||
|
||||
if (currentQuery === '' || isSameQuery) {
|
||||
SQLNotifications.removeNotification();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (altersData) {
|
||||
SQLNotifications.showNotification({
|
||||
status: 'loading',
|
||||
info: _t('notifications.sql.alter-loading'),
|
||||
closable: false
|
||||
});
|
||||
|
||||
this._applyButtonStatusModel.set('loading', true);
|
||||
|
||||
this._sqlModel.set('content', currentQuery);
|
||||
|
||||
this._SQL.execute(currentQuery, null, {
|
||||
success: function () {
|
||||
SQLNotifications.showNotification({
|
||||
status: 'success',
|
||||
info: _t('notifications.sql.alter-success'),
|
||||
closable: true,
|
||||
delay: Notifier.DEFAULT_DELAY
|
||||
});
|
||||
|
||||
this._applyButtonStatusModel.set('loading', false);
|
||||
|
||||
this._applyDefaultSQLAfterAlteringData();
|
||||
|
||||
if (typeof callbackAfterAlterSuccess === 'function') {
|
||||
callbackAfterAlterSuccess.call(this);
|
||||
}
|
||||
}.bind(this),
|
||||
error: function (errors) {
|
||||
var parsedErrors = this._parseErrors(errors.responseJSON && errors.responseJSON.error);
|
||||
|
||||
this._applyButtonStatusModel.set('loading', false);
|
||||
|
||||
this._codemirrorModel.set('errors', parsedErrors);
|
||||
|
||||
SQLNotifications.showErrorNotification(parsedErrors);
|
||||
|
||||
this._checkClearButton();
|
||||
}.bind(this)
|
||||
});
|
||||
} else {
|
||||
this._runQuery(currentQuery, this._saveSQL.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
_checkClearButton: function () {
|
||||
var customSql = this._codemirrorModel.get('content');
|
||||
var isDefaultQuery = SQLUtils.isSameQuery(customSql, this._defaultSQL());
|
||||
this._clearSQLModel.set({ visible: !isDefaultQuery });
|
||||
},
|
||||
|
||||
_applyDefaultSQLAfterAlteringData: function () {
|
||||
var originalQuery = this._defaultSQL();
|
||||
this._codemirrorModel.set({
|
||||
content: originalQuery,
|
||||
errors: []
|
||||
});
|
||||
this._sqlModel.set('content', originalQuery);
|
||||
this._querySchemaModel.set('query_errors', []);
|
||||
this._clearSQLModel.set({ visible: false });
|
||||
this._querySchemaModel.resetDueToAlteredData();
|
||||
this._clearSQL();
|
||||
},
|
||||
|
||||
_clearSQL: function () {
|
||||
var originalQuery = this._defaultSQL();
|
||||
this._codemirrorModel.set({
|
||||
content: originalQuery,
|
||||
errors: []
|
||||
});
|
||||
this._runQuery(originalQuery, this._saveSQL.bind(this));
|
||||
},
|
||||
|
||||
_showErrors: function (model) {
|
||||
var errors = this._querySchemaModel.get('query_errors');
|
||||
this._forceErrors(errors);
|
||||
this._checkClearButton();
|
||||
},
|
||||
|
||||
_forceErrors: function (errors, options) {
|
||||
var hasErrors = errors && errors.length > 0;
|
||||
var parsedErrors = hasErrors && this._parseErrors(errors);
|
||||
var editorErrors = options && options.showEditorError === false ? [] : parsedErrors;
|
||||
this._codemirrorModel.set('errors', editorErrors);
|
||||
this._editorModel.set('disabled', hasErrors);
|
||||
|
||||
if (hasErrors) {
|
||||
SQLNotifications.showErrorNotification(parsedErrors);
|
||||
}
|
||||
},
|
||||
|
||||
_parseErrors: function (errors) {
|
||||
if (!errors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
errors = _.isArray(errors) ? errors : [errors];
|
||||
return errors.map(function (error) {
|
||||
return {
|
||||
message: error
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var moment = require('moment');
|
||||
var template = require('./calendar-dropdown.tpl');
|
||||
|
||||
/**
|
||||
* Dropdown for a calendar selector.
|
||||
* Uses the DatePicker plugin internally to render the calendar and view behaviour.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Dropdown',
|
||||
|
||||
// defaults, used for
|
||||
options: {
|
||||
flat: true,
|
||||
date: '2008-07-01',
|
||||
current: '2008-07-31',
|
||||
calendars: 1,
|
||||
starts: 1
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
if (!this.model) throw new Error('model is required');
|
||||
this.elder('initialize');
|
||||
this._initDefaults();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
initialDateStr: this.model.get('date')
|
||||
})
|
||||
);
|
||||
|
||||
$('body').append(this.el);
|
||||
this._initCalendar(); // must be called after element is added to body!
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initDefaults: function () {
|
||||
var utc = new Date().getTimezoneOffset();
|
||||
var today = moment(new Date()).utcOffset(utc).format('YYYY-MM-DD');
|
||||
this.options.current = today;
|
||||
this.options.date = today;
|
||||
},
|
||||
|
||||
// should not be called until element is located in document
|
||||
_initCalendar: function () {
|
||||
var self = this;
|
||||
this._$calendar().DatePicker(
|
||||
_.extend(this.options, this.model.attributes, {
|
||||
onChange: function (formatted) {
|
||||
self.model.set('date', formatted);
|
||||
self.$('.js-date-str').text(formatted);
|
||||
self.hide();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_$calendar: function () {
|
||||
return this.$('.js-calendar');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._$calendar().DatePickerHide();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="DatePicker">
|
||||
<div class="js-calendar DatePicker-calendar DatePicker-calendar--simple"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<form>
|
||||
<div class="DatePicker-timersFrom">
|
||||
<div class="DatePicker-timersHour">
|
||||
<label class="DatePicker-timersLabel u-upperCase"><%- _t('components.datepicker.hour') %></label>
|
||||
<div data-editors="fromHour"></div>
|
||||
</div>
|
||||
<div class="DatePicker-timersMin">
|
||||
<label class="DatePicker-timersLabel u-upperCase"><%- _t('components.datepicker.min') %></label>
|
||||
<div data-editors="fromMin"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="DatePicker-timersTo">
|
||||
<div class="DatePicker-timersHour">
|
||||
<label class="DatePicker-timersLabel u-upperCase"><%- _t('components.datepicker.hour') %></label>
|
||||
<div data-editors="toHour"></div>
|
||||
</div>
|
||||
<div class="DatePicker-timersMin">
|
||||
<label class="DatePicker-timersLabel u-upperCase"><%- _t('components.datepicker.min') %></label>
|
||||
<div data-editors="toMin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,14 @@
|
||||
<form class="DatePicker-timers">
|
||||
<div class="DatePicker-timersField u-rSpace--xl">
|
||||
<div class="DatePicker-timersInput" data-fields="fromHour"></div>
|
||||
</div>
|
||||
<div class="DatePicker-timersField u-rSpace--xl">
|
||||
<div class="DatePicker-timersInput" data-fields="fromMin"></div>
|
||||
</div>
|
||||
<div class="DatePicker-timersField DatePicker-timersField--spaced u-rSpace--xl">
|
||||
<div class="DatePicker-timersInput" data-fields="toHour"></div>
|
||||
</div>
|
||||
<div class="DatePicker-timersField">
|
||||
<div class="DatePicker-timersInput" data-fields="toMin"></div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,262 @@
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var moment = require('moment');
|
||||
var $ = require('jquery');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
require('builder/components/form-components/index');
|
||||
require('datepicker');
|
||||
var datePickerTemplate = require('./date-picker-range-form.tpl');
|
||||
var template = require('./date-picker-range.tpl');
|
||||
|
||||
var MAX_RANGE = 30;
|
||||
var FOUR_HOURS = { amount: 4, unit: 'hours' };
|
||||
var ONE_DAY = { amount: 1, unit: 'day' };
|
||||
var ONE_WEEK = { amount: 1, unit: 'week' };
|
||||
|
||||
/**
|
||||
* Custom picer for a dates range.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'DatePicker',
|
||||
|
||||
options: {
|
||||
flat: true,
|
||||
date: ['2008-07-31', '2008-07-31'],
|
||||
current: '2008-07-31',
|
||||
calendars: 2,
|
||||
mode: 'range',
|
||||
starts: 1
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-dates': '_toggleCalendar',
|
||||
'click .js-fourHours': function () { this._setPreviousTime(FOUR_HOURS); },
|
||||
'click .js-oneDay': function () { this._setPreviousTime(ONE_DAY); },
|
||||
'click .js-oneWeek': function () { this._setPreviousTime(ONE_WEEK); }
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
var isDisabled = (opts && opts.disabled) ? opts.disabled : false;
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
fromDate: '',
|
||||
fromHour: 0,
|
||||
fromMin: 0,
|
||||
toDate: '',
|
||||
toHour: 23,
|
||||
toMin: 59,
|
||||
user_timezone: 0, // Explained as GMT+0
|
||||
disabled: isDisabled
|
||||
});
|
||||
|
||||
this.template = opts.template || template;
|
||||
|
||||
this._initBinds();
|
||||
this._setDefaultDate();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this.$el.append(
|
||||
this.template(
|
||||
_.extend(
|
||||
this.model.attributes,
|
||||
{
|
||||
max_days: MAX_RANGE,
|
||||
pad: Utils.pad
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
setTimeout(function () {
|
||||
self._initCalendar();
|
||||
self._hideCalendar();
|
||||
self._initTimers();
|
||||
}, 100);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change', this._setValues, this);
|
||||
this.model.bind('change', this._onValuesChange, this);
|
||||
$(document).bind('click', this._onDocumentClick.bind(this));
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
$(document).unbind('click', this._onDocumentClick.bind(this));
|
||||
},
|
||||
|
||||
_formattedDate: function (which) {
|
||||
var text = 'components.datepicker.' + which;
|
||||
return _t(text) + ' ' +
|
||||
'<strong>' +
|
||||
this.model.get(which + 'Date') + ' ' +
|
||||
(Utils.pad(this.model.get(which + 'Hour'), 2) + ':' + Utils.pad(this.model.get(which + 'Min'), 2)) +
|
||||
'</strong>';
|
||||
},
|
||||
|
||||
_setValues: function () {
|
||||
var text = _t('components.datepicker.dates-placeholder');
|
||||
var data = this.model.attributes;
|
||||
|
||||
if (data.fromDate && data.toDate) {
|
||||
var calendarIcon = '<i class="CDB-IconFont CDB-IconFont-calendar DatePicker-datesIcon"></i>';
|
||||
text = this._formattedDate('from') + ' ' + this._formattedDate('to') + calendarIcon;
|
||||
}
|
||||
|
||||
this.$('.DatePicker-dates').html(text);
|
||||
},
|
||||
|
||||
_setDefaultDate: function () {
|
||||
var datesUTC = this.model.get('user_timezone');
|
||||
var today = moment().utc(datesUTC);
|
||||
var previous = moment().utc(datesUTC).subtract((MAX_RANGE - 1), 'days');
|
||||
this.options.date = [previous.format('YYYY-MM-DD'), today.format('YYYY-MM-DD')];
|
||||
this.options.current = today.format('YYYY-MM-DD');
|
||||
this._setModelFromPrevious(previous);
|
||||
},
|
||||
|
||||
_initCalendar: function () {
|
||||
var selector = '.DatePicker-calendar';
|
||||
|
||||
// Can't initialize calendar if not already present in document... avoid errors being thrown
|
||||
if (!document.body.contains(this.$(selector)[0])) return;
|
||||
|
||||
this.calendar = this.$(selector).DatePicker(
|
||||
_.extend(this.options, {
|
||||
onChange: this._onDatesChange.bind(this),
|
||||
onRender: function (d) { // Disable future dates and dates < MAX_RANGE days ago
|
||||
var date = d.valueOf();
|
||||
var now = new Date();
|
||||
|
||||
var thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(now.getDate() - MAX_RANGE);
|
||||
|
||||
return (date < thirtyDaysAgo) || (date > now) ? { disabled: true } : '';
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_onDatesChange: function (formatted, dates) {
|
||||
// Check if selected dates have more than MAX_RANGE days
|
||||
var start = moment(formatted[0]);
|
||||
var end = moment(formatted[1]);
|
||||
|
||||
if (Math.abs(start.diff(end, 'days')) > MAX_RANGE) {
|
||||
formatted[1] = moment(formatted[0]).add('days', MAX_RANGE).format('YYYY-MM-DD');
|
||||
this.$('.DatePicker-calendar').DatePickerSetDate([formatted[0], formatted[1]]);
|
||||
}
|
||||
|
||||
this.model.set({
|
||||
fromDate: formatted[0],
|
||||
toDate: formatted[1]
|
||||
});
|
||||
},
|
||||
|
||||
_hideCalendar: function (e) {
|
||||
if (e) this.killEvent(e);
|
||||
this.$('.DatePicker-dropdown').hide();
|
||||
},
|
||||
|
||||
_toggleCalendar: function (ev) {
|
||||
if (ev) this.killEvent(ev);
|
||||
this.$('.DatePicker-dropdown').toggle();
|
||||
},
|
||||
|
||||
_setPreviousTime: function (timeSpan) {
|
||||
var previous = moment().utc(0).subtract(timeSpan.amount, timeSpan.unit);
|
||||
this._setModelFromPrevious(previous);
|
||||
this._setDatepickerFromPrevious(previous);
|
||||
this.closeCalendar();
|
||||
},
|
||||
|
||||
_setModelFromPrevious: function (previous) {
|
||||
var today = moment().utc(0);
|
||||
|
||||
this.model.set({
|
||||
fromDate: previous.format('YYYY-MM-DD'),
|
||||
fromHour: parseInt(previous.format('H'), 10),
|
||||
fromMin: parseInt(previous.format('m'), 10),
|
||||
toDate: today.format('YYYY-MM-DD'),
|
||||
toHour: parseInt(today.format('H'), 10),
|
||||
toMin: parseInt(today.format('m'), 10)
|
||||
});
|
||||
},
|
||||
|
||||
_setDatepickerFromPrevious: function (previous) {
|
||||
var today = moment().utc(0);
|
||||
this.$('.DatePicker-calendar').DatePickerSetDate([ previous.format('YYYY-MM-DD'), today.format('YYYY-MM-DD') ]);
|
||||
},
|
||||
|
||||
_initTimers: function () {
|
||||
var generateNumberType = function (min, max) {
|
||||
var title = max === 23
|
||||
? _t('components.datepicker.hour')
|
||||
: _t('components.datepicker.min');
|
||||
return {
|
||||
type: 'Number',
|
||||
title: title,
|
||||
validators: ['required', {
|
||||
type: 'interval',
|
||||
min: min,
|
||||
max: max
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
this.model.schema = {
|
||||
fromHour: generateNumberType(0, 23),
|
||||
fromMin: generateNumberType(0, 59),
|
||||
toHour: generateNumberType(0, 23),
|
||||
toMin: generateNumberType(0, 59)
|
||||
};
|
||||
|
||||
this._datesForm = new Backbone.Form({
|
||||
model: this.model,
|
||||
template: datePickerTemplate
|
||||
});
|
||||
|
||||
this._datesForm.bind('change', function () {
|
||||
this.commit();
|
||||
});
|
||||
this.$('.js-timers').append(this._datesForm.render().el);
|
||||
},
|
||||
|
||||
_onValuesChange: function () {
|
||||
this.trigger('changeDate', this.model.toJSON(), this);
|
||||
},
|
||||
|
||||
getDates: function () {
|
||||
return this.model.toJSON();
|
||||
},
|
||||
|
||||
closeCalendar: function () {
|
||||
this.$('.DatePicker-dropdown').hide();
|
||||
},
|
||||
|
||||
_onDocumentClick: function (e) {
|
||||
var $el = $(e.target);
|
||||
|
||||
if ($el.closest('.DatePicker').length === 0) {
|
||||
this.closeCalendar();
|
||||
}
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._datesForm && this._datesForm.remove();
|
||||
this._destroyBinds();
|
||||
this.closeCalendar();
|
||||
this.$('.DatePicker-calendar').DatePickerHide();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<button <% if (disabled) { %>disabled="disabled"<% } %> class="DatePicker-dates js-dates has-icon CDB-Text CDB-Size-medium <%- disabled ? 'is-disabled' : '' %>">
|
||||
<%- _t('components.datepicker.from') %> <strong><%- fromDate %> <%- pad(fromHour,2) %>:<%- pad(fromMin,2) %></strong> <%- _t('components.datepicker.to') %> <strong><%- toDate %> <%- pad(toHour,2) %>:<%- pad(toMin,2) %></strong>
|
||||
<i class="CDB-IconFont CDB-IconFont-calendar DatePicker-datesIcon"></i>
|
||||
</button>
|
||||
<div class="DatePicker-dropdown CDB-Text">
|
||||
<div class="DatePicker-calendar"></div>
|
||||
<div class="DatePicker-timers js-timers"></div>
|
||||
<div class="DatePicker-shortcuts">
|
||||
<p class="DatePicker-shortcutsText">
|
||||
<%- _t('components.datepicker.get-last') %> <button type="button" class="Button--link js-fourHours"><%- _t('components.datepicker.hours-pluralize', { smart_count: 4 }) %></button>,
|
||||
<button type="button" class="Button--link js-oneDay"><%- _t('components.datepicker.days-pluralize', { smart_count: 1 }) %></button> <%- _t('components.datepicker.or') %>
|
||||
<button type="button" class="Button--link js-oneWeek"><%- _t('components.datepicker.weeks-pluralize', { smart_count: 1 }) %></button>
|
||||
</p>
|
||||
<p class="DatePicker-shortcutsText"><%- _t('components.datepicker.gmt-convertion') %></p>
|
||||
</div>
|
||||
</div>
|
||||
135
lib/assets/javascripts/builder/components/date-picker/date-picker-view.js
Executable file
135
lib/assets/javascripts/builder/components/date-picker/date-picker-view.js
Executable file
@@ -0,0 +1,135 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var moment = require('moment');
|
||||
var $ = require('jquery');
|
||||
require('builder/components/form-components/index');
|
||||
require('datepicker');
|
||||
var template = require('./date-picker.tpl');
|
||||
|
||||
/**
|
||||
* Custom picer for a dates range.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'DatePicker',
|
||||
|
||||
options: {
|
||||
flat: true,
|
||||
date: '2008-07-31',
|
||||
current: '2008-07-31',
|
||||
calendars: 1,
|
||||
starts: 1
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-dates': '_toggleCalendar'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.template = opts.template || template;
|
||||
this._initBinds();
|
||||
this._setDefaultDate();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.append(this.template({
|
||||
readOnly: this.model.get('readOnly'),
|
||||
date: this.options.date
|
||||
}));
|
||||
|
||||
if (this.model.get('readOnly')) {
|
||||
this.undelegateEvents();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
$(document).bind('click', this._onDocumentClick.bind(this));
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
$(document).unbind('click', this._onDocumentClick.bind(this));
|
||||
},
|
||||
|
||||
_setDefaultDate: function () {
|
||||
var utc = new Date().getTimezoneOffset();
|
||||
var today = moment(new Date()).utcOffset(utc).format('YYYY-MM-DD');
|
||||
this.options.date = this.model.get('value') && moment(this.model.get('value')).format('YYYY-MM-DD') || today;
|
||||
this.options.current = this.options.date;
|
||||
},
|
||||
|
||||
_$calendar: function () {
|
||||
return this.$('.js-calendar');
|
||||
},
|
||||
|
||||
_$dropdown: function () {
|
||||
return this.$('.js-DatePicker-simpleDropdown');
|
||||
},
|
||||
|
||||
_hideCalendar: function (ev) {
|
||||
if (ev) {
|
||||
this.killEvent(ev);
|
||||
}
|
||||
this.model.set('visible', false);
|
||||
this._destroyCalendar();
|
||||
this._$dropdown().hide();
|
||||
},
|
||||
|
||||
_showCalendar: function () {
|
||||
this.model.set('visible', true);
|
||||
|
||||
this._$calendar().DatePicker(
|
||||
_.extend(this.options, this.model.attributes, {
|
||||
onChange: function (formatted, date) {
|
||||
this.model.set('value', formatted);
|
||||
this.$('.js-date-str').text(formatted);
|
||||
this._hideCalendar();
|
||||
}.bind(this),
|
||||
onRender: function (d) { // Disable future dates and dates
|
||||
var date = d.valueOf();
|
||||
var now = new Date();
|
||||
|
||||
return (date > now) ? { disabled: true } : '';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._$dropdown().show();
|
||||
},
|
||||
|
||||
_destroyCalendar: function () {
|
||||
if (this.$('.DatePicker').length > 0) {
|
||||
this._$calendar() && this._$calendar().DatePickerHide();
|
||||
}
|
||||
},
|
||||
|
||||
_toggleCalendar: function (ev) {
|
||||
if (ev) {
|
||||
this.killEvent(ev);
|
||||
}
|
||||
|
||||
this.model.get('visible') ? this._hideCalendar() : this._showCalendar();
|
||||
},
|
||||
|
||||
closeCalendar: function () {
|
||||
this._hideCalendar();
|
||||
},
|
||||
|
||||
_onDocumentClick: function (e) {
|
||||
var $el = $(e.target);
|
||||
|
||||
if ($el.closest('.DatePicker').length === 0) {
|
||||
this._hideCalendar();
|
||||
}
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._destroyBinds();
|
||||
this._hideCalendar();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
7
lib/assets/javascripts/builder/components/date-picker/date-picker.tpl
Executable file
7
lib/assets/javascripts/builder/components/date-picker/date-picker.tpl
Executable file
@@ -0,0 +1,7 @@
|
||||
<button class="js-dates DatePicker-dates DatePicker-dates--singleDate js-DatePicker-dates has-icon <% if (readOnly) { %> is-disabled <% } %>">
|
||||
<strong class="js-date-str"><%- date %></strong>
|
||||
<i class="CDB-IconFont CDB-IconFont-calendar DatePicker-datesIcon"></i>
|
||||
</button>
|
||||
<div class="DatePicker-simpleDropdown datepickerHidden js-DatePicker-simpleDropdown">
|
||||
<div class="js-calendar DatePicker-calendar DatePicker-calendar--simple"></div>
|
||||
</div>
|
||||
76
lib/assets/javascripts/builder/components/date-picker/edit-field-model.js
Executable file
76
lib/assets/javascripts/builder/components/date-picker/edit-field-model.js
Executable file
@@ -0,0 +1,76 @@
|
||||
var Backbone = require('backbone');
|
||||
var moment = require('moment');
|
||||
|
||||
/**
|
||||
* Default model for each field model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
attribute: '',
|
||||
value: '',
|
||||
type: 'string',
|
||||
readOnly: false
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
// Validation control variable
|
||||
this.validationError = '';
|
||||
this.bind('valid', function () {
|
||||
this.validationError = '';
|
||||
}, this);
|
||||
this.bind('error', function (m, error) {
|
||||
this.validationError = error;
|
||||
});
|
||||
},
|
||||
|
||||
_validate: function (attrs, options) {
|
||||
var valid = Backbone.Model.prototype._validate.apply(this, arguments);
|
||||
if (valid) {
|
||||
this.trigger('valid');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
validate: function (attrs) {
|
||||
if (!attrs) return;
|
||||
|
||||
var val = attrs.value;
|
||||
var type = attrs.type;
|
||||
|
||||
if (attrs.type === 'number') {
|
||||
var pattern = /^(\+|-)?(?:[0-9]+|[0-9]*\.[0-9]+)$/;
|
||||
if (val && !pattern.test(val)) {
|
||||
return 'Invalid number';
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (val !== null && val !== true && val !== false) {
|
||||
return 'Invalid boolean';
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
if (val && !moment(val).isValid()) {
|
||||
return 'Invalid date';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getError: function () {
|
||||
return this.validationError;
|
||||
},
|
||||
|
||||
isValid: function () {
|
||||
if (!this.validate) {
|
||||
return true;
|
||||
}
|
||||
return !this.validate(this.attributes) && this.validationError === '';
|
||||
}
|
||||
|
||||
});
|
||||
38
lib/assets/javascripts/builder/components/dialog/dialog-model.js
Executable file
38
lib/assets/javascripts/builder/components/dialog/dialog-model.js
Executable file
@@ -0,0 +1,38 @@
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
/**
|
||||
* View model of the dialog-view
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
visible: true,
|
||||
createContentView: function () {
|
||||
return new CoreView();
|
||||
}
|
||||
},
|
||||
|
||||
createContentView: function () {
|
||||
return this.get('createContentView')(this);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.set('visible', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.set('visible', false);
|
||||
},
|
||||
|
||||
isHidden: function () {
|
||||
return !this.get('visible');
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.Model.prototype.destroy}
|
||||
*/
|
||||
destroy: function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
this.trigger.apply(this, ['destroy'].concat(args));
|
||||
}
|
||||
});
|
||||
57
lib/assets/javascripts/builder/components/dialog/dialog-view.js
Executable file
57
lib/assets/javascripts/builder/components/dialog/dialog-view.js
Executable file
@@ -0,0 +1,57 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var DropdownOverlayView = require('builder/components/dropdown-overlay/dropdown-overlay-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Editor-boxModal Editor-FormDialog js-formDialog is-opening',
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this._renderContentView();
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderContentView: function () {
|
||||
var view = this.model.createContentView();
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().$el);
|
||||
|
||||
this.dropdownOverlay = new DropdownOverlayView({
|
||||
onClickAction: this.hide.bind(this),
|
||||
visible: true
|
||||
});
|
||||
this.addView(this.dropdownOverlay);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:show', this._onShowChange);
|
||||
this.listenTo(this.model, 'destroy', this._onDestroy);
|
||||
},
|
||||
|
||||
_onShowChange: function (m, show) {
|
||||
if (show) {
|
||||
this.$el.show();
|
||||
this.$el.removeClass('is-closing').addClass('is-opening');
|
||||
} else {
|
||||
this.$el.removeClass('is-opening').addClass('is-closing');
|
||||
this.$el.hide();
|
||||
}
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.model.show();
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.model.hide();
|
||||
},
|
||||
|
||||
_onDestroy: function () {
|
||||
this.hide();
|
||||
this.dropdownOverlay && this.dropdownOverlay.clean();
|
||||
this.clean();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
/*
|
||||
* Dropdown overlay to disable all interactions between elements
|
||||
* beneath the dropdown
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'CDB-Box-modalOverlay',
|
||||
|
||||
events: {
|
||||
'click': '_onOverlayClicked'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.container = opts && opts.container;
|
||||
this.onClickAction = opts && opts.onClickAction;
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
visible: _.isUndefined(opts && opts.visible) ? false : opts.visible
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
this.render();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:visible', this._onVisibilityChange);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
(this.container || $(document.body)).append(this.$el);
|
||||
this.$el.toggle(this.isVisible());
|
||||
return this;
|
||||
},
|
||||
|
||||
_onOverlayClicked: function () {
|
||||
this.onClickAction && this.onClickAction();
|
||||
this.hide();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.model.set('visible', true);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.model.set('visible', false);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return this.model.get('visible');
|
||||
},
|
||||
|
||||
_onVisibilityChange: function () {
|
||||
this.$el.toggle(this.isVisible());
|
||||
}
|
||||
});
|
||||
41
lib/assets/javascripts/builder/components/error/error-view.js
Executable file
41
lib/assets/javascripts/builder/components/error/error-view.js
Executable file
@@ -0,0 +1,41 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var cdb = require('internal-carto.js');
|
||||
var baseTemplate = require('./error.tpl');
|
||||
|
||||
/**
|
||||
* A typical error view.
|
||||
* @param {Object} options
|
||||
* @param {String} options.title If not provided will use a generic text as fallback
|
||||
* @param {String} options.desc If not provied will use a generic text as fallback
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'IntermediateInfo',
|
||||
|
||||
initialize: function (opts) {
|
||||
var attrs = _.defaults(
|
||||
_.pick(opts, ['title', 'desc']),
|
||||
{
|
||||
title: _t('components.error.default-title'),
|
||||
desc: _t('components.error.default-desc')
|
||||
}
|
||||
);
|
||||
this._template = this.options && this.options.template || baseTemplate;
|
||||
this.model = new Backbone.Model(attrs);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this._html());
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var m = this.model;
|
||||
return this._template({
|
||||
title: m.get('title'),
|
||||
desc: cdb.core.sanitize.html(m.get('desc'))
|
||||
});
|
||||
}
|
||||
});
|
||||
5
lib/assets/javascripts/builder/components/error/error.tpl
Executable file
5
lib/assets/javascripts/builder/components/error/error.tpl
Executable file
@@ -0,0 +1,5 @@
|
||||
<div class="LayoutIcon LayoutIcon--negative">
|
||||
<i class="CDB-IconFont CDB-IconFont-cockroach"></i>
|
||||
</div>
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-errorTextColor u-bSpace--m u-tSpace-xl"><%- title %></h3>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor"><%= desc %></p>
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
Mode: {
|
||||
NESTED: 'nested',
|
||||
FLOAT: 'float',
|
||||
DEFAULT: 'nested'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
module.exports = {
|
||||
Size: {
|
||||
MARKER_MIN: 7,
|
||||
IMAGE_MIN: 20,
|
||||
DEFAULT: {
|
||||
min: 1,
|
||||
max: 45,
|
||||
step: 0.5
|
||||
},
|
||||
DefaultInput100: {
|
||||
MIN: 0,
|
||||
MAX: 100,
|
||||
STEP: 1
|
||||
},
|
||||
DEFAULT_RANGE: [ 5, 20 ]
|
||||
},
|
||||
|
||||
Panes: {
|
||||
FIXED: 'fixed',
|
||||
BY_VALUE: 'value',
|
||||
FILE: 'file'
|
||||
},
|
||||
|
||||
Tabs: {
|
||||
BINS: 'bins',
|
||||
QUANTIFICATION: 'quantification'
|
||||
},
|
||||
|
||||
Quantification: {
|
||||
REFERENCE: {
|
||||
'Jenks': 'jenks',
|
||||
'Equal Interval': 'equal',
|
||||
'Heads/Tails': 'headtails',
|
||||
'Quantile': 'quantiles'
|
||||
}
|
||||
},
|
||||
|
||||
Settings: {
|
||||
COLOR: {
|
||||
quantifications: {
|
||||
items: ['jenks', 'equal', 'headtails', 'quantiles', 'category'],
|
||||
defaultIndex: 0
|
||||
}
|
||||
},
|
||||
|
||||
COLOR_RAMPS: {
|
||||
quantifications: {
|
||||
items: ['quantiles', 'jenks', 'equal', 'headtails', 'category'],
|
||||
defaultIndex: 0
|
||||
},
|
||||
bins: {
|
||||
items: ['2', '3', '4', '5', '6', '7'],
|
||||
defaultIndex: 3
|
||||
}
|
||||
},
|
||||
|
||||
NUMBER: {
|
||||
quantifications: {
|
||||
items: ['quantiles', 'jenks', 'equal', 'headtails'],
|
||||
defaultIndex: 0
|
||||
},
|
||||
bins: {
|
||||
items: ['2', '3', '4', '5', '6', '7'],
|
||||
defaultIndex: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
Type: {
|
||||
SIMPLE: 'simple',
|
||||
ANIMATION: 'animation',
|
||||
HEATMAP: 'heatmap',
|
||||
POLYGON: 'polygon',
|
||||
REGIONS: 'regions',
|
||||
HEXABINS: 'hexabins',
|
||||
SQUARES: 'squares',
|
||||
NONE: 'none'
|
||||
},
|
||||
Blending: {
|
||||
SIMPLE: [
|
||||
'none',
|
||||
'multiply',
|
||||
'screen',
|
||||
'overlay',
|
||||
'darken',
|
||||
'lighten',
|
||||
'color-dodge',
|
||||
'color-burn',
|
||||
'xor',
|
||||
'src-over'
|
||||
],
|
||||
ANIMATION: [
|
||||
'lighter',
|
||||
'multiply',
|
||||
'source-over',
|
||||
'xor'
|
||||
]
|
||||
}
|
||||
};
|
||||
62
lib/assets/javascripts/builder/components/form-components/editors/base.js
Executable file
62
lib/assets/javascripts/builder/components/form-components/editors/base.js
Executable file
@@ -0,0 +1,62 @@
|
||||
var Backbone = require('backbone');
|
||||
var $ = require('jquery');
|
||||
var ESC_KEY_CODE = 27;
|
||||
|
||||
Backbone.Form.editors.Base = Backbone.Form.editors.Base.extend({
|
||||
applyESCBind: function (callback) {
|
||||
this._ESCBindCallback = callback;
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
_onKeyDown: function (ev) {
|
||||
var anyModalOpen;
|
||||
if (ev.which === ESC_KEY_CODE) {
|
||||
anyModalOpen = this._anyModalOpen();
|
||||
!anyModalOpen && this._ESCBindCallback();
|
||||
}
|
||||
},
|
||||
|
||||
applyClickOutsideBind: function (callback) {
|
||||
this._clickBindCallback = callback;
|
||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this.$el.attr('data-cid', this.cid);
|
||||
document.addEventListener('click', this._onDocumentClick);
|
||||
},
|
||||
|
||||
_onDocumentClick: function (e) {
|
||||
var $el = $(e.target);
|
||||
var anyModalOpen = this._anyModalOpen();
|
||||
if ($el.closest('[data-cid="' + this.cid + '"]').length === 0 && !anyModalOpen) {
|
||||
this._clickBindCallback();
|
||||
}
|
||||
},
|
||||
|
||||
_anyModalOpen: function () {
|
||||
var modals = this.options.modals;
|
||||
if (!modals) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return modals.isOpen();
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
this._removeESCandClickListeners();
|
||||
},
|
||||
|
||||
_removeESCandClickListeners: function () {
|
||||
if (this._ESCBindCallback) {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
if (this._clickBindCallback) {
|
||||
document.removeEventListener('click', this._onDocumentClick);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this._destroyBinds && this._destroyBinds();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var FactoryHints = require('builder/editor/editor-hints/factory-hints');
|
||||
var CodeMirrorView = require('builder/components/code-mirror/code-mirror-view');
|
||||
|
||||
Backbone.Form.editors.CodeEditor = Backbone.Form.editors.TextArea.extend({
|
||||
render: function () {
|
||||
this.setValue(this.value);
|
||||
|
||||
this._codemirrorModel = new Backbone.Model({
|
||||
content: this.value,
|
||||
readonly: false,
|
||||
lineNumbers: false
|
||||
});
|
||||
|
||||
FactoryHints.init({
|
||||
tokens: this.options.tokens,
|
||||
tableName: false,
|
||||
columnsName: false
|
||||
});
|
||||
|
||||
this._initViews();
|
||||
|
||||
this._toggleDisableState();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
var val = this.$el.val();
|
||||
|
||||
val = this.codeMirrorView.getContent();
|
||||
|
||||
return (val === '') ? null : val;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._destroyEditor();
|
||||
|
||||
var hints = FactoryHints.reset().hints;
|
||||
this.codeMirrorView = new CodeMirrorView({
|
||||
model: this._codemirrorModel,
|
||||
hints: hints,
|
||||
mode: 'text/mustache',
|
||||
autocompleteChars: 2,
|
||||
autocompletePrefix: '{{',
|
||||
autocompleteSuffix: '}}',
|
||||
placeholder: this.options.placeholder
|
||||
});
|
||||
this.codeMirrorView.bind('codeChanged', function () {
|
||||
this.trigger('change', this.codeMirrorView.getContent());
|
||||
}, this);
|
||||
|
||||
var $codeMirrorEl = $(this.codeMirrorView.render().el);
|
||||
this.$el.replaceWith($codeMirrorEl);
|
||||
this.setElement($codeMirrorEl);
|
||||
this.$el.addClass('CodeMirror-formInput');
|
||||
// The default el is replace it with another dom node
|
||||
// we should add tracking class manually again
|
||||
this._addTrackingClass();
|
||||
},
|
||||
|
||||
_addTrackingClass: function () {
|
||||
if (this.options.trackingClass) {
|
||||
var trackClasses = this.options.trackingClass + ' track-' + this.options.key + this.options.editorType;
|
||||
this.$el.addClass(trackClasses);
|
||||
}
|
||||
},
|
||||
|
||||
_hasEditor: function () {
|
||||
return this.options.editor && !!this.codeMirrorView;
|
||||
},
|
||||
|
||||
_destroyEditor: function () {
|
||||
if (this._hasEditor()) {
|
||||
this.codeMirrorView.remove();
|
||||
}
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this._destroyEditor();
|
||||
Backbone.Form.editors.Base.prototype.remove.apply(this);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.$el.remove();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var StackLayoutView = require('builder/components/stack-layout/stack-layout-view');
|
||||
var MeasurementsView = require('./measurements-list-view');
|
||||
var FiltersView = require('./filters-list-view');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'filtersCollection',
|
||||
'measurementsCollection',
|
||||
'measurementModel',
|
||||
'region'
|
||||
];
|
||||
|
||||
var braces = function (value) {
|
||||
var template = _.template("'{<%- value %>}'");
|
||||
return template({
|
||||
value: value
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Box-modal CustomList',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
// For internal state
|
||||
this.model = new Backbone.Model({
|
||||
visible: false
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this._generateStackLayoutView();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:visible', function (mdl, isVisible) {
|
||||
isVisible ? this.render() : this.clearSubViews();
|
||||
this._toggleVisibility();
|
||||
});
|
||||
},
|
||||
|
||||
_generateStackLayoutView: function () {
|
||||
var createListView = function (stackLayoutModel, opts) {
|
||||
return this._createListView(stackLayoutModel, opts);
|
||||
}.bind(this);
|
||||
|
||||
var createFilterView = function (stackLayoutModel, opts) {
|
||||
return this._createFilterView(stackLayoutModel, opts);
|
||||
}.bind(this);
|
||||
|
||||
var stackViewCollection = new Backbone.Collection([{
|
||||
createStackView: createListView
|
||||
}, {
|
||||
createStackView: createFilterView
|
||||
}]);
|
||||
|
||||
this._stackLayoutView = new StackLayoutView({ collection: stackViewCollection });
|
||||
this.addView(this._stackLayoutView);
|
||||
this.$el.append(this._stackLayoutView.render().$el);
|
||||
},
|
||||
|
||||
_buildFilters: function () {
|
||||
var selectedFilters = this._filtersCollection.getSelected();
|
||||
return _.map(selectedFilters, function (filter) {
|
||||
return filter.getValue();
|
||||
}).join(', ');
|
||||
},
|
||||
|
||||
_fetchMeasurements: function () {
|
||||
var region = this._region;
|
||||
var fetchOptions = {
|
||||
region: region && braces(region)
|
||||
};
|
||||
|
||||
return this._measurementsCollection.fetch(fetchOptions);
|
||||
},
|
||||
|
||||
_fetchMeasurementsWithFilter: function () {
|
||||
var region = this._region;
|
||||
var filters = this._buildFilters();
|
||||
|
||||
var fetchOptions = {
|
||||
filters: filters && braces(filters),
|
||||
region: region && braces(region)
|
||||
};
|
||||
|
||||
return this._measurementsCollection.fetch(fetchOptions);
|
||||
},
|
||||
|
||||
_searchMeasurements: function (keyword) {
|
||||
var region = this._region;
|
||||
var filters = this._buildFilters();
|
||||
|
||||
var fetchOptions = {
|
||||
filters: filters && braces(filters),
|
||||
region: region && braces(region),
|
||||
search: keyword,
|
||||
abortable: true
|
||||
};
|
||||
|
||||
return this._measurementsCollection.fetch(fetchOptions);
|
||||
},
|
||||
|
||||
_fetchFilters: function () {
|
||||
var region = this._region;
|
||||
var fetchOptions = {
|
||||
region: region && braces(region)
|
||||
};
|
||||
|
||||
return this._filtersCollection.fetch(fetchOptions);
|
||||
},
|
||||
|
||||
_fetchCollections: function () {
|
||||
var selectedFilters = this._filtersCollection.getSelected();
|
||||
|
||||
if (selectedFilters.length > 0) {
|
||||
this._fetchMeasurementsWithFilter();
|
||||
} else {
|
||||
this._fetchMeasurements();
|
||||
}
|
||||
},
|
||||
|
||||
_createListView: function (stackLayoutModel, opts) {
|
||||
this._fetchCollections();
|
||||
|
||||
var view = new MeasurementsView({
|
||||
filtersCollection: this._filtersCollection,
|
||||
measurementsCollection: this._measurementsCollection,
|
||||
searchMeasurements: this._searchMeasurements.bind(this),
|
||||
measurementModel: this._measurementModel
|
||||
});
|
||||
|
||||
view.bind('filters', function () {
|
||||
stackLayoutModel.nextStep();
|
||||
}, this);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
_createFilterView: function (stackLayoutModel, opts) {
|
||||
// FIXME
|
||||
// we could save this request, it only depends on region
|
||||
if (this._filtersCollection.size() === 0) {
|
||||
this._fetchFilters();
|
||||
}
|
||||
|
||||
var view = new FiltersView({
|
||||
filtersCollection: this._filtersCollection
|
||||
});
|
||||
|
||||
view.bind('back', function () {
|
||||
this._measurementsCollection.trigger('maybeFiltersUpdated');
|
||||
stackLayoutModel.prevStep();
|
||||
}, this);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.model.set('visible', false);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this.model.set('visible', !this.model.get('visible'));
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return this.model.get('visible');
|
||||
},
|
||||
|
||||
_toggleVisibility: function () {
|
||||
this.$el.toggleClass('is-visible', !!this.isVisible());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<% if (typeof isLoading != 'undefined' && isLoading) { %>
|
||||
<div class="u-flex">
|
||||
<div class="CDB-LoaderIcon CDB-LoaderIcon--small is-dark">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="u-lSpace u-secondaryTextColor"><%- _t('components.backbone-forms.select.loading') %></span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<%- label %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,241 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var EditorHelpers = require('builder/components/form-components/helpers/editor');
|
||||
var template = require('./data-observatory-measurements.tpl');
|
||||
var selectedItemTemplate = require('./data-observatory-measurement-item.tpl');
|
||||
var DropdownDialogView = require('./data-observatory-dropdown-measurements-view');
|
||||
var PopupManager = require('builder/components/popup-manager');
|
||||
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
|
||||
var MeasurementsCollection = require('builder/data/data-observatory/measurements-collection');
|
||||
var FiltersCollection = require('builder/data/data-observatory/filters-collection');
|
||||
|
||||
var ENTER_KEY_CODE = 13;
|
||||
var STATE = {
|
||||
idle: 'idle',
|
||||
loading: 'loading',
|
||||
fetching: 'fetching',
|
||||
fetched: 'fetched',
|
||||
error: 'error'
|
||||
};
|
||||
var MEASUREMENT_ATTRIBUTES = ['aggregate', 'type', 'label', 'val', 'description', 'filter', 'license'];
|
||||
|
||||
Backbone.Form.editors.DataObservatoryDropdown = Backbone.Form.editors.Base.extend({
|
||||
|
||||
tagName: 'div',
|
||||
className: 'u-ellipsis Editor-formSelect',
|
||||
|
||||
events: {
|
||||
'click .js-button': '_onButtonClick',
|
||||
'keydown .js-button': '_onButtonKeyDown',
|
||||
'focus .js-button': function () {
|
||||
this.trigger('focus', this);
|
||||
},
|
||||
'blur': function () {
|
||||
this.trigger('blur', this);
|
||||
}
|
||||
},
|
||||
|
||||
options: {
|
||||
selectedItemTemplate: selectedItemTemplate
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
Backbone.Form.editors.Base.prototype.initialize.call(this, opts);
|
||||
EditorHelpers.setOptions(this, opts);
|
||||
|
||||
this.template = opts.template || template;
|
||||
this.dialogMode = this.options.dialogMode || DialogConstants.Mode.DEFAULT;
|
||||
|
||||
this.measurementModel = this.options.measurementModel;
|
||||
|
||||
var fetchOptions = {
|
||||
configModel: this.options.configModel,
|
||||
nodeDefModel: this.options.nodeDefModel
|
||||
};
|
||||
|
||||
this.measurementsCollection = new MeasurementsCollection([], fetchOptions);
|
||||
this.filtersCollection = new FiltersCollection([], fetchOptions);
|
||||
|
||||
this._initBinds();
|
||||
|
||||
this._dialogView = new DropdownDialogView({
|
||||
configModel: this.options.configModel,
|
||||
nodeDefModel: this.options.nodeDefModel,
|
||||
measurementsCollection: this.measurementsCollection,
|
||||
filtersCollection: this.filtersCollection,
|
||||
measurementModel: this.measurementModel,
|
||||
region: this.options.region
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var isLoading = this._isLoading();
|
||||
var isDisabled = this.options.disabled;
|
||||
var item = this.measurementModel;
|
||||
var placeholder = this._getPlaceholder();
|
||||
var isNull = this._hasValue();
|
||||
var label = isNull ? placeholder : item.getName();
|
||||
var title = item.getName() || '';
|
||||
|
||||
this.$el.html(
|
||||
this.template({
|
||||
title: title,
|
||||
label: label,
|
||||
keyAttr: this.options.keyAttr,
|
||||
isDisabled: isDisabled,
|
||||
isLoading: isLoading,
|
||||
isNull: isNull
|
||||
})
|
||||
);
|
||||
|
||||
this._popupManager = new PopupManager(this.cid, this.$el, this._dialogView.$el);
|
||||
this._popupManager.append(this.dialogMode);
|
||||
|
||||
if (item) {
|
||||
this._renderLicense(item);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
var hide = function () {
|
||||
this._dialogView.hide();
|
||||
this._popupManager && this._popupManager.untrack();
|
||||
}.bind(this);
|
||||
|
||||
this.applyESCBind(hide);
|
||||
this.applyClickOutsideBind(hide);
|
||||
|
||||
this.listenTo(this.measurementsCollection, 'change:selected', this._onItemSelected);
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
this.stopListening(this.measurementsCollection);
|
||||
Backbone.Form.editors.Base.prototype._destroyBinds.call(this);
|
||||
},
|
||||
|
||||
_getPlaceholder: function (isDisabled) {
|
||||
var keyAttr = this.options.keyAttr;
|
||||
var placeholder = this.options.placeholder || _t('components.backbone-forms.select.placeholder', { keyAttr: keyAttr });
|
||||
return placeholder;
|
||||
},
|
||||
|
||||
_hasValue: function () {
|
||||
var name = this.measurementModel.getValue();
|
||||
return name == null || name === '';
|
||||
},
|
||||
|
||||
_isLoading: function () {
|
||||
var state = this.measurementModel.getState();
|
||||
return state === STATE.fetching;
|
||||
},
|
||||
|
||||
_onItemSelected: function (mdl) {
|
||||
var selected = this.measurementsCollection.getSelectedItem();
|
||||
|
||||
if (selected) {
|
||||
this.measurementModel.clear({silent: true});
|
||||
this.measurementModel.set(_.pick(selected.attributes, MEASUREMENT_ATTRIBUTES));
|
||||
}
|
||||
|
||||
this._dialogView.hide();
|
||||
this._popupManager.untrack();
|
||||
this._renderLicense(selected);
|
||||
this._renderButton(selected).focus();
|
||||
|
||||
this.trigger('change', this);
|
||||
},
|
||||
|
||||
_onButtonClick: function () {
|
||||
this._dialogView.toggle();
|
||||
this._dialogView.isVisible() ? this._popupManager.track() : this._popupManager.untrack();
|
||||
},
|
||||
|
||||
_onButtonKeyDown: function (ev) {
|
||||
if (ev.which === ENTER_KEY_CODE) {
|
||||
ev.preventDefault();
|
||||
if (!this._dialogView.isVisible()) {
|
||||
ev.stopPropagation();
|
||||
this._onButtonClick();
|
||||
} else {
|
||||
this._popupManager.track();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
var value = this.getValue();
|
||||
var validators = this.schema.validators;
|
||||
var getValidator = this.getValidator;
|
||||
|
||||
if (!validators) return null;
|
||||
|
||||
// Run through validators until an error is found
|
||||
var error = null;
|
||||
_.every(validators, function (validator) {
|
||||
error = getValidator(validator)(value, {});
|
||||
|
||||
return !!error;
|
||||
});
|
||||
|
||||
// Return error to be aggregated by list
|
||||
return error ? error : null; // eslint-disable-line
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
this.$('.js-button').focus();
|
||||
},
|
||||
|
||||
blur: function () {
|
||||
this.$('.js-button').blur();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
var item = this.measurementModel;
|
||||
if (item) {
|
||||
return item.getValue();
|
||||
} else if (this.value) {
|
||||
return this.value;
|
||||
}
|
||||
},
|
||||
|
||||
setValue: function (value) {
|
||||
var selectedModel = this.measurementModel;
|
||||
if (selectedModel) {
|
||||
this._renderButton(selectedModel);
|
||||
}
|
||||
this.value = value;
|
||||
},
|
||||
|
||||
_renderButton: function (mdl) {
|
||||
var button = this.$('.js-button');
|
||||
var label = mdl.getName();
|
||||
var $html = this.options.selectedItemTemplate({
|
||||
label: label
|
||||
});
|
||||
|
||||
button
|
||||
.removeClass('is-empty')
|
||||
.attr('title', label)
|
||||
.html($html);
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
_renderLicense: function (mdl) {
|
||||
var license = mdl.get('license');
|
||||
var $license = this.$('.js-license');
|
||||
$license
|
||||
.removeClass('u-isHidden')
|
||||
.find('span')
|
||||
.text(license);
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this._popupManager && this._popupManager.destroy();
|
||||
this._dialogView && this._dialogView.clean();
|
||||
this._destroyBinds();
|
||||
Backbone.Form.editors.Base.prototype.remove.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="CDB-InputText CDB-Text is-cursor js-button u-ellipsis
|
||||
<% if (isDisabled) { %> is-disabled <% } %>
|
||||
<% if (!label) { %> is-empty <% } %>
|
||||
<% if (isNull) { %> is-empty <% } %>"
|
||||
tabindex="0"
|
||||
title="<%- title %>">
|
||||
<% if (isLoading) { %>
|
||||
<div class="CDB-LoaderIcon CDB-LoaderIcon--small is-dark u-iBlock">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="u-lSpace u-secondaryTextColor"><%- _t('components.backbone-forms.select.loading') %></span>
|
||||
<% } else { %>
|
||||
<%- label %>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="js-license CDB-Text CDB-FontSize-small u-altTextColor u-tSpace u-bSpace u-isHidden u-flex">
|
||||
<a href="https://cartodb.github.io/bigmetadata/licenses.html" target="_blank">
|
||||
<span class="u-tSpace u-bSpace DataObservatory-license"></span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
var _ = require('underscore');
|
||||
var CustomListMultiItemView = require('builder/components/custom-list/custom-list-multi-item-view');
|
||||
|
||||
var NAME = _.template('<%-name %> (<%- items %>)');
|
||||
|
||||
module.exports = CustomListMultiItemView.extend({
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
var name = this.model.getName() == null ? 'null' : this.model.getName();
|
||||
name = name.replace(/"/g, '');
|
||||
|
||||
this.$el.append(
|
||||
this.options.template(
|
||||
_.extend({
|
||||
isSelected: this.model.get('selected'),
|
||||
isDisabled: this.model.get('disabled'),
|
||||
name: NAME({
|
||||
name: name,
|
||||
items: this.model.get('items')
|
||||
}),
|
||||
val: this.model.getValue(),
|
||||
description: this.model.get('description')
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.$el
|
||||
.attr('data-val', this.model.getValue())
|
||||
.toggleClass('is-disabled', !!this.model.get('disabled'));
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<button type="button" class="CDB-ListDecoration-itemLink u-actionTextColor
|
||||
<% if (isSelected) { %> is-selected <% } %>
|
||||
" title="<%= name %>">
|
||||
<div class="u-flex">
|
||||
<div class="u-iBlock u-rSpace--m">
|
||||
<input class="CDB-Checkbox js-input" type="checkbox" name="" value="" <% if (isSelected) { %>checked<% } %> <% if (isDisabled) { %>disabled<% } %> />
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="u-bSpace"><%= name %></div>
|
||||
<div class="CDB-Size-small u-altTextColor"><%- description %></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="CDB-Box-modalHeader">
|
||||
<ul class="CDB-Box-modalHeaderItem CDB-Box-modalHeaderItem--block CDB-Box-modalHeaderItem--paddingHorizontal">
|
||||
<li class="CDB-ListDecoration-item CDB-ListDecoration-itemPadding--vertical CDB-Text CDB-Size-medium u-secondaryTextColor">
|
||||
<button class="u-actionTextColor js-back u-rSpace">
|
||||
<i class="CDB-IconFont CDB-IconFont-arrowPrev Size-large"></i>
|
||||
</button>
|
||||
<%- headerTitle%>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="js-content"></div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user