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:
Arthur Barrett
2013-02-07 19:51:22 -05:00
parent 2097b69332
commit 7ea87793c2
4 changed files with 173 additions and 191 deletions

View File

@@ -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):

View File

@@ -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;
}
}
}

View File

@@ -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()

View File

@@ -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>