cartodb/lib/assets/javascripts/builder/components/code-mirror/code-mirror-view.js

366 lines
10 KiB
JavaScript
Raw Normal View History

2020-06-15 10:58:47 +08:00
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);
}
});