Merge pull request #4716 from edx/anton/studio-tooltip-manager
Studio: Add tooltips.
This commit is contained in:
@@ -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 -->
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
93
common/static/js/spec/tooltip_manager_spec.js
Normal file
93
common/static/js/spec/tooltip_manager_spec.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
83
common/static/js/src/tooltip_manager.js
Normal file
83
common/static/js/src/tooltip_manager.js
Normal file
@@ -0,0 +1,83 @@
|
||||
(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">
|
||||
|
||||
Reference in New Issue
Block a user