Initial commit
This commit is contained in:
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 */
|
||||
Reference in New Issue
Block a user