tooltip.js
332 lines
| 11.7 KiB
| application/javascript
|
JavascriptLexer
|
r16580 | // Copyright (c) IPython Development Team. | ||
// Distributed under the terms of the Modified BSD License. | ||||
|
r17198 | define([ | ||
'base/js/namespace', | ||||
|
r17200 | 'jquery', | ||
|
r17198 | 'base/js/utils', | ||
], function(IPython, $, utils) { | ||||
|
r7173 | "use strict"; | ||
|
r7145 | |||
|
r7162 | // tooltip constructor | ||
|
r17198 | var Tooltip = function (events) { | ||
var that = this; | ||||
this.events = events; | ||||
this.time_before_tooltip = 1200; | ||||
|
r11715 | |||
|
r17198 | // handle to html | ||
this.tooltip = $('#tooltip'); | ||||
this._hidden = true; | ||||
|
r7189 | |||
|
r17198 | // variable for consecutive call | ||
this._old_cell = null; | ||||
this._old_request = null; | ||||
this._consecutive_counter = 0; | ||||
|
r7189 | |||
|
r17198 | // 'sticky ?' | ||
this._sticky = false; | ||||
|
r7189 | |||
|
r17198 | // display tooltip if the docstring is empty? | ||
this._hide_if_no_docstring = false; | ||||
// contain the button in the upper right corner | ||||
this.buttons = $('<div/>').addClass('tooltipbuttons'); | ||||
// will contain the docstring | ||||
this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip'); | ||||
// build the buttons menu on the upper right | ||||
// expand the tooltip to see more | ||||
var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner | ||||
.attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () { | ||||
that.expand(); | ||||
}).append( | ||||
$('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus')); | ||||
// open in pager | ||||
var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press shift-tab 4 times)'); | ||||
var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n'); | ||||
morelink.append(morespan); | ||||
morelink.click(function () { | ||||
that.showInPager(that._old_cell); | ||||
}); | ||||
|
r7147 | |||
|
r17198 | // close the tooltip | ||
var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button'); | ||||
var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close'); | ||||
closelink.append(closespan); | ||||
closelink.click(function () { | ||||
that.remove_and_cancel_tooltip(true); | ||||
}); | ||||
|
r7162 | |||
|
r17198 | this._clocklink = $('<a/>').attr('href', "#"); | ||
this._clocklink.attr('role', "button"); | ||||
this._clocklink.addClass('ui-button'); | ||||
this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds'); | ||||
var clockspan = $('<span/>').text('Close'); | ||||
clockspan.addClass('ui-icon'); | ||||
clockspan.addClass('ui-icon-clock'); | ||||
this._clocklink.append(clockspan); | ||||
this._clocklink.click(function () { | ||||
that.cancel_stick(); | ||||
}); | ||||
|
r7173 | |||
|
r17198 | //construct the tooltip | ||
// add in the reverse order you want them to appear | ||||
this.buttons.append(closelink); | ||||
this.buttons.append(expandlink); | ||||
this.buttons.append(morelink); | ||||
this.buttons.append(this._clocklink); | ||||
this._clocklink.hide(); | ||||
// we need a phony element to make the small arrow | ||||
// of the tooltip in css | ||||
// we will move the arrow later | ||||
this.arrow = $('<div/>').addClass('pretooltiparrow'); | ||||
this.tooltip.append(this.buttons); | ||||
this.tooltip.append(this.arrow); | ||||
this.tooltip.append(this.text); | ||||
// function that will be called if you press tab 1, 2, 3... times in a row | ||||
this.tabs_functions = [function (cell, text, cursor) { | ||||
that._request_tooltip(cell, text, cursor); | ||||
}, function () { | ||||
that.expand(); | ||||
}, function () { | ||||
that.stick(); | ||||
}, function (cell) { | ||||
that.cancel_stick(); | ||||
that.showInPager(cell); | ||||
}]; | ||||
// call after all the tabs function above have bee call to clean their effects | ||||
// if necessary | ||||
this.reset_tabs_function = function (cell, text) { | ||||
this._old_cell = (cell) ? cell : null; | ||||
this._old_request = (text) ? text : null; | ||||
this._consecutive_counter = 0; | ||||
|
r7189 | }; | ||
|
r17198 | }; | ||
|
r7189 | |||
|
r15493 | Tooltip.prototype.is_visible = function () { | ||
return !this._hidden; | ||||
}; | ||||
|
r7191 | Tooltip.prototype.showInPager = function (cell) { | ||
|
r7172 | // reexecute last call in pager by appending ? to show back in pager | ||
var that = this; | ||||
|
r18278 | this.events.trigger('open_with_text.Pager', that._reply.content); | ||
|
r7172 | this.remove_and_cancel_tooltip(); | ||
|
r15493 | }; | ||
|
r7160 | |||
|
r7162 | // grow the tooltip verticaly | ||
|
r7191 | Tooltip.prototype.expand = function () { | ||
|
r7160 | this.text.removeClass('smalltooltip'); | ||
this.text.addClass('bigtooltip'); | ||||
|
r7173 | $('#expanbutton').hide('slow'); | ||
|
r15493 | }; | ||
|
r7160 | |||
|
r7151 | // deal with all the logic of hiding the tooltip | ||
// and reset it's status | ||||
|
r7202 | Tooltip.prototype._hide = function () { | ||
|
r15493 | this._hidden = true; | ||
|
r7202 | this.tooltip.fadeOut('fast'); | ||
|
r7189 | $('#expanbutton').show('slow'); | ||
this.text.removeClass('bigtooltip'); | ||||
this.text.addClass('smalltooltip'); | ||||
// keep scroll top to be sure to always see the first line | ||||
this.text.scrollTop(0); | ||||
|
r7204 | this.code_mirror = null; | ||
|
r15493 | }; | ||
|
r7145 | |||
|
r12459 | // return true on successfully removing a visible tooltip; otherwise return | ||
// false. | ||||
|
r7191 | Tooltip.prototype.remove_and_cancel_tooltip = function (force) { | ||
|
r7145 | // note that we don't handle closing directly inside the calltip | ||
// as in the completer, because it is not focusable, so won't | ||||
// get the event. | ||||
|
r12955 | this.cancel_pending(); | ||
|
r12533 | if (!this._hidden) { | ||
if (force || !this._sticky) { | ||||
|
r12459 | this.cancel_stick(); | ||
this._hide(); | ||||
} | ||||
this.reset_tabs_function(); | ||||
return true; | ||||
} else { | ||||
return false; | ||||
|
r7160 | } | ||
|
r15493 | }; | ||
|
r7160 | |||
|
r7162 | // cancel autocall done after '(' for example. | ||
|
r7191 | Tooltip.prototype.cancel_pending = function () { | ||
|
r15493 | if (this._tooltip_timeout !== null) { | ||
|
r7173 | clearTimeout(this._tooltip_timeout); | ||
this._tooltip_timeout = null; | ||||
|
r7145 | } | ||
|
r15493 | }; | ||
|
r7162 | |||
// will trigger tooltip after timeout | ||||
|
r11715 | Tooltip.prototype.pending = function (cell, hide_if_no_docstring) { | ||
|
r7157 | var that = this; | ||
|
r7191 | this._tooltip_timeout = setTimeout(function () { | ||
|
r15493 | that.request(cell, hide_if_no_docstring); | ||
|
r7189 | }, that.time_before_tooltip); | ||
|
r15493 | }; | ||
|
r7173 | |||
|
r12819 | // easy access for julia monkey patching. | ||
|
r14716 | Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi; | ||
|
r12819 | |||
Tooltip.prototype.extract_oir_token = function(line){ | ||||
|
r7189 | // use internally just to make the request to the kernel | ||
// Feel free to shorten this logic if you are better | ||||
// than me in regEx | ||||
// basicaly you shoul be able to get xxx.xxx.xxx from | ||||
// something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2, | ||||
// remove everything between matchin bracket (need to iterate) | ||||
var matchBracket = /\([^\(\)]+\)/g; | ||||
var endBracket = /\([^\(]*$/g; | ||||
|
r12819 | var oldline = line; | ||
|
r7189 | |||
|
r12819 | line = line.replace(matchBracket, ""); | ||
while (oldline != line) { | ||||
oldline = line; | ||||
line = line.replace(matchBracket, ""); | ||||
|
r7189 | } | ||
// remove everything after last open bracket | ||||
|
r12819 | line = line.replace(endBracket, ""); | ||
|
r13883 | // reset the regex object | ||
Tooltip.last_token_re.lastIndex = 0; | ||||
|
r15493 | return Tooltip.last_token_re.exec(line); | ||
|
r13208 | }; | ||
|
r7189 | |||
|
r16580 | Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) { | ||
|
r13327 | var callbacks = $.proxy(this._show, this); | ||
|
r16587 | var msg_id = cell.kernel.inspect(text, cursor_pos, callbacks); | ||
|
r13207 | }; | ||
|
r7162 | |||
// make an imediate completion request | ||||
|
r11715 | Tooltip.prototype.request = function (cell, hide_if_no_docstring) { | ||
|
r7173 | // request(codecell) | ||
|
r7172 | // Deal with extracting the text from the cell and counting | ||
// call in a row | ||||
|
r7160 | this.cancel_pending(); | ||
var editor = cell.code_mirror; | ||||
var cursor = editor.getCursor(); | ||||
|
r16588 | var cursor_pos = utils.to_absolute_cursor_pos(editor, cursor); | ||
|
r16580 | var text = cell.get_text(); | ||
|
r7173 | |||
|
r11715 | this._hide_if_no_docstring = hide_if_no_docstring; | ||
|
r8949 | if(editor.somethingSelected()){ | ||
text = editor.getSelection(); | ||||
} | ||||
|
r7204 | // need a permanent handel to code_mirror for future auto recall | ||
|
r7172 | this.code_mirror = editor; | ||
|
r7173 | // now we treat the different number of keypress | ||
|
r7172 | // first if same cell, same text, increment counter by 1 | ||
|
r15493 | if (this._old_cell == cell && this._old_request == text && this._hidden === false) { | ||
|
r7173 | this._consecutive_counter++; | ||
|
r7160 | } else { | ||
|
r7172 | // else reset | ||
|
r7173 | this.cancel_stick(); | ||
|
r7191 | this.reset_tabs_function (cell, text); | ||
|
r7160 | } | ||
|
r7173 | |||
|
r16580 | this.tabs_functions[this._consecutive_counter](cell, text, cursor_pos); | ||
|
r7173 | |||
// then if we are at the end of list function, reset | ||||
|
r13793 | if (this._consecutive_counter == this.tabs_functions.length) { | ||
|
r16580 | this.reset_tabs_function (cell, text, cursor); | ||
} | ||||
|
r7173 | |||
|
r7172 | return; | ||
|
r15493 | }; | ||
|
r7162 | |||
// cancel the option of having the tooltip to stick | ||||
|
r7191 | Tooltip.prototype.cancel_stick = function () { | ||
|
r7189 | clearTimeout(this._stick_timeout); | ||
this._stick_timeout = null; | ||||
this._clocklink.hide('slow'); | ||||
this._sticky = false; | ||||
|
r15493 | }; | ||
|
r7160 | |||
|
r7162 | // put the tooltip in a sicky state for 10 seconds | ||
// it won't be removed by remove_and_cancell() unless you called with | ||||
// the first parameter set to true. | ||||
// remove_and_cancell_tooltip(true) | ||||
|
r7191 | Tooltip.prototype.stick = function (time) { | ||
|
r15493 | time = (time !== undefined) ? time : 10; | ||
|
r7162 | var that = this; | ||
|
r7160 | this._sticky = true; | ||
|
r7173 | this._clocklink.show('slow'); | ||
|
r7191 | this._stick_timeout = setTimeout(function () { | ||
|
r7160 | that._sticky = false; | ||
|
r7173 | that._clocklink.hide('slow'); | ||
|
r7189 | }, time * 1000); | ||
|
r15493 | }; | ||
|
r7156 | |||
|
r7162 | // should be called with the kernel reply to actually show the tooltip | ||
|
r7202 | Tooltip.prototype._show = function (reply) { | ||
|
r7153 | // move the bubble if it is not hidden | ||
// otherwise fade it | ||||
|
r16580 | this._reply = reply; | ||
|
r13207 | var content = reply.content; | ||
|
r14787 | if (!content.found) { | ||
// object not found, nothing to show | ||||
return; | ||||
} | ||||
|
r13207 | this.name = content.name; | ||
|
r7160 | |||
// do some math to have the tooltip arrow on more or less on left or right | ||||
// width of the editor | ||||
|
r7173 | var w = $(this.code_mirror.getScrollerElement()).width(); | ||
|
r7160 | // ofset of the editor | ||
|
r7173 | var o = $(this.code_mirror.getScrollerElement()).offset(); | ||
|
r8949 | |||
// whatever anchor/head order but arrow at mid x selection | ||||
var anchor = this.code_mirror.cursorCoords(false); | ||||
var head = this.code_mirror.cursorCoords(true); | ||||
|
r10425 | var xinit = (head.left+anchor.left)/2; | ||
|
r7189 | var xinter = o.left + (xinit - o.left) / w * (w - 450); | ||
|
r7155 | var posarrowleft = xinit - xinter; | ||
|
r7162 | |||
|
r15493 | if (this._hidden === false) { | ||
|
r7189 | this.tooltip.animate({ | ||
'left': xinter - 30 + 'px', | ||||
|
r10425 | 'top': (head.bottom + 10) + 'px' | ||
|
r7189 | }); | ||
} else { | ||||
this.tooltip.css({ | ||||
'left': xinter - 30 + 'px' | ||||
}); | ||||
this.tooltip.css({ | ||||
|
r10425 | 'top': (head.bottom + 10) + 'px' | ||
|
r7189 | }); | ||
|
r7153 | } | ||
|
r7189 | this.arrow.animate({ | ||
'left': posarrowleft + 'px' | ||||
}); | ||||
|
r16580 | |||
|
r11715 | this._hidden = false; | ||
|
r15493 | this.tooltip.fadeIn('fast'); | ||
|
r7147 | this.text.children().remove(); | ||
|
r16580 | |||
// This should support rich data types, but only text/plain for now | ||||
|
r15376 | // Any HTML within the docstring is escaped by the fixConsole() method. | ||
|
r16580 | var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain'])); | ||
|
r7151 | this.text.append(pre); | ||
|
r7158 | // keep scroll top to be sure to always see the first line | ||
this.text.scrollTop(0); | ||||
|
r15493 | }; | ||
|
r7156 | |||
|
r17198 | // Backwards compatability. | ||
|
r7145 | IPython.Tooltip = Tooltip; | ||
|
r7173 | |||
|
r17201 | return {'Tooltip': Tooltip}; | ||
|
r17198 | }); | ||