diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py index 5b26f4d953..5b3cc15c77 100644 --- a/common/lib/xmodule/xmodule/annotatable_module.py +++ b/common/lib/xmodule/xmodule/annotatable_module.py @@ -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): diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss index 9b2cbc3763..d1f39332f6 100644 --- a/common/lib/xmodule/xmodule/css/annotatable/display.scss +++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss @@ -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; - } -} +} \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 4a5e2e42d6..45cbb20bec 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -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) -> + $("Reply to Comment") + + hideAllTips: () -> + @$(@spanSelector).each (index, el) -> $(el).qtip('api').hide() \ No newline at end of file diff --git a/lms/templates/annotatable.html b/lms/templates/annotatable.html index 78c2d5a97a..5f60c6cba2 100644 --- a/lms/templates/annotatable.html +++ b/lms/templates/annotatable.html @@ -1,22 +1,14 @@