Files
edx-platform/xmodule/js/src/annotatable/display.js
2022-06-20 18:20:06 +05:00

444 lines
14 KiB
JavaScript

// Once generated by CoffeeScript 1.9.3, but now lives as pure JS
/* eslint-disable */
// TODO: Examine all of the xss-lint exceptions (https://openedx.atlassian.net/browse/PLAT-2084)
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Annotatable = (function() {
Annotatable.prototype._debug = false;
/*
selectors for the annotatable xmodule
*/
Annotatable.prototype.wrapperSelector = '.annotatable-wrapper';
Annotatable.prototype.toggleAnnotationsSelector = '.annotatable-toggle-annotations';
Annotatable.prototype.toggleInstructionsSelector = '.annotatable-toggle-instructions';
Annotatable.prototype.instructionsSelector = '.annotatable-instructions';
Annotatable.prototype.sectionSelector = '.annotatable-section';
Annotatable.prototype.spanSelector = '.annotatable-span';
Annotatable.prototype.replySelector = '.annotatable-reply';
/*
these selectors are for responding to events from the annotation capa problem type
*/
Annotatable.prototype.problemXModuleSelector = '.xmodule_CapaModule';
Annotatable.prototype.problemSelector = 'div.problem';
Annotatable.prototype.problemInputSelector = 'div.problem .annotation-input';
Annotatable.prototype.problemReturnSelector = 'div.problem .annotation-return';
function Annotatable(el) {
this.onMoveTip = bind(this.onMoveTip, this);
this.onShowTip = bind(this.onShowTip, this);
this.onClickReturn = bind(this.onClickReturn, this);
this.onClickReply = bind(this.onClickReply, this);
this.onClickToggleInstructions = bind(this.onClickToggleInstructions, this);
this.onClickToggleAnnotations = bind(this.onClickToggleAnnotations, this);
if (this._debug) {
console.log('loaded Annotatable');
}
this.el = el;
this.$el = $(el);
this.init();
}
Annotatable.prototype.$ = function(selector) {
return $(selector, this.el);
};
Annotatable.prototype.init = function() {
this.initEvents();
return this.initTips();
};
Annotatable.prototype.initEvents = function() {
/*
Initialize toggle handlers for the instructions and annotations sections
*/
var ref;
ref = [false, false], this.annotationsHidden = ref[0], this.instructionsHidden = ref[1];
this.$(this.toggleAnnotationsSelector).bind('click', this.onClickToggleAnnotations);
this.$(this.toggleInstructionsSelector).bind('click', this.onClickToggleInstructions);
/*
Initialize handler for 'reply to annotation' events that scroll to
the associated problem. The reply buttons are part of the tooltip
content. It's important that the tooltips be configured to render
as descendants of the annotation module and *not* the document.body.
*/
this.$el.on('click', this.replySelector, this.onClickReply);
/*
Initialize handler for 'return to annotation' events triggered from problems.
1) There are annotationinput capa problems rendered on the page
2) Each one has an embedded return link (see annotation capa problem template).
Since the capa problem injects HTML content via AJAX, the best we can do is
is let the click events bubble up to the body and handle them there.
*/
return $(document).on('click', this.problemReturnSelector, this.onClickReturn);
};
Annotatable.prototype.initTips = function() {
/*
tooltips are used to display annotations for highlighted text spans
*/
return this.$(this.spanSelector).each((function(_this) {
return function(index, el) {
return $(el).qtip(_this.getSpanTipOptions(el));
};
})(this));
};
Annotatable.prototype.getSpanTipOptions = function(el) {
return {
content: {
title: {
text: this.makeTipTitle(el)
},
text: this.makeTipContent(el)
},
position: {
/*
of tooltip
*/
my: 'bottom center',
/*
of target
*/
at: 'top center',
/*
where the tooltip was triggered (i.e. the annotation span)
*/
target: $(el),
container: this.$(this.wrapperSelector),
adjust: {
y: -5
}
},
show: {
event: 'click mouseenter',
solo: true
},
hide: {
event: 'click mouseleave',
delay: 500,
/*
don't hide the tooltip if it is moused over
*/
fixed: true
},
style: {
classes: 'ui-tooltip-annotatable'
},
events: {
show: this.onShowTip,
move: this.onMoveTip
}
};
};
Annotatable.prototype.onClickToggleAnnotations = function(e) {
return this.toggleAnnotations();
};
Annotatable.prototype.onClickToggleInstructions = function(e) {
return this.toggleInstructions();
};
Annotatable.prototype.onClickReply = function(e) {
return this.replyTo(e.currentTarget);
};
Annotatable.prototype.onClickReturn = function(e) {
return this.returnFrom(e.currentTarget);
};
Annotatable.prototype.onShowTip = function(event, api) {
if (this.annotationsHidden) {
return event.preventDefault();
}
};
Annotatable.prototype.onMoveTip = function(event, api, position) {
/*
This method handles a vertical positioning bug in Firefox as
well as an edge case in which a tooltip is displayed above a
non-overlapping span like this:
(( TOOLTIP ))
\/
text text text ... text text text ...... <span span span>
<span span span>
The problem is that the tooltip looks disconnected from both spans, so
we should re-position the tooltip to appear above the span.
*/
var adjust_y, container, container_offset, focus_rect, is_non_overlapping, offset_left, offset_top, rect_center, rect_top, rects, ref, ref1, ref2, target, tip, tip_height, tip_left, tip_top, tip_width, win_width;
tip = api.elements.tooltip;
adjust_y = ((ref = api.options.position) != null ? (ref1 = ref.adjust) != null ? ref1.y : void 0 : void 0) || 0;
container = ((ref2 = api.options.position) != null ? ref2.container : void 0) || $('body');
target = api.elements.target;
rects = $(target).get(0).getClientRects();
is_non_overlapping = (rects != null ? rects.length : void 0) === 2 && rects[0].left > rects[1].right;
if (is_non_overlapping) {
/*
we want to choose the largest of the two non-overlapping spans and display
the tooltip above the center of it (see api.options.position settings)
*/
focus_rect = (rects[0].width > rects[1].width ? rects[0] : rects[1]);
} else {
/*
always compute the new position because Firefox doesn't
properly vertically position the tooltip
*/
focus_rect = rects[0];
}
rect_center = focus_rect.left + (focus_rect.width / 2);
rect_top = focus_rect.top;
tip_width = $(tip).width();
tip_height = $(tip).height();
/*
tooltip is positioned relative to its container, so we need to factor in offsets
*/
container_offset = $(container).offset();
offset_left = -container_offset.left;
offset_top = $(document).scrollTop() - container_offset.top;
tip_left = offset_left + rect_center - (tip_width / 2);
tip_top = offset_top + rect_top - tip_height + adjust_y;
/*
make sure the new tip position doesn't clip the edges of the screen
*/
win_width = $(window).width();
if (tip_left < offset_left) {
tip_left = offset_left;
} else if (tip_left + tip_width > win_width + offset_left) {
tip_left = win_width + offset_left - tip_width;
}
/*
final step: update the position object (used by qtip2 to show the tip after the move event)
*/
return $.extend(position, {
'left': tip_left,
'top': tip_top
});
};
Annotatable.prototype.getSpanForProblemReturn = function(el) {
var problem_id;
problem_id = $(this.problemReturnSelector).index(el);
return this.$(this.spanSelector).filter("[data-problem-id='" + problem_id + "']");
};
Annotatable.prototype.getProblem = function(el) {
var problem_id;
problem_id = this.getProblemId(el);
return $(this.problemInputSelector).eq(problem_id);
};
Annotatable.prototype.getProblemId = function(el) {
return $(el).data('problem-id');
};
Annotatable.prototype.toggleAnnotations = function() {
var hide;
hide = (this.annotationsHidden = !this.annotationsHidden);
this.toggleAnnotationButtonText(hide);
this.toggleSpans(hide);
return this.toggleTips(hide);
};
Annotatable.prototype.toggleTips = function(hide) {
var visible;
visible = this.findVisibleTips();
return this.hideTips(visible);
};
Annotatable.prototype.toggleAnnotationButtonText = function(hide) {
var buttonText;
if (hide) {
buttonText = gettext('Show Annotations');
} else {
buttonText = gettext('Hide Annotations');
}
return this.$(this.toggleAnnotationsSelector).text(buttonText);
};
Annotatable.prototype.toggleInstructions = function() {
var hide;
hide = (this.instructionsHidden = !this.instructionsHidden);
this.toggleInstructionsButton(hide);
return this.toggleInstructionsText(hide);
};
Annotatable.prototype.toggleInstructionsButton = function(hide) {
var cls, txt;
if (hide) {
txt = gettext('Expand Instructions');
} else {
txt = gettext('Collapse Instructions');
}
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
return this.$(this.toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1]);
};
Annotatable.prototype.toggleInstructionsText = function(hide) {
var slideMethod;
slideMethod = (hide ? 'slideUp' : 'slideDown');
return this.$(this.instructionsSelector)[slideMethod]();
};
Annotatable.prototype.toggleSpans = function(hide) {
return this.$(this.spanSelector).toggleClass('hide', hide, 250);
};
Annotatable.prototype.replyTo = function(buttonEl) {
var el, offset;
offset = -20;
el = this.getProblem(buttonEl);
if (el.length > 0) {
return this.scrollTo(el, this.afterScrollToProblem, offset);
} else {
if (this._debug) {
return console.log('problem not found. event: ', e);
}
}
};
Annotatable.prototype.returnFrom = function(buttonEl) {
var el, offset;
offset = -200;
el = this.getSpanForProblemReturn(buttonEl);
if (el.length > 0) {
return this.scrollTo(el, this.afterScrollToSpan, offset);
} else {
if (this._debug) {
return console.log('span not found. event:', e);
}
}
};
Annotatable.prototype.scrollTo = function(el, after, offset) {
if (offset == null) {
offset = -20;
}
if ($(el).length > 0) {
return $('html,body').scrollTo(el, {
duration: 500,
onAfter: this._once((function(_this) {
return function() {
return after != null ? after.call(_this, el) : void 0;
};
})(this)),
offset: offset
});
}
};
Annotatable.prototype.afterScrollToProblem = function(problem_el) {
return problem_el.effect('highlight', {}, 500);
};
Annotatable.prototype.afterScrollToSpan = function(span_el) {
return span_el.addClass('selected', 400, 'swing', function() {
return span_el.removeClass('selected', 400, 'swing');
});
};
Annotatable.prototype.makeTipContent = function(el) {
return (function(_this) {
return function(api) {
var comment, problem_id, reply, text;
text = $(el).data('comment-body');
comment = _this.createComment(text);
problem_id = _this.getProblemId(el);
reply = _this.createReplyLink(problem_id);
return $(comment).add(reply);
};
})(this);
};
Annotatable.prototype.makeTipTitle = function(el) {
return (function(_this) {
return function(api) {
var title;
title = $(el).data('comment-title');
if (title) {
return title;
} else {
return gettext('Commentary');
}
};
})(this);
};
Annotatable.prototype.createComment = function(text) {
return $("<div class=\"annotatable-comment\">" + text + "</div>"); // xss-lint: disable=javascript-concat-html
};
Annotatable.prototype.createReplyLink = function(problem_id) {
var linktxt;
linktxt = gettext('Reply to Annotation');
return $("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"" + problem_id + "\">" + linktxt + "</a>"); // xss-lint: disable=javascript-concat-html
};
Annotatable.prototype.findVisibleTips = function() {
var visible;
visible = [];
this.$(this.spanSelector).each(function(index, el) {
var api, tip;
api = $(el).qtip('api');
tip = $(api != null ? api.elements.tooltip : void 0);
if (tip.is(':visible')) {
return visible.push(el);
}
});
return visible;
};
Annotatable.prototype.hideTips = function(elements) {
return $(elements).qtip('hide');
};
Annotatable.prototype._once = function(fn) {
var done;
done = false;
return (function(_this) {
return function() {
if (!done) {
fn.call(_this);
}
return done = true;
};
})(this);
};
return Annotatable;
})();
}).call(this);