Commit 7ea87793 by Arthur Barrett

Modified annotatable to retrieve and display instructor comments from span…

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).
parent 2097b693
...@@ -32,10 +32,6 @@ class AnnotatableModule(XModule): ...@@ -32,10 +32,6 @@ class AnnotatableModule(XModule):
""" Returns true if the element is a valid annotation span, false otherwise. """ """ Returns true if the element is a valid annotation span, false otherwise. """
return element.tag == 'span' and element.get('class') == 'annotatable' 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): def _iterspans(self, xmltree, callbacks):
""" Iterates over span elements and invokes each callback on the span. """ """ Iterates over span elements and invokes each callback on the span. """
...@@ -45,60 +41,62 @@ class AnnotatableModule(XModule): ...@@ -45,60 +41,62 @@ class AnnotatableModule(XModule):
for callback in callbacks: for callback in callbacks:
callback(element, index, xmltree) callback(element, index, xmltree)
index += 1 index += 1
def _get_span_container(self, span): def _set_span_data(self, span, index, xmltree):
""" Returns the first container element of the span. """ Sets an ID and discussion anchor for 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? if 'anchor' in span.attrib:
discussion_title = 'Thread Title {0}'.format(index) # How do we get the discussion Title? span.set('data-discussion-anchor', span.get('anchor'))
discussion_html = self._get_discussion_html(discussion_id, discussion_title) del span.attrib['anchor']
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): def _decorate_span(self, span, index, xmltree):
""" Adds an icon to the annotation span. """ """ 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 = etree.Element('span', { 'class': 'annotatable-icon'} )
span_icon.text = ''; span_icon.text = '';
span_icon.tail = span.text span_icon.tail = span.text
span.text = '' span.text = ''
span.insert(0, span_icon) 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): def _render(self):
""" Renders annotatable content by transforming spans and adding discussions. """ """ Renders annotatable content by transforming spans and adding discussions. """
xmltree = etree.fromstring(self.content) 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) return etree.tostring(xmltree)
def get_html(self): def get_html(self):
...@@ -107,8 +105,7 @@ class AnnotatableModule(XModule): ...@@ -107,8 +105,7 @@ class AnnotatableModule(XModule):
context = { context = {
'display_name': self.display_name, 'display_name': self.display_name,
'element_id': self.element_id, 'element_id': self.element_id,
'html_content': self._render(), 'html_content': self._render()
'json_discussion_for': json.dumps(self.discussion_for)
} }
# template dir: lms/templates # template dir: lms/templates
...@@ -121,7 +118,7 @@ class AnnotatableModule(XModule): ...@@ -121,7 +118,7 @@ class AnnotatableModule(XModule):
self.element_id = self.location.html_id(); self.element_id = self.location.html_id();
self.content = self.definition['data'] self.content = self.definition['data']
self.discussion_for = {} # Maps spans to discussions by id (for JS) self.spans = {}
class AnnotatableDescriptor(RawDescriptor): class AnnotatableDescriptor(RawDescriptor):
......
...@@ -20,18 +20,31 @@ ...@@ -20,18 +20,31 @@
} }
span.annotatable { span.annotatable {
color: $blue;
cursor: pointer; cursor: pointer;
.annotatable-icon { @each $highlight in (
margin: auto 2px auto 4px; (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 { &.hide {
cursor: none; cursor: none;
color: inherit; background-color: inherit;
.annotatable-icon { .annotatable-icon {
display: none; display: none;
} }
} }
.annotatable-comment {
display: none;
}
.annotatable-icon {
margin: auto 2px auto 4px;
}
} }
.annotatable-icon { .annotatable-icon {
...@@ -42,6 +55,11 @@ span.annotatable { ...@@ -42,6 +55,11 @@ span.annotatable {
background: url(../images/link-icon.png) no-repeat; background: url(../images/link-icon.png) no-repeat;
} }
.annotatable-reply {
display: block;
margin: 1em 0 .5em 0;
}
.help-icon { .help-icon {
display: block; display: block;
position: absolute; position: absolute;
...@@ -53,30 +71,20 @@ span.annotatable { ...@@ -53,30 +71,20 @@ span.annotatable {
background: url(../images/info-icon.png) no-repeat; background: url(../images/info-icon.png) no-repeat;
} }
.annotatable-discussion { .ui-tooltip.qtip.ui-tooltip-annotatable {
display: block; $border-color: #F1D031;
border: 1px solid $border-color; .ui-tooltip-titlebar {
border-radius: 3px; border-color: $border-color;
margin: 1em 0;
position: relative;
padding: 4px;
.annotatable-discussion-label {
font-weight: bold;
}
.annotatable-icon {
margin: auto 4px auto 0px;
}
.annotatable-show-discussion {
position: absolute;
right: 8px;
margin-top: 4px;
}
&.opaque {
opacity: 0.4;
} }
&.hide { .ui-tooltip-content {
display: none; 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;
} }
} }
\ No newline at end of file
...@@ -2,108 +2,93 @@ class @Annotatable ...@@ -2,108 +2,93 @@ class @Annotatable
@_debug: true @_debug: true
wrapperSelector: '.annotatable-wrapper' wrapperSelector: '.annotatable-wrapper'
spanSelector: 'span.annotatable[data-span-id]'
discussionSelector: '.annotatable-discussion[data-discussion-id]'
toggleSelector: '.annotatable-toggle' toggleSelector: '.annotatable-toggle'
spanSelector: 'span.annotatable'
commentSelector: '.annotatable-comment'
replySelector: 'a.annotatable-reply'
constructor: (el) -> constructor: (el) ->
console.log 'loaded Annotatable' if @_debug console.log 'loaded Annotatable' if @_debug
@el = el @init(el)
@init()
$: (selector) ->
$(selector, @el)
init: () -> init: (el) ->
@el = el
@hideAnnotations = false @hideAnnotations = false
@spandata = {}
@loadSpanData()
@initEvents() @initEvents()
@initToolTips()
initEvents: () -> initEvents: () ->
$(@toggleSelector, @el).bind('click', @_bind @onClickToggleAnnotations) @$(@toggleSelector).bind 'click', @onClickToggleAnnotations
@$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply
initToolTips: () ->
@$(@spanSelector).each (index, el) =>
$(el).qtip(@getTipOptions el)
$(@wrapperSelector, @el).delegate(@spanSelector, { getTipOptions: (el) ->
'click': @_bind @onSpanEvent @onClickSpan content:
'mouseenter': @_bind @onSpanEvent @onEnterSpan title:
'mouseleave': @_bind @onSpanEvent @onLeaveSpan 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
loadSpanData: () -> onShowTipComment: (event, api) =>
@spandata = $(@wrapperSelector, @el).data('spans') event.preventDefault() if @hideAnnotations
getDiscussionId: (span_id) ->
@spandata[span_id]
getDiscussionEl: (discussion_id) ->
$(@discussionSelector, @el).filter('[data-discussion-id="'+discussion_id+'"]')
onClickToggleAnnotations: (e) -> onClickToggleAnnotations: (e) =>
@hideAnnotations = !@hideAnnotations @hideAnnotations = !@hideAnnotations
$(@spanSelector, @el).add(@discussionSelector, @el).toggleClass('hide', @hideAnnotations) 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) -> @hideAllTips() if hide
@scrollToDiscussion(discussion.el) @$(@spanSelector).toggleClass('hide', hide)
@$(@toggleSelector).text((if hide then 'Show' else 'Hide') + ' Annotations')
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) -> onClickReply: (e) =>
scrollTop = el.offset().top hash = $(e.currentTarget).attr('href')
docHeight = $(document).height() if hash?.charAt(0) == '#'
winHeight = $(window).height() name = hash.substr(1)
winScrollTop = window.scrollY anchor = $("a[name='#{name}']").first()
@scrollTo(anchor) if anchor.length == 1
viewStart = winScrollTop scrollTo: (el, padding = 20) ->
viewEnd = winScrollTop + (.75 * winHeight) scrollTop = el.offset().top - padding
inView = viewStart < scrollTop < viewEnd $('html,body').animate(scrollTop: scrollTop, 500, 'swing')
scrollable = !inView makeTipComment: (el) ->
atDocEnd = viewStart + winHeight >= docHeight return (api) =>
comment = $(@commentSelector, el).first().clone()
anchor = $(el).data('discussion-anchor')
if anchor
comment.append(@createReplyLink(anchor))
comment.contents()
return (if atDocEnd then false else scrollable) makeTipTitle: (el) ->
return (api) =>
comment = $(@commentSelector, el).first()
title = comment.attr('title')
(if title then title else 'Commentary')
makeHighlighter: (el) -> createReplyLink: (anchor) ->
return @_once -> el.effect('highlight', {}, 500) $("<a class=\"annotatable-reply\" href=\"##{anchor}\">Reply to Comment</a>")
_once: (fn) -> hideAllTips: () ->
done = false @$(@spanSelector).each (index, el) -> $(el).qtip('api').hide()
return => \ No newline at end of file
fn.call this unless done
done = true
_bind: (fn) ->
return => fn.apply(this, arguments)
<div class="annotatable-wrapper" id="${element_id}-wrapper"> <div class="annotatable-wrapper" id="${element_id}-wrapper">
<div class="annotatable-header">
<div class="annotatable-header"> <div class="help-icon"></div>
<div class="help-icon"></div> % if display_name is not UNDEFINED and display_name is not None:
% if display_name is not UNDEFINED and display_name is not None: <div class="annotatable-title">${display_name} </div>
<div class="annotatable-title">${display_name} </div> % endif
% endif <div class="annotatable-description">Annotated Reading + Guided Discussion</div>
<div class="annotatable-description">Annotated Reading + Guided Discussion</div> <a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a>
<a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a> </div>
</div>
<div class="annotatable-content">
${html_content}
</div>
<script> <div class="annotatable-content">
$(function() { ${html_content}
$('#${element_id}-wrapper').data('spans', ${json_discussion_for}); </div>
});
</script>
</div> </div>
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