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):
""" 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. """
......@@ -46,59 +42,61 @@ class AnnotatableModule(XModule):
callback(element, index, xmltree)
index += 1
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. """
def _set_span_data(self, span, index, xmltree):
""" Sets an ID and discussion anchor for the span. """
span_id = 'span-{0}'.format(index) # How should we anchor spans?
span.set('data-span-id', span_id)
if 'anchor' in span.attrib:
span.set('data-discussion-anchor', span.get('anchor'))
del span.attrib['anchor']
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)
def _decorate_span(self, span, index, xmltree):
""" Decorates the span with an icon and highlight. """
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. """
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;
.ui-tooltip.qtip.ui-tooltip-annotatable {
$border-color: #F1D031;
.ui-tooltip-titlebar {
border-color: $border-color;
}
.ui-tooltip-content {
background: rgba(255, 255, 255, 0.9);
border: 1px solid $border-color;
border-radius: 3px;
margin: 1em 0;
position: relative;
color: #000;
margin-bottom: 6px;
margin-right: 0;
overflow: visible;
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 {
display: none;
text-align: left;
-webkit-font-smoothing: antialiased;
}
}
\ No newline at end of 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)
$: (selector) ->
$(selector, @el)
init: () ->
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) ->
@$(@toggleSelector).bind 'click', @onClickToggleAnnotations
@$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply
initToolTips: () ->
@$(@spanSelector).each (index, el) =>
$(el).qtip(@getTipOptions el)
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
$(@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)
_once: (fn) ->
done = false
return =>
fn.call this unless done
done = true
_bind: (fn) ->
return => fn.apply(this, arguments)
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()
\ No newline at end of file
<div class="annotatable-wrapper" id="${element_id}-wrapper">
<div class="annotatable-header">
<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>
<div class="annotatable-content">
<div class="annotatable-content">
${html_content}
</div>
<script>
$(function() {
$('#${element_id}-wrapper').data('spans', ${json_discussion_for});
});
</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