Bug 32613: Add autocomplete to SQL reports editor
[koha.git] / koha-tmpl / intranet-tmpl / lib / codemirror / show-hint.js
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 // Distributed under an MIT license: https://codemirror.net/LICENSE
3
4 (function(mod) {
5   if (typeof exports == "object" && typeof module == "object") // CommonJS
6     mod(require("../../lib/codemirror"));
7   else if (typeof define == "function" && define.amd) // AMD
8     define(["../../lib/codemirror"], mod);
9   else // Plain browser env
10     mod(CodeMirror);
11 })(function(CodeMirror) {
12   "use strict";
13
14   var HINT_ELEMENT_CLASS        = "CodeMirror-hint";
15   var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
16
17   // This is the old interface, kept around for now to stay
18   // backwards-compatible.
19   CodeMirror.showHint = function(cm, getHints, options) {
20     if (!getHints) return cm.showHint(options);
21     if (options && options.async) getHints.async = true;
22     var newOpts = {hint: getHints};
23     if (options) for (var prop in options) newOpts[prop] = options[prop];
24     return cm.showHint(newOpts);
25   };
26
27   CodeMirror.defineExtension("showHint", function(options) {
28     options = parseOptions(this, this.getCursor("start"), options);
29     var selections = this.listSelections()
30     if (selections.length > 1) return;
31     // By default, don't allow completion when something is selected.
32     // A hint function can have a `supportsSelection` property to
33     // indicate that it can handle selections.
34     if (this.somethingSelected()) {
35       if (!options.hint.supportsSelection) return;
36       // Don't try with cross-line selections
37       for (var i = 0; i < selections.length; i++)
38         if (selections[i].head.line != selections[i].anchor.line) return;
39     }
40
41     if (this.state.completionActive) this.state.completionActive.close();
42     var completion = this.state.completionActive = new Completion(this, options);
43     if (!completion.options.hint) return;
44
45     CodeMirror.signal(this, "startCompletion", this);
46     completion.update(true);
47   });
48
49   CodeMirror.defineExtension("closeHint", function() {
50     if (this.state.completionActive) this.state.completionActive.close()
51   })
52
53   function Completion(cm, options) {
54     this.cm = cm;
55     this.options = options;
56     this.widget = null;
57     this.debounce = 0;
58     this.tick = 0;
59     this.startPos = this.cm.getCursor("start");
60     this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
61
62     var self = this;
63     cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
64   }
65
66   var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
67     return setTimeout(fn, 1000/60);
68   };
69   var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
70
71   Completion.prototype = {
72     close: function() {
73       if (!this.active()) return;
74       this.cm.state.completionActive = null;
75       this.tick = null;
76       this.cm.off("cursorActivity", this.activityFunc);
77
78       if (this.widget && this.data) CodeMirror.signal(this.data, "close");
79       if (this.widget) this.widget.close();
80       CodeMirror.signal(this.cm, "endCompletion", this.cm);
81     },
82
83     active: function() {
84       return this.cm.state.completionActive == this;
85     },
86
87     pick: function(data, i) {
88       var completion = data.list[i], self = this;
89       this.cm.operation(function() {
90         if (completion.hint)
91           completion.hint(self.cm, data, completion);
92         else
93           self.cm.replaceRange(getText(completion), completion.from || data.from,
94                                completion.to || data.to, "complete");
95         CodeMirror.signal(data, "pick", completion);
96         self.cm.scrollIntoView();
97       })
98       this.close();
99     },
100
101     cursorActivity: function() {
102       if (this.debounce) {
103         cancelAnimationFrame(this.debounce);
104         this.debounce = 0;
105       }
106
107       var identStart = this.startPos;
108       if(this.data) {
109         identStart = this.data.from;
110       }
111
112       var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
113       if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
114           pos.ch < identStart.ch || this.cm.somethingSelected() ||
115           (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
116         this.close();
117       } else {
118         var self = this;
119         this.debounce = requestAnimationFrame(function() {self.update();});
120         if (this.widget) this.widget.disable();
121       }
122     },
123
124     update: function(first) {
125       if (this.tick == null) return
126       var self = this, myTick = ++this.tick
127       fetchHints(this.options.hint, this.cm, this.options, function(data) {
128         if (self.tick == myTick) self.finishUpdate(data, first)
129       })
130     },
131
132     finishUpdate: function(data, first) {
133       if (this.data) CodeMirror.signal(this.data, "update");
134
135       var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
136       if (this.widget) this.widget.close();
137
138       this.data = data;
139
140       if (data && data.list.length) {
141         if (picked && data.list.length == 1) {
142           this.pick(data, 0);
143         } else {
144           this.widget = new Widget(this, data);
145           CodeMirror.signal(data, "shown");
146         }
147       }
148     }
149   };
150
151   function parseOptions(cm, pos, options) {
152     var editor = cm.options.hintOptions;
153     var out = {};
154     for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
155     if (editor) for (var prop in editor)
156       if (editor[prop] !== undefined) out[prop] = editor[prop];
157     if (options) for (var prop in options)
158       if (options[prop] !== undefined) out[prop] = options[prop];
159     if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
160     return out;
161   }
162
163   function getText(completion) {
164     if (typeof completion == "string") return completion;
165     else return completion.text;
166   }
167
168   function buildKeyMap(completion, handle) {
169     var baseMap = {
170       Up: function() {handle.moveFocus(-1);},
171       Down: function() {handle.moveFocus(1);},
172       PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
173       PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
174       Home: function() {handle.setFocus(0);},
175       End: function() {handle.setFocus(handle.length - 1);},
176       Enter: handle.pick,
177       Tab: handle.pick,
178       Esc: handle.close
179     };
180
181     var mac = /Mac/.test(navigator.platform);
182
183     if (mac) {
184       baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);};
185       baseMap["Ctrl-N"] = function() {handle.moveFocus(1);};
186     }
187
188     var custom = completion.options.customKeys;
189     var ourMap = custom ? {} : baseMap;
190     function addBinding(key, val) {
191       var bound;
192       if (typeof val != "string")
193         bound = function(cm) { return val(cm, handle); };
194       // This mechanism is deprecated
195       else if (baseMap.hasOwnProperty(val))
196         bound = baseMap[val];
197       else
198         bound = val;
199       ourMap[key] = bound;
200     }
201     if (custom)
202       for (var key in custom) if (custom.hasOwnProperty(key))
203         addBinding(key, custom[key]);
204     var extra = completion.options.extraKeys;
205     if (extra)
206       for (var key in extra) if (extra.hasOwnProperty(key))
207         addBinding(key, extra[key]);
208     return ourMap;
209   }
210
211   function getHintElement(hintsElement, el) {
212     while (el && el != hintsElement) {
213       if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
214       el = el.parentNode;
215     }
216   }
217
218   function Widget(completion, data) {
219     this.completion = completion;
220     this.data = data;
221     this.picked = false;
222     var widget = this, cm = completion.cm;
223     var ownerDocument = cm.getInputField().ownerDocument;
224     var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
225
226     var hints = this.hints = ownerDocument.createElement("ul");
227     var theme = completion.cm.options.theme;
228     hints.className = "CodeMirror-hints " + theme;
229     this.selectedHint = data.selectedHint || 0;
230
231     var completions = data.list;
232     for (var i = 0; i < completions.length; ++i) {
233       var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
234       var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
235       if (cur.className != null) className = cur.className + " " + className;
236       elt.className = className;
237       if (cur.render) cur.render(elt, data, cur);
238       else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
239       elt.hintId = i;
240     }
241
242     var container = completion.options.container || ownerDocument.body;
243     var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
244     var left = pos.left, top = pos.bottom, below = true;
245     var offsetLeft = 0, offsetTop = 0;
246     if (container !== ownerDocument.body) {
247       // We offset the cursor position because left and top are relative to the offsetParent's top left corner.
248       var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
249       var offsetParent = isContainerPositioned ? container : container.offsetParent;
250       var offsetParentPosition = offsetParent.getBoundingClientRect();
251       var bodyPosition = ownerDocument.body.getBoundingClientRect();
252       offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
253       offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
254     }
255     hints.style.left = (left - offsetLeft) + "px";
256     hints.style.top = (top - offsetTop) + "px";
257
258     // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
259     var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
260     var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
261     container.appendChild(hints);
262     var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
263     var scrolls = hints.scrollHeight > hints.clientHeight + 1
264     var startScroll = cm.getScrollInfo();
265
266     if (overlapY > 0) {
267       var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
268       if (curTop - height > 0) { // Fits above cursor
269         hints.style.top = (top = pos.top - height - offsetTop) + "px";
270         below = false;
271       } else if (height > winH) {
272         hints.style.height = (winH - 5) + "px";
273         hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
274         var cursor = cm.getCursor();
275         if (data.from.ch != cursor.ch) {
276           pos = cm.cursorCoords(cursor);
277           hints.style.left = (left = pos.left - offsetLeft) + "px";
278           box = hints.getBoundingClientRect();
279         }
280       }
281     }
282     var overlapX = box.right - winW;
283     if (overlapX > 0) {
284       if (box.right - box.left > winW) {
285         hints.style.width = (winW - 5) + "px";
286         overlapX -= (box.right - box.left) - winW;
287       }
288       hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px";
289     }
290     if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
291       node.style.paddingRight = cm.display.nativeBarWidth + "px"
292
293     cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
294       moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
295       setFocus: function(n) { widget.changeActive(n); },
296       menuSize: function() { return widget.screenAmount(); },
297       length: completions.length,
298       close: function() { completion.close(); },
299       pick: function() { widget.pick(); },
300       data: data
301     }));
302
303     if (completion.options.closeOnUnfocus) {
304       var closingOnBlur;
305       cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
306       cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
307     }
308
309     cm.on("scroll", this.onScroll = function() {
310       var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
311       var newTop = top + startScroll.top - curScroll.top;
312       var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
313       if (!below) point += hints.offsetHeight;
314       if (point <= editor.top || point >= editor.bottom) return completion.close();
315       hints.style.top = newTop + "px";
316       hints.style.left = (left + startScroll.left - curScroll.left) + "px";
317     });
318
319     CodeMirror.on(hints, "dblclick", function(e) {
320       var t = getHintElement(hints, e.target || e.srcElement);
321       if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
322     });
323
324     CodeMirror.on(hints, "click", function(e) {
325       var t = getHintElement(hints, e.target || e.srcElement);
326       if (t && t.hintId != null) {
327         widget.changeActive(t.hintId);
328         if (completion.options.completeOnSingleClick) widget.pick();
329       }
330     });
331
332     CodeMirror.on(hints, "mousedown", function() {
333       setTimeout(function(){cm.focus();}, 20);
334     });
335     this.scrollToActive()
336
337     CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
338     return true;
339   }
340
341   Widget.prototype = {
342     close: function() {
343       if (this.completion.widget != this) return;
344       this.completion.widget = null;
345       this.hints.parentNode.removeChild(this.hints);
346       this.completion.cm.removeKeyMap(this.keyMap);
347
348       var cm = this.completion.cm;
349       if (this.completion.options.closeOnUnfocus) {
350         cm.off("blur", this.onBlur);
351         cm.off("focus", this.onFocus);
352       }
353       cm.off("scroll", this.onScroll);
354     },
355
356     disable: function() {
357       this.completion.cm.removeKeyMap(this.keyMap);
358       var widget = this;
359       this.keyMap = {Enter: function() { widget.picked = true; }};
360       this.completion.cm.addKeyMap(this.keyMap);
361     },
362
363     pick: function() {
364       this.completion.pick(this.data, this.selectedHint);
365     },
366
367     changeActive: function(i, avoidWrap) {
368       if (i >= this.data.list.length)
369         i = avoidWrap ? this.data.list.length - 1 : 0;
370       else if (i < 0)
371         i = avoidWrap ? 0  : this.data.list.length - 1;
372       if (this.selectedHint == i) return;
373       var node = this.hints.childNodes[this.selectedHint];
374       if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
375       node = this.hints.childNodes[this.selectedHint = i];
376       node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
377       this.scrollToActive()
378       CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
379     },
380
381     scrollToActive: function() {
382       var margin = this.completion.options.scrollMargin || 0;
383       var node1 = this.hints.childNodes[Math.max(0, this.selectedHint - margin)];
384       var node2 = this.hints.childNodes[Math.min(this.data.list.length - 1, this.selectedHint + margin)];
385       var firstNode = this.hints.firstChild;
386       if (node1.offsetTop < this.hints.scrollTop)
387         this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop;
388       else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
389         this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop;
390     },
391
392     screenAmount: function() {
393       return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
394     }
395   };
396
397   function applicableHelpers(cm, helpers) {
398     if (!cm.somethingSelected()) return helpers
399     var result = []
400     for (var i = 0; i < helpers.length; i++)
401       if (helpers[i].supportsSelection) result.push(helpers[i])
402     return result
403   }
404
405   function fetchHints(hint, cm, options, callback) {
406     if (hint.async) {
407       hint(cm, callback, options)
408     } else {
409       var result = hint(cm, options)
410       if (result && result.then) result.then(callback)
411       else callback(result)
412     }
413   }
414
415   function resolveAutoHints(cm, pos) {
416     var helpers = cm.getHelpers(pos, "hint"), words
417     if (helpers.length) {
418       var resolved = function(cm, callback, options) {
419         var app = applicableHelpers(cm, helpers);
420         function run(i) {
421           if (i == app.length) return callback(null)
422           fetchHints(app[i], cm, options, function(result) {
423             if (result && result.list.length > 0) callback(result)
424             else run(i + 1)
425           })
426         }
427         run(0)
428       }
429       resolved.async = true
430       resolved.supportsSelection = true
431       return resolved
432     } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
433       return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
434     } else if (CodeMirror.hint.anyword) {
435       return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
436     } else {
437       return function() {}
438     }
439   }
440
441   CodeMirror.registerHelper("hint", "auto", {
442     resolve: resolveAutoHints
443   });
444
445   CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
446     var cur = cm.getCursor(), token = cm.getTokenAt(cur)
447     var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
448     if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
449       term = token.string.substr(0, cur.ch - token.start)
450     } else {
451       term = ""
452       from = cur
453     }
454     var found = [];
455     for (var i = 0; i < options.words.length; i++) {
456       var word = options.words[i];
457       if (word.slice(0, term.length) == term)
458         found.push(word);
459     }
460
461     if (found.length) return {list: found, from: from, to: to};
462   });
463
464   CodeMirror.commands.autocomplete = CodeMirror.showHint;
465
466   var defaultOptions = {
467     hint: CodeMirror.hint.auto,
468     completeSingle: true,
469     alignWithWord: true,
470     closeCharacters: /[\s()\[\]{};:>,]/,
471     closeOnUnfocus: true,
472     completeOnSingleClick: true,
473     container: null,
474     customKeys: null,
475     extraKeys: null
476   };
477
478   CodeMirror.defineOption("hintOptions", null);
479 });