Commit 5bf38806 by Anton Stupak

Merge pull request #4716 from edx/anton/studio-tooltip-manager

Studio: Add tooltips.
parents 6d263fe4 37c3a50f
......@@ -81,6 +81,7 @@
"draggabilly": "js/vendor/draggabilly.pkgd",
"URI": "js/vendor/URI.min",
"ieshim": "js/src/ie_shim",
"tooltip_manager": "js/src/tooltip_manager",
// externally hosted files
"tender": [
......@@ -213,6 +214,9 @@
"URI": {
exports: "URI"
},
"tooltip_manager": {
deps: ["jquery", "underscore"]
},
"xblock/core": {
exports: "XBlock",
deps: ["jquery", "jquery.immediateDescendents"]
......@@ -234,7 +238,7 @@
deps: ["jquery", "gettext"],
callback: function() {
// load other scripts on every page, after jquery loads
require(["js/base", "coffee/src/main", "coffee/src/logger", "datepair", "accessibility", "ieshim"]);
require(["js/base", "coffee/src/main", "coffee/src/logger", "datepair", "accessibility", "ieshim", "tooltip_manager"]);
// we need "datepair" because it dynamically modifies the page
// when it is loaded -- yuck!
}
......@@ -249,18 +253,18 @@
% if context_course:
<script type="text/javascript">
require(['js/models/course'], function(Course) {
window.course = new Course({
id: "${context_course.id}",
name: "${context_course.display_name_with_default | h}",
url_name: "${context_course.location.name | h}",
org: "${context_course.location.org | h}",
num: "${context_course.location.course | h}",
revision: "${context_course.location.revision | h}"
});
});
</script>
<script type="text/javascript">
require(['js/models/course'], function(Course) {
window.course = new Course({
id: "${context_course.id}",
name: "${context_course.display_name_with_default | h}",
url_name: "${context_course.location.name | h}",
org: "${context_course.location.org | h}",
num: "${context_course.location.course | h}",
revision: "${context_course.location.revision | h}"
});
});
</script>
% endif
<!-- view -->
......
class @TooltipManager
constructor: () ->
@$body = $('body')
@$tooltip = $('<div class="tooltip"></div>')
@$body.delegate '[data-tooltip]',
'mouseover': @showTooltip,
'mousemove': @moveTooltip,
'mouseout': @hideTooltip,
'click': @hideTooltip
showTooltip: (e) =>
$target = $(e.target).closest('[data-tooltip]')
tooltipText = $target.attr('data-tooltip')
@$tooltip.html(tooltipText)
@$body.append(@$tooltip)
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x,
'top': tooltipCoords.y
@tooltipTimer = setTimeout ()=>
@$tooltip.show().css('opacity', 1)
@tooltipTimer = setTimeout ()=>
@hideTooltip()
, 3000
, 500
moveTooltip: (e) =>
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x
'top': tooltipCoords.y
hideTooltip: (e) =>
@$tooltip.hide().css('opacity', 0)
clearTimeout(@tooltipTimer)
# Move initialization at the bottom to make sure that TooltipManager is already
# assigned to the Global object.
$ ->
new TooltipManager
describe('TooltipManager', function () {
'use strict';
var PAGE_X = 100, PAGE_Y = 100, WIDTH = 100, HEIGHT = 100, DELTA = 10,
showTooltip;
beforeEach(function () {
setFixtures(sandbox({
'id': 'test-id',
'data-tooltip': 'some text here.'
}));
this.element = $('#test-id');
this.tooltip = new TooltipManager(document.body);
jasmine.Clock.useMock();
// Set default dimensions to make testing easer.
$('.tooltip').height(HEIGHT).width(WIDTH);
// Re-write default jasmine-jquery to consider opacity.
this.addMatchers({
toBeVisible: function() {
return this.actual.is(':visible') || parseFloat(this.actual.css('opacity'));
},
toBeHidden: function() {
return this.actual.is(':hidden') || !parseFloat(this.actual.css('opacity'));
},
});
});
afterEach(function () {
this.tooltip.destroy();
});
showTooltip = function (element) {
element.trigger($.Event("mouseover", {
pageX: PAGE_X,
pageY: PAGE_Y
}));
jasmine.Clock.tick(500);
};
it('can destroy itself', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
this.tooltip.destroy();
expect($('.tooltip')).not.toExist();
showTooltip(this.element);
expect($('.tooltip')).not.toExist();
});
it('should be shown when mouse is over the element', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
expect($('.tooltip').text()).toBe('some text here.');
});
it('should be hidden when mouse is out of the element', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
this.element.trigger($.Event("mouseout"));
jasmine.Clock.tick(50);
expect($('.tooltip')).toBeHidden();
});
it('should be hidden when user clicks on the element', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
this.element.trigger($.Event("click"));
jasmine.Clock.tick(50);
expect($('.tooltip')).toBeHidden();
});
it('should moves correctly', function () {
showTooltip(this.element);
expect($('.tooltip')).toBeVisible();
// PAGE_X - 0.5 * WIDTH
// 100 - 0.5 * 100 = 50
expect(parseInt($('.tooltip').css('left'))).toBe(50);
// PAGE_Y - (HEIGHT + 15)
// 100 - (100 + 15) = -15
expect(parseInt($('.tooltip').css('top'))).toBe(-15);
this.element.trigger($.Event("mousemove", {
pageX: PAGE_X + DELTA,
pageY: PAGE_Y + DELTA
}));
// PAGE_X + DELTA - 0.5 * WIDTH
// 100 + 10 - 0.5 * 100 = 60
expect(parseInt($('.tooltip').css('left'))).toBe(60);
// PAGE_Y + DELTA - (HEIGHT + 15)
// 100 + 10 - (100 + 15) = -5
expect(parseInt($('.tooltip').css('top'))).toBe(-5);
});
});
(function() {
'use strict';
var TooltipManager = function (element) {
this.element = $(element);
// If tooltip container already exist, use it.
this.tooltip = $('div.' + this.className.split(/\s+/).join('.'));
// Otherwise, create new one.
if (!this.tooltip.length) {
this.tooltip = $('<div />', {
'class': this.className
}).appendTo(this.element);
}
this.hide();
_.bindAll(this);
this.bindEvents();
};
TooltipManager.prototype = {
// Space separated list of class names for the tooltip container.
className: 'tooltip',
SELECTOR: '[data-tooltip]',
bindEvents: function () {
this.element.on({
'mouseover.TooltipManager': this.showTooltip,
'mousemove.TooltipManager': this.moveTooltip,
'mouseout.TooltipManager': this.hideTooltip,
'click.TooltipManager': this.hideTooltip
}, this.SELECTOR);
},
getCoords: function (pageX, pageY) {
return {
'left': pageX - 0.5 * this.tooltip.outerWidth(),
'top': pageY - (this.tooltip.outerHeight() + 15)
};
},
show: function () {
this.tooltip.show().css('opacity', 1);
},
hide: function () {
this.tooltip.hide().css('opacity', 0);
},
showTooltip: function(event) {
var tooltipText = $(event.currentTarget).attr('data-tooltip');
this.tooltip
.html(tooltipText)
.css(this.getCoords(event.pageX, event.pageY));
if (this.tooltipTimer) {
clearTimeout(this.tooltipTimer);
}
this.tooltipTimer = setTimeout(this.show, 500);
},
moveTooltip: function(event) {
this.tooltip.css(this.getCoords(event.pageX, event.pageY));
},
hideTooltip: function() {
clearTimeout(this.tooltipTimer);
// Wait for a 50ms before hiding the tooltip to avoid blinking when
// the item contains nested elements.
this.tooltipTimer = setTimeout(this.hide, 50);
},
destroy: function () {
this.tooltip.remove();
// Unbind all delegated event handlers in the ".TooltipManager"
// namespace.
this.element.off('.TooltipManager', this.SELECTOR);
}
};
window.TooltipManager = TooltipManager;
$(document).ready(function () {
new TooltipManager(document.body);
});
}());
......@@ -12,5 +12,6 @@
<script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/src/tooltip_manager.js')}"></script>
<link href="${static.url('css/vendor/jquery.autocomplete.css')}" rel="stylesheet" type="text/css">
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment