diff --git a/web/static/js/graph.js b/web/static/js/graph.js index d226b00c1..5b410ce40 100644 --- a/web/static/js/graph.js +++ b/web/static/js/graph.js @@ -71,6 +71,14 @@ Prometheus.Graph.prototype.initialize = function() { self.stacked = self.queryForm.find("input[name=stacked]"); self.insertMetric = self.queryForm.find("select[name=insert_metric]"); + // Return moves focus back to expr instead of submitting. + self.insertMetric.bind('keydown', 'return', function(e) { + self.expr.focus(); + self.expr.val(self.expr.val()); + + return e.preventDefault(); + }) + self.graph = graphWrapper.find(".graph"); self.legend = graphWrapper.find(".legend"); self.spinner = graphWrapper.find(".spinner"); @@ -96,8 +104,11 @@ Prometheus.Graph.prototype.initialize = function() { self.queryForm.find("input[name=dec_end]").click(function() { self.decreaseEnd(); }); self.insertMetric.change(function() { - self.expr.val(self.expr.val() + self.insertMetric.val()); + self.expr.selection('replace', {text: self.insertMetric.val(), mode: 'before'}) + self.insertMetric.focus(); // refocusing }); + self.insertMetric.click(function() { self.expr.focus() }) + self.expr.focus(); // TODO: move to external Graph method. self.populateInsertableMetrics(); diff --git a/web/static/vendor/js/jquery.hotkeys.js b/web/static/vendor/js/jquery.hotkeys.js new file mode 100644 index 000000000..5905f9db2 --- /dev/null +++ b/web/static/vendor/js/jquery.hotkeys.js @@ -0,0 +1,100 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "), + textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"]; + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1 ) ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); \ No newline at end of file diff --git a/web/static/vendor/js/jquery.selection.js b/web/static/vendor/js/jquery.selection.js new file mode 100644 index 000000000..86b100c60 --- /dev/null +++ b/web/static/vendor/js/jquery.selection.js @@ -0,0 +1,360 @@ +/*! + * jQuery.selection - jQuery Plugin + * + * Copyright (c) 2010-2012 IWASAKI Koji (@madapaja). + * http://blog.madapaja.net/ + * Under The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +(function($, win, doc) { + /** + * 要素の文字列選択状態を取得します + * + * @param {Element} element 対象要素 + * @return {Object} return + * @return {String} return.text 選択されている文字列 + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + var _getCaretInfo = function(element){ + var res = { + text: '', + start: 0, + end: 0 + }; + + if (!element.value) { + /* 値がない、もしくは空文字列 */ + return res; + } + + try { + if (win.getSelection) { + /* IE 以外 */ + res.start = element.selectionStart; + res.end = element.selectionEnd; + res.text = element.value.slice(res.start, res.end); + } else if (doc.selection) { + /* for IE */ + element.focus(); + + var range = doc.selection.createRange(), + range2 = doc.body.createTextRange(), + tmpLength; + + res.text = range.text; + + try { + range2.moveToElementText(element); + range2.setEndPoint('StartToStart', range); + } catch (e) { + range2 = element.createTextRange(); + range2.setEndPoint('StartToStart', range); + } + + res.start = element.value.length - range2.text.length; + res.end = res.start + range.text.length; + } + } catch (e) { + /* あきらめる */ + } + + return res; + }; + + /** + * 要素に対するキャレット操作 + * @type {Object} + */ + var _CaretOperation = { + /** + * 要素のキャレット位置を取得します + * + * @param {Element} element 対象要素 + * @return {Object} return + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + getPos: function(element) { + var tmp = _getCaretInfo(element); + return {start: tmp.start, end: tmp.end}; + }, + + /** + * 要素のキャレット位置を設定します + * + * @param {Element} element 対象要素 + * @param {Object} toRange 設定するキャレット位置 + * @param {Integer} toRange.start 選択開始位置 + * @param {Integer} toRange.end 選択終了位置 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + setPos: function(element, toRange, caret) { + caret = this._caretMode(caret); + + if (caret == 'start') { + toRange.end = toRange.start; + } else if (caret == 'end') { + toRange.start = toRange.end; + } + + element.focus(); + try { + if (element.createTextRange) { + var range = element.createTextRange(); + + if (win.navigator.userAgent.toLowerCase().indexOf("msie") >= 0) { + toRange.start = element.value.substr(0, toRange.start).replace(/\r/g, '').length; + toRange.end = element.value.substr(0, toRange.end).replace(/\r/g, '').length; + } + + range.collapse(true); + range.moveStart('character', toRange.start); + range.moveEnd('character', toRange.end - toRange.start); + + range.select(); + } else if (element.setSelectionRange) { + element.setSelectionRange(toRange.start, toRange.end); + } + } catch (e) { + /* あきらめる */ + } + }, + + /** + * 要素内の選択文字列を取得します + * + * @param {Element} element 対象要素 + * @return {String} return 選択文字列 + */ + getText: function(element) { + return _getCaretInfo(element).text; + }, + + /** + * キャレットモードを選択します + * + * @param {String} caret キャレットモード + * @return {String} return "keep" | "start" | "end" のいずれか + */ + _caretMode: function(caret) { + caret = caret || "keep"; + if (caret == false) { + caret = 'end'; + } + + switch (caret) { + case 'keep': + case 'start': + case 'end': + break; + + default: + caret = 'keep'; + } + + return caret; + }, + + /** + * 選択文字列を置き換えます + * + * @param {Element} element 対象要素 + * @param {String} text 置き換える文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + replace: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.start + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * 文字列を選択文字列の前に挿入します + * + * @param {Element} element 対象要素 + * @param {String} text 挿入文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + insertBefore: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start + text.length, end: tmp.end + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.start); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * 文字列を選択文字列の後に挿入します + * + * @param {Element} element 対象要素 + * @param {String} text 挿入文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + insertAfter: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.end}; + + element.value = orig.substr(0, tmp.end) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + } + }; + + /* jQuery.selection を追加 */ + $.extend({ + /** + * ウィンドウの選択されている文字列を取得 + * + * @param {String} mode 選択モード "text" | "html" のいずれか + * @return {String} return + */ + selection: function(mode) { + var getText = ((mode || 'text').toLowerCase() == 'text'); + + try { + if (win.getSelection) { + if (getText) { + // get text + return win.getSelection().toString(); + } else { + // get html + var sel = win.getSelection(), range; + + if (sel.getRangeAt) { + range = sel.getRangeAt(0); + } else { + range = doc.createRange(); + range.setStart(sel.anchorNode, sel.anchorOffset); + range.setEnd(sel.focusNode, sel.focusOffset); + } + + return $('
').append(range.cloneContents()).html(); + } + } else if (doc.selection) { + if (getText) { + // get text + return doc.selection.createRange().text; + } else { + // get html + return doc.selection.createRange().htmlText; + } + } + } catch (e) { + /* あきらめる */ + } + + return ''; + } + }); + + /* selection を追加 */ + $.fn.extend({ + selection: function(mode, opts) { + opts = opts || {}; + + switch (mode) { + /** + * selection('getPos') + * キャレット位置を取得します + * + * @return {Object} return + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + case 'getPos': + return _CaretOperation.getPos(this[0]); + break; + + /** + * selection('setPos', opts) + * キャレット位置を設定します + * + * @param {Integer} opts.start 選択開始位置 + * @param {Integer} opts.end 選択終了位置 + */ + case 'setPos': + return this.each(function() { + _CaretOperation.setPos(this, opts); + }); + break; + + /** + * selection('replace', opts) + * 選択文字列を置き換えます + * + * @param {String} opts.text 置き換える文字列 + * @param {String} opts.caret キャレットモード "keep" | "start" | "end" のいずれか + */ + case 'replace': + return this.each(function() { + _CaretOperation.replace(this, opts.text, opts.caret); + }); + break; + + /** + * selection('insert', opts) + * 選択文字列の前、もしくは後に文字列を挿入えます + * + * @param {String} opts.text 挿入文字列 + * @param {String} opts.caret キャレットモード "keep" | "start" | "end" のいずれか + * @param {String} opts.mode 挿入モード "before" | "after" のいずれか + */ + case 'insert': + return this.each(function() { + if (opts.mode == 'before') { + _CaretOperation.insertBefore(this, opts.text, opts.caret); + } else { + _CaretOperation.insertAfter(this, opts.text, opts.caret); + } + }); + + break; + + /** + * selection('get') + * 選択されている文字列を取得 + * + * @return {String} return + */ + case 'get': + default: + return _CaretOperation.getText(this[0]); + break; + } + + return this; + } + }); +})(jQuery, window, window.document);