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); } });