Modified annotatable to retrieve and display instructor comments from span elements. Instructor commentaries are displayed in tooltips with a reply link when the highlighted area is clicked. The commentaries are connected to inline discussion forums by anchors (for now).
This commit is contained in:
@@ -32,10 +32,6 @@ class AnnotatableModule(XModule):
|
||||
""" Returns true if the element is a valid annotation span, false otherwise. """
|
||||
return element.tag == 'span' and element.get('class') == 'annotatable'
|
||||
|
||||
def _is_span_container(self, element):
|
||||
""" Returns true if the element is a valid span contanier, false otherwise. """
|
||||
return element.tag == 'p' # Assume content is in paragraph form (for now...)
|
||||
|
||||
def _iterspans(self, xmltree, callbacks):
|
||||
""" Iterates over span elements and invokes each callback on the span. """
|
||||
|
||||
@@ -45,60 +41,62 @@ class AnnotatableModule(XModule):
|
||||
for callback in callbacks:
|
||||
callback(element, index, xmltree)
|
||||
index += 1
|
||||
|
||||
def _set_span_data(self, span, index, xmltree):
|
||||
""" Sets an ID and discussion anchor for the span. """
|
||||
|
||||
if 'anchor' in span.attrib:
|
||||
span.set('data-discussion-anchor', span.get('anchor'))
|
||||
del span.attrib['anchor']
|
||||
|
||||
def _get_span_container(self, span):
|
||||
""" Returns the first container element of the span.
|
||||
The intent is to add the discussion widgets at the
|
||||
end of the container, not interspersed with the text. """
|
||||
|
||||
container = None
|
||||
for parent in span.iterancestors():
|
||||
if self._is_span_container(parent):
|
||||
container = parent
|
||||
break
|
||||
|
||||
if container is None:
|
||||
return parent
|
||||
return container
|
||||
|
||||
def _get_discussion_html(self, discussion_id, discussion_title):
|
||||
""" Returns html to display the discussion thread """
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
'discussion_title': discussion_title
|
||||
}
|
||||
return self.system.render_template('annotatable_discussion.html', context)
|
||||
|
||||
def _attach_discussion(self, span, index, xmltree):
|
||||
""" Attaches a discussion thread to the annotation span. """
|
||||
|
||||
span_id = 'span-{0}'.format(index) # How should we anchor spans?
|
||||
span.set('data-span-id', span_id)
|
||||
|
||||
discussion_id = 'discussion-{0}'.format(index) # How do we get a real discussion ID?
|
||||
discussion_title = 'Thread Title {0}'.format(index) # How do we get the discussion Title?
|
||||
discussion_html = self._get_discussion_html(discussion_id, discussion_title)
|
||||
discussion_xmltree = etree.fromstring(discussion_html)
|
||||
|
||||
span_container = self._get_span_container(span)
|
||||
span_container.append(discussion_xmltree)
|
||||
|
||||
self.discussion_for[span_id] = discussion_id
|
||||
|
||||
def _add_icon(self, span, index, xmltree):
|
||||
""" Adds an icon to the annotation span. """
|
||||
def _decorate_span(self, span, index, xmltree):
|
||||
""" Decorates the span with an icon and highlight. """
|
||||
|
||||
cls = ['annotatable', ]
|
||||
marker = self._get_marker_color(span)
|
||||
if marker is None:
|
||||
cls.append('highlight-yellow')
|
||||
else:
|
||||
cls.append('highlight-'+marker)
|
||||
|
||||
span.set('class', ' '.join(cls))
|
||||
span_icon = etree.Element('span', { 'class': 'annotatable-icon'} )
|
||||
span_icon.text = '';
|
||||
span_icon.tail = span.text
|
||||
span.text = ''
|
||||
span.insert(0, span_icon)
|
||||
|
||||
def _decorate_comment(self, span, index, xmltree):
|
||||
""" Sets the comment class. """
|
||||
|
||||
comment = None
|
||||
for child in span.iterchildren():
|
||||
if child.get('class') == 'comment':
|
||||
comment = child
|
||||
break
|
||||
|
||||
if comment is not None:
|
||||
comment.set('class', 'annotatable-comment')
|
||||
|
||||
def _get_marker_color(self, span):
|
||||
valid_markers = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
if 'marker' in span.attrib:
|
||||
marker = span.attrib['marker']
|
||||
del span.attrib['marker']
|
||||
if marker in valid_markers:
|
||||
return marker
|
||||
return None
|
||||
|
||||
def _render(self):
|
||||
""" Renders annotatable content by transforming spans and adding discussions. """
|
||||
|
||||
xmltree = etree.fromstring(self.content)
|
||||
self._iterspans(xmltree, [ self._add_icon, self._attach_discussion ])
|
||||
self._iterspans(xmltree, [
|
||||
self._set_span_data,
|
||||
self._decorate_span,
|
||||
self._decorate_comment
|
||||
])
|
||||
|
||||
return etree.tostring(xmltree)
|
||||
|
||||
def get_html(self):
|
||||
@@ -107,8 +105,7 @@ class AnnotatableModule(XModule):
|
||||
context = {
|
||||
'display_name': self.display_name,
|
||||
'element_id': self.element_id,
|
||||
'html_content': self._render(),
|
||||
'json_discussion_for': json.dumps(self.discussion_for)
|
||||
'html_content': self._render()
|
||||
}
|
||||
|
||||
# template dir: lms/templates
|
||||
@@ -121,7 +118,7 @@ class AnnotatableModule(XModule):
|
||||
|
||||
self.element_id = self.location.html_id();
|
||||
self.content = self.definition['data']
|
||||
self.discussion_for = {} # Maps spans to discussions by id (for JS)
|
||||
self.spans = {}
|
||||
|
||||
|
||||
class AnnotatableDescriptor(RawDescriptor):
|
||||
|
||||
@@ -20,18 +20,31 @@
|
||||
}
|
||||
|
||||
span.annotatable {
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
.annotatable-icon {
|
||||
margin: auto 2px auto 4px;
|
||||
@each $highlight in (
|
||||
(yellow rgb(239, 255, 0)),
|
||||
(orange rgb(255,113,0)),
|
||||
(purple rgb(255,0,197)),
|
||||
(blue rgb(0,90,255)),
|
||||
(green rgb(111,255,9))) {
|
||||
&.highlight-#{nth($highlight,1)} {
|
||||
background-color: #{lighten(nth($highlight,2), 20%)};
|
||||
}
|
||||
}
|
||||
&.hide {
|
||||
cursor: none;
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
.annotatable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-comment {
|
||||
display: none;
|
||||
}
|
||||
.annotatable-icon {
|
||||
margin: auto 2px auto 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-icon {
|
||||
@@ -42,6 +55,11 @@ span.annotatable {
|
||||
background: url(../images/link-icon.png) no-repeat;
|
||||
}
|
||||
|
||||
.annotatable-reply {
|
||||
display: block;
|
||||
margin: 1em 0 .5em 0;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -53,30 +71,20 @@ span.annotatable {
|
||||
background: url(../images/info-icon.png) no-repeat;
|
||||
}
|
||||
|
||||
.annotatable-discussion {
|
||||
display: block;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 3px;
|
||||
margin: 1em 0;
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
|
||||
.annotatable-discussion-label {
|
||||
font-weight: bold;
|
||||
.ui-tooltip.qtip.ui-tooltip-annotatable {
|
||||
$border-color: #F1D031;
|
||||
.ui-tooltip-titlebar {
|
||||
border-color: $border-color;
|
||||
}
|
||||
.annotatable-icon {
|
||||
margin: auto 4px auto 0px;
|
||||
.ui-tooltip-content {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid $border-color;
|
||||
color: #000;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
overflow: visible;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.annotatable-show-discussion {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&.opaque {
|
||||
opacity: 0.4;
|
||||
}
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,108 +2,93 @@ class @Annotatable
|
||||
@_debug: true
|
||||
|
||||
wrapperSelector: '.annotatable-wrapper'
|
||||
spanSelector: 'span.annotatable[data-span-id]'
|
||||
discussionSelector: '.annotatable-discussion[data-discussion-id]'
|
||||
toggleSelector: '.annotatable-toggle'
|
||||
spanSelector: 'span.annotatable'
|
||||
commentSelector: '.annotatable-comment'
|
||||
replySelector: 'a.annotatable-reply'
|
||||
|
||||
constructor: (el) ->
|
||||
console.log 'loaded Annotatable' if @_debug
|
||||
@el = el
|
||||
@init()
|
||||
@init(el)
|
||||
|
||||
init: () ->
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
init: (el) ->
|
||||
@el = el
|
||||
@hideAnnotations = false
|
||||
@spandata = {}
|
||||
@loadSpanData()
|
||||
@initEvents()
|
||||
@initToolTips()
|
||||
|
||||
initEvents: () ->
|
||||
$(@toggleSelector, @el).bind('click', @_bind @onClickToggleAnnotations)
|
||||
|
||||
$(@wrapperSelector, @el).delegate(@spanSelector, {
|
||||
'click': @_bind @onSpanEvent @onClickSpan
|
||||
'mouseenter': @_bind @onSpanEvent @onEnterSpan
|
||||
'mouseleave': @_bind @onSpanEvent @onLeaveSpan
|
||||
})
|
||||
|
||||
loadSpanData: () ->
|
||||
@spandata = $(@wrapperSelector, @el).data('spans')
|
||||
|
||||
getDiscussionId: (span_id) ->
|
||||
@spandata[span_id]
|
||||
|
||||
getDiscussionEl: (discussion_id) ->
|
||||
$(@discussionSelector, @el).filter('[data-discussion-id="'+discussion_id+'"]')
|
||||
|
||||
onClickToggleAnnotations: (e) ->
|
||||
@hideAnnotations = !@hideAnnotations
|
||||
$(@spanSelector, @el).add(@discussionSelector, @el).toggleClass('hide', @hideAnnotations)
|
||||
$(@toggleSelector, @el).text(if @hideAnnotations then 'Show Annotations' else 'Hide Annotations')
|
||||
|
||||
onSpanEvent: (fn) ->
|
||||
(e) =>
|
||||
span_el = e.currentTarget
|
||||
span_id = span_el.getAttribute('data-span-id')
|
||||
discussion_id = @getDiscussionId(span_id)
|
||||
discussion_el = @getDiscussionEl(discussion_id)
|
||||
span = {
|
||||
id: span_id
|
||||
el: span_el
|
||||
}
|
||||
discussion = {
|
||||
id: discussion_id
|
||||
el: discussion_el
|
||||
}
|
||||
if !@hideAnnotations
|
||||
fn.call this, span, discussion
|
||||
|
||||
onClickSpan: (span, discussion) ->
|
||||
@scrollToDiscussion(discussion.el)
|
||||
|
||||
onEnterSpan: (span, discussion) ->
|
||||
@focusDiscussion(discussion.el, true)
|
||||
|
||||
onLeaveSpan: (span, discussion) ->
|
||||
@focusDiscussion(discussion.el, false)
|
||||
|
||||
focusDiscussion: (el, state) ->
|
||||
$(@discussionSelector, @el).not(el).toggleClass('opaque', state)
|
||||
|
||||
scrollToDiscussion: (el) ->
|
||||
padding = 20
|
||||
complete = @makeHighlighter(el)
|
||||
animOpts = {
|
||||
scrollTop : el.offset().top - padding
|
||||
}
|
||||
|
||||
if @canScrollToDiscussion(el)
|
||||
$('html, body').animate(animOpts, 500, 'swing', complete)
|
||||
else
|
||||
complete()
|
||||
|
||||
canScrollToDiscussion: (el) ->
|
||||
scrollTop = el.offset().top
|
||||
docHeight = $(document).height()
|
||||
winHeight = $(window).height()
|
||||
winScrollTop = window.scrollY
|
||||
|
||||
viewStart = winScrollTop
|
||||
viewEnd = winScrollTop + (.75 * winHeight)
|
||||
inView = viewStart < scrollTop < viewEnd
|
||||
|
||||
scrollable = !inView
|
||||
atDocEnd = viewStart + winHeight >= docHeight
|
||||
|
||||
return (if atDocEnd then false else scrollable)
|
||||
|
||||
makeHighlighter: (el) ->
|
||||
return @_once -> el.effect('highlight', {}, 500)
|
||||
@$(@toggleSelector).bind 'click', @onClickToggleAnnotations
|
||||
@$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply
|
||||
|
||||
_once: (fn) ->
|
||||
done = false
|
||||
return =>
|
||||
fn.call this unless done
|
||||
done = true
|
||||
initToolTips: () ->
|
||||
@$(@spanSelector).each (index, el) =>
|
||||
$(el).qtip(@getTipOptions el)
|
||||
|
||||
_bind: (fn) ->
|
||||
return => fn.apply(this, arguments)
|
||||
getTipOptions: (el) ->
|
||||
content:
|
||||
title:
|
||||
text: @makeTipTitle(el)
|
||||
button: 'Close'
|
||||
text: @makeTipComment(el)
|
||||
position:
|
||||
my: 'bottom center' # of tooltip
|
||||
at: 'top center' # of target
|
||||
target: 'mouse'
|
||||
container: @$(@wrapperSelector)
|
||||
adjust:
|
||||
mouse: false # dont follow the mouse
|
||||
method: 'shift none'
|
||||
show:
|
||||
event: 'click'
|
||||
hide:
|
||||
event: 'click'
|
||||
style:
|
||||
classes: 'ui-tooltip-annotatable'
|
||||
events:
|
||||
show: @onShowTipComment
|
||||
|
||||
onShowTipComment: (event, api) =>
|
||||
event.preventDefault() if @hideAnnotations
|
||||
|
||||
onClickToggleAnnotations: (e) =>
|
||||
@hideAnnotations = !@hideAnnotations
|
||||
hide = @hideAnnotations
|
||||
|
||||
@hideAllTips() if hide
|
||||
@$(@spanSelector).toggleClass('hide', hide)
|
||||
@$(@toggleSelector).text((if hide then 'Show' else 'Hide') + ' Annotations')
|
||||
|
||||
onClickReply: (e) =>
|
||||
hash = $(e.currentTarget).attr('href')
|
||||
if hash?.charAt(0) == '#'
|
||||
name = hash.substr(1)
|
||||
anchor = $("a[name='#{name}']").first()
|
||||
@scrollTo(anchor) if anchor.length == 1
|
||||
|
||||
scrollTo: (el, padding = 20) ->
|
||||
scrollTop = el.offset().top - padding
|
||||
$('html,body').animate(scrollTop: scrollTop, 500, 'swing')
|
||||
|
||||
makeTipComment: (el) ->
|
||||
return (api) =>
|
||||
comment = $(@commentSelector, el).first().clone()
|
||||
anchor = $(el).data('discussion-anchor')
|
||||
if anchor
|
||||
comment.append(@createReplyLink(anchor))
|
||||
comment.contents()
|
||||
|
||||
makeTipTitle: (el) ->
|
||||
return (api) =>
|
||||
comment = $(@commentSelector, el).first()
|
||||
title = comment.attr('title')
|
||||
(if title then title else 'Commentary')
|
||||
|
||||
createReplyLink: (anchor) ->
|
||||
$("<a class=\"annotatable-reply\" href=\"##{anchor}\">Reply to Comment</a>")
|
||||
|
||||
hideAllTips: () ->
|
||||
@$(@spanSelector).each (index, el) -> $(el).qtip('api').hide()
|
||||
@@ -1,22 +1,14 @@
|
||||
<div class="annotatable-wrapper" id="${element_id}-wrapper">
|
||||
|
||||
<div class="annotatable-header">
|
||||
<div class="help-icon"></div>
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<div class="annotatable-title">${display_name} </div>
|
||||
% endif
|
||||
<div class="annotatable-description">Annotated Reading + Guided Discussion</div>
|
||||
<a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a>
|
||||
</div>
|
||||
|
||||
<div class="annotatable-content">
|
||||
${html_content}
|
||||
</div>
|
||||
<div class="annotatable-header">
|
||||
<div class="help-icon"></div>
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<div class="annotatable-title">${display_name} </div>
|
||||
% endif
|
||||
<div class="annotatable-description">Annotated Reading + Guided Discussion</div>
|
||||
<a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#${element_id}-wrapper').data('spans', ${json_discussion_for});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="annotatable-content">
|
||||
${html_content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user