diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index eb7bfb6db9..3b755b0ec2 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -1,6 +1,8 @@ """ Tests for utils. """ from contentstore import utils import mock +import collections +import copy from django.test import TestCase from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase): 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) ) + + +class ExtraPanelTabTestCase(TestCase): + """ Tests adding and removing extra course tabs. """ + + def get_tab_type_dicts(self, tab_types): + """ Returns an array of tab dictionaries. """ + if tab_types: + return [{'tab_type': tab_type} for tab_type in tab_types.split(',')] + else: + return [] + + def get_course_with_tabs(self, tabs=[]): + """ Returns a mock course object with a tabs attribute. """ + course = collections.namedtuple('MockCourse', ['tabs']) + if isinstance(tabs, basestring): + course.tabs = self.get_tab_type_dicts(tabs) + else: + course.tabs = tabs + return course + + def test_add_extra_panel_tab(self): + """ Tests if a tab can be added to a course tab list. """ + for tab_type in utils.EXTRA_TAB_PANELS.keys(): + tab = utils.EXTRA_TAB_PANELS.get(tab_type) + + # test adding with changed = True + for tab_setup in ['', 'x', 'x,y,z']: + course = self.get_course_with_tabs(tab_setup) + expected_tabs = copy.copy(course.tabs) + expected_tabs.append(tab) + changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course) + self.assertTrue(changed) + self.assertEqual(actual_tabs, expected_tabs) + + # test adding with changed = False + tab_test_setup = [ + [tab], + [tab, self.get_tab_type_dicts('x,y,z')], + [self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')], + [self.get_tab_type_dicts('x,y,z'), tab]] + + for tab_setup in tab_test_setup: + course = self.get_course_with_tabs(tab_setup) + expected_tabs = copy.copy(course.tabs) + changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course) + self.assertFalse(changed) + self.assertEqual(actual_tabs, expected_tabs) + + def test_remove_extra_panel_tab(self): + """ Tests if a tab can be removed from a course tab list. """ + for tab_type in utils.EXTRA_TAB_PANELS.keys(): + tab = utils.EXTRA_TAB_PANELS.get(tab_type) + + # test removing with changed = True + tab_test_setup = [ + [tab], + [tab, self.get_tab_type_dicts('x,y,z')], + [self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')], + [self.get_tab_type_dicts('x,y,z'), tab]] + + for tab_setup in tab_test_setup: + course = self.get_course_with_tabs(tab_setup) + expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)] + changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course) + self.assertTrue(changed) + self.assertEqual(actual_tabs, expected_tabs) + + # test removing with changed = False + for tab_setup in ['', 'x', 'x,y,z']: + course = self.get_course_with_tabs(tab_setup) + expected_tabs = copy.copy(course.tabs) + changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course) + self.assertFalse(changed) + self.assertEqual(actual_tabs, expected_tabs) + diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index a5a3b47bce..ea3e3ecd6a 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta #In order to instantiate an open ended tab automatically, need to have this data OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} +NOTES_PANEL = {"name": "My Notes", "type": "notes"} +EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]]) def get_modulestore(location): @@ -192,9 +194,10 @@ class CoursePageNames: Checklists = "checklists" -def add_open_ended_panel_tab(course): +def add_extra_panel_tab(tab_type, course): """ - Used to add the open ended panel tab to a course if it does not exist. + Used to add the panel tab to a course if it does not exist. + @param tab_type: A string representing the tab type. @param course: A course object from the modulestore. @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. """ @@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course): course_tabs = copy.copy(course.tabs) changed = False #Check to see if open ended panel is defined in the course - if OPEN_ENDED_PANEL not in course_tabs: + + tab_panel = EXTRA_TAB_PANELS.get(tab_type) + if tab_panel not in course_tabs: #Add panel to the tabs if it is not defined - course_tabs.append(OPEN_ENDED_PANEL) + course_tabs.append(tab_panel) changed = True return changed, course_tabs -def remove_open_ended_panel_tab(course): +def remove_extra_panel_tab(tab_type, course): """ - Used to remove the open ended panel tab from a course if it exists. + Used to remove the panel tab from a course if it exists. + @param tab_type: A string representing the tab type. @param course: A course object from the modulestore. @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. """ @@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course): course_tabs = copy.copy(course.tabs) changed = False #Check to see if open ended panel is defined in the course - if OPEN_ENDED_PANEL in course_tabs: + + tab_panel = EXTRA_TAB_PANELS.get(tab_type) + if tab_panel in course_tabs: #Add panel to the tabs if it is not defined - course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL] + course_tabs = [ct for ct in course_tabs if ct != tab_panel] changed = True return changed, course_tabs diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 5127effae6..34a659ab29 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -41,7 +41,8 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] -ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES +NOTE_COMPONENT_TYPES = ['notes'] +ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index c6fa340f67..f326764589 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -20,8 +20,8 @@ from xmodule.modulestore import Location from contentstore.course_info_model \ import get_course_updates, update_course_updates, delete_course_update from contentstore.utils \ - import get_lms_link_for_item, add_open_ended_panel_tab, \ - remove_open_ended_panel_tab + import get_lms_link_for_item, add_extra_panel_tab, \ + remove_extra_panel_tab from models.settings.course_details \ import CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel @@ -32,7 +32,8 @@ from util.json_request import expect_json from .access import has_access, get_location_and_verify_access from .requests import get_request_method from .tabs import initialize_course_tabs -from .component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY +from .component import OPEN_ENDED_COMPONENT_TYPES, \ + NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY __all__ = ['course_index', 'create_new_course', 'course_info', 'course_info_updates', 'get_course_settings', @@ -352,38 +353,52 @@ def course_advanced_updates(request, org, course, name): request_body = json.loads(request.body) # Whether or not to filter the tabs key out of the settings metadata filter_tabs = True - # Check to see if the user instantiated any advanced components. - # This is a hack to add the open ended panel tab - # to a course automatically if the user has indicated that they want - # to edit the combinedopenended or peergrading - # module, and to remove it if they have removed the open ended elements. + + #Check to see if the user instantiated any advanced components. This is a hack + #that does the following : + # 1) adds/removes the open ended panel tab to a course automatically if the user + # has indicated that they want to edit the combinedopendended or peergrading module + # 2) adds/removes the notes panel tab to a course automatically if the user has + # indicated that they want the notes module enabled in their course + # TODO refactor the above into distinct advanced policy settings if ADVANCED_COMPONENT_POLICY_KEY in request_body: - # Check to see if the user instantiated any open ended components - found_oe_type = False - # Get the course so that we can scrape current tabs + #Get the course so that we can scrape current tabs course_module = modulestore().get_item(location) - for oe_type in OPEN_ENDED_COMPONENT_TYPES: - if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: - # Add an open ended tab to the course if needed - changed, new_tabs = add_open_ended_panel_tab(course_module) - # If a tab has been added to the course, then send the - # metadata along to CourseMetadata.update_from_json + + #Maps tab types to components + tab_component_map = { + 'open_ended': OPEN_ENDED_COMPONENT_TYPES, + 'notes': NOTE_COMPONENT_TYPES, + } + + #Check to see if the user instantiated any notes or open ended components + for tab_type in tab_component_map.keys(): + component_types = tab_component_map.get(tab_type) + found_ac_type = False + for ac_type in component_types: + if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Add tab to the course if needed + changed, new_tabs = add_extra_panel_tab(tab_type, course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + if changed: + course_module.tabs = new_tabs + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + #Set this flag to avoid the tab removal code below. + found_ac_type = True + break + #If we did not find a module type in the advanced settings, + # we may need to remove the tab from the course. + if not found_ac_type: + #Remove tab from the course if needed + changed, new_tabs = remove_extra_panel_tab(tab_type, course_module) if changed: + course_module.tabs = new_tabs request_body.update({'tabs': new_tabs}) - # Indicate that tabs should not be filtered out of the metadata + #Indicate that tabs should *not* be filtered out of the metadata filter_tabs = False - # Set this flag to avoid the open ended tab removal code below. - found_oe_type = True - break - # If we did not find an open ended module type in the advanced settings, - # we may need to remove the open ended tab from the course. - if not found_oe_type: - # Remove open ended tab to the course if needed - changed, new_tabs = remove_open_ended_panel_tab(course_module) - if changed: - request_body.update({'tabs': new_tabs}) - # Indicate that tabs should not be filtered out of the metadata - filter_tabs = False + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) diff --git a/common/static/js/vendor/annotator.js b/common/static/js/vendor/annotator.js new file mode 100644 index 0000000000..f66baa2c7e --- /dev/null +++ b/common/static/js/vendor/annotator.js @@ -0,0 +1,1827 @@ +/* +** Annotator 1.2.6-dev-dc18206 +** https://github.com/okfn/annotator/ +** +** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning. +** Dual licensed under the MIT and GPLv3 licenses. +** https://github.com/okfn/annotator/blob/master/LICENSE +** +** Built at: 2013-05-16 18:01:57Z +*/ + + +(function() { + var $, Annotator, Delegator, LinkParser, Range, findChild, fn, functions, g, getNodeName, getNodePosition, gettext, simpleXPathJQuery, simpleXPathPure, util, _Annotator, _gettext, _i, _j, _len, _len1, _ref, _ref1, _t, + __slice = [].slice, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + simpleXPathJQuery = function(relativeRoot) { + var jq; + + jq = this.map(function() { + var elem, idx, path, tagName; + + path = ''; + elem = this; + while (elem && elem.nodeType === 1 && elem !== relativeRoot) { + tagName = elem.tagName.replace(":", "\\:"); + idx = $(elem.parentNode).children(tagName).index(elem) + 1; + idx = "[" + idx + "]"; + path = "/" + elem.tagName.toLowerCase() + idx + path; + elem = elem.parentNode; + } + return path; + }); + return jq.get(); + }; + + simpleXPathPure = function(relativeRoot) { + var getPathSegment, getPathTo, jq, rootNode; + + getPathSegment = function(node) { + var name, pos; + + name = getNodeName(node); + pos = getNodePosition(node); + return "" + name + "[" + pos + "]"; + }; + rootNode = relativeRoot; + getPathTo = function(node) { + var xpath; + + xpath = ''; + while (node !== rootNode) { + if (node == null) { + throw new Error("Called getPathTo on a node which was not a descendant of @rootNode. " + rootNode); + } + xpath = (getPathSegment(node)) + '/' + xpath; + node = node.parentNode; + } + xpath = '/' + xpath; + xpath = xpath.replace(/\/$/, ''); + return xpath; + }; + jq = this.map(function() { + var path; + + path = getPathTo(this); + return path; + }); + return jq.get(); + }; + + findChild = function(node, type, index) { + var child, children, found, name, _i, _len; + + if (!node.hasChildNodes()) { + throw new Error("XPath error: node has no children!"); + } + children = node.childNodes; + found = 0; + for (_i = 0, _len = children.length; _i < _len; _i++) { + child = children[_i]; + name = getNodeName(child); + if (name === type) { + found += 1; + if (found === index) { + return child; + } + } + } + throw new Error("XPath error: wanted child not found."); + }; + + getNodeName = function(node) { + var nodeName; + + nodeName = node.nodeName.toLowerCase(); + switch (nodeName) { + case "#text": + return "text()"; + case "#comment": + return "comment()"; + case "#cdata-section": + return "cdata-section()"; + default: + return nodeName; + } + }; + + getNodePosition = function(node) { + var pos, tmp; + + pos = 0; + tmp = node; + while (tmp) { + if (tmp.nodeName === node.nodeName) { + pos++; + } + tmp = tmp.previousSibling; + } + return pos; + }; + + gettext = null; + + if (typeof Gettext !== "undefined" && Gettext !== null) { + _gettext = new Gettext({ + domain: "annotator" + }); + gettext = function(msgid) { + return _gettext.gettext(msgid); + }; + } else { + gettext = function(msgid) { + return msgid; + }; + } + + _t = function(msgid) { + return gettext(msgid); + }; + + if (!(typeof jQuery !== "undefined" && jQuery !== null ? (_ref = jQuery.fn) != null ? _ref.jquery : void 0 : void 0)) { + console.error(_t("Annotator requires jQuery: have you included lib/vendor/jquery.js?")); + } + + if (!(JSON && JSON.parse && JSON.stringify)) { + console.error(_t("Annotator requires a JSON implementation: have you included lib/vendor/json2.js?")); + } + + $ = jQuery.sub(); + + $.flatten = function(array) { + var flatten; + + flatten = function(ary) { + var el, flat, _i, _len; + + flat = []; + for (_i = 0, _len = ary.length; _i < _len; _i++) { + el = ary[_i]; + flat = flat.concat(el && $.isArray(el) ? flatten(el) : el); + } + return flat; + }; + return flatten(array); + }; + + $.plugin = function(name, object) { + return jQuery.fn[name] = function(options) { + var args; + + args = Array.prototype.slice.call(arguments, 1); + return this.each(function() { + var instance; + + instance = $.data(this, name); + if (instance) { + return options && instance[options].apply(instance, args); + } else { + instance = new object(this, options); + return $.data(this, name, instance); + } + }); + }; + }; + + $.fn.textNodes = function() { + var getTextNodes; + + getTextNodes = function(node) { + var nodes; + + if (node && node.nodeType !== 3) { + nodes = []; + if (node.nodeType !== 8) { + node = node.lastChild; + while (node) { + nodes.push(getTextNodes(node)); + node = node.previousSibling; + } + } + return nodes.reverse(); + } else { + return node; + } + }; + return this.map(function() { + return $.flatten(getTextNodes(this)); + }); + }; + + $.fn.xpath = function(relativeRoot) { + var exception, result; + + try { + result = simpleXPathJQuery.call(this, relativeRoot); + } catch (_error) { + exception = _error; + console.log("jQuery-based XPath construction failed! Falling back to manual."); + result = simpleXPathPure.call(this, relativeRoot); + } + return result; + }; + + $.xpath = function(xp, root) { + var idx, name, node, step, steps, _i, _len, _ref1; + + steps = xp.substring(1).split("/"); + node = root; + for (_i = 0, _len = steps.length; _i < _len; _i++) { + step = steps[_i]; + _ref1 = step.split("["), name = _ref1[0], idx = _ref1[1]; + idx = idx != null ? parseInt((idx != null ? idx.split("]") : void 0)[0]) : 1; + node = findChild(node, name.toLowerCase(), idx); + } + return node; + }; + + $.escape = function(html) { + return html.replace(/&(?!\w+;)/g, '&').replace(//g, '>').replace(/"/g, '"'); + }; + + $.fn.escape = function(html) { + if (arguments.length) { + return this.html($.escape(html)); + } + return this.html(); + }; + + $.fn.reverse = []._reverse || [].reverse; + + functions = ["log", "debug", "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "time", "timeEnd", "profile", "profileEnd", "count", "clear", "table", "error", "notifyFirebug", "firebug", "userObjects"]; + + if (typeof console !== "undefined" && console !== null) { + if (console.group == null) { + console.group = function(name) { + return console.log("GROUP: ", name); + }; + } + if (console.groupCollapsed == null) { + console.groupCollapsed = console.group; + } + for (_i = 0, _len = functions.length; _i < _len; _i++) { + fn = functions[_i]; + if (console[fn] == null) { + console[fn] = function() { + return console.log(_t("Not implemented:") + (" console." + name)); + }; + } + } + } else { + this.console = {}; + for (_j = 0, _len1 = functions.length; _j < _len1; _j++) { + fn = functions[_j]; + this.console[fn] = function() {}; + } + this.console['error'] = function() { + var args; + + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return alert("ERROR: " + (args.join(', '))); + }; + this.console['warn'] = function() { + var args; + + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return alert("WARNING: " + (args.join(', '))); + }; + } + + Delegator = (function() { + Delegator.prototype.events = {}; + + Delegator.prototype.options = {}; + + Delegator.prototype.element = null; + + function Delegator(element, options) { + this.options = $.extend(true, {}, this.options, options); + this.element = $(element); + this.on = this.subscribe; + this.addEvents(); + } + + Delegator.prototype.addEvents = function() { + var event, functionName, sel, selector, _k, _ref1, _ref2, _results; + + _ref1 = this.events; + _results = []; + for (sel in _ref1) { + functionName = _ref1[sel]; + _ref2 = sel.split(' '), selector = 2 <= _ref2.length ? __slice.call(_ref2, 0, _k = _ref2.length - 1) : (_k = 0, []), event = _ref2[_k++]; + _results.push(this.addEvent(selector.join(' '), event, functionName)); + } + return _results; + }; + + Delegator.prototype.addEvent = function(bindTo, event, functionName) { + var closure, isBlankSelector, + _this = this; + + closure = function() { + return _this[functionName].apply(_this, arguments); + }; + isBlankSelector = typeof bindTo === 'string' && bindTo.replace(/\s+/g, '') === ''; + if (isBlankSelector) { + bindTo = this.element; + } + if (typeof bindTo === 'string') { + this.element.delegate(bindTo, event, closure); + } else { + if (this.isCustomEvent(event)) { + this.subscribe(event, closure); + } else { + $(bindTo).bind(event, closure); + } + } + return this; + }; + + Delegator.prototype.isCustomEvent = function(event) { + event = event.split('.')[0]; + return $.inArray(event, Delegator.natives) === -1; + }; + + Delegator.prototype.publish = function() { + this.element.triggerHandler.apply(this.element, arguments); + return this; + }; + + Delegator.prototype.subscribe = function(event, callback) { + var closure; + + closure = function() { + return callback.apply(this, [].slice.call(arguments, 1)); + }; + closure.guid = callback.guid = ($.guid += 1); + this.element.bind(event, closure); + return this; + }; + + Delegator.prototype.unsubscribe = function() { + this.element.unbind.apply(this.element, arguments); + return this; + }; + + return Delegator; + + })(); + + Delegator.natives = (function() { + var key, specials, val; + + specials = (function() { + var _ref1, _results; + + _ref1 = jQuery.event.special; + _results = []; + for (key in _ref1) { + if (!__hasProp.call(_ref1, key)) continue; + val = _ref1[key]; + _results.push(key); + } + return _results; + })(); + return "blur focus focusin focusout load resize scroll unload click dblclick\nmousedown mouseup mousemove mouseover mouseout mouseenter mouseleave\nchange select submit keydown keypress keyup error".split(/[^a-z]+/).concat(specials); + })(); + + Range = {}; + + Range.sniff = function(r) { + if (r.commonAncestorContainer != null) { + return new Range.BrowserRange(r); + } else if (typeof r.start === "string") { + return new Range.SerializedRange(r); + } else if (r.start && typeof r.start === "object") { + return new Range.NormalizedRange(r); + } else { + console.error(_t("Could not sniff range type")); + return false; + } + }; + + Range.nodeFromXPath = function(xpath, root) { + var customResolver, evaluateXPath, namespace, node, segment; + + if (root == null) { + root = document; + } + evaluateXPath = function(xp, nsResolver) { + var exception; + + if (nsResolver == null) { + nsResolver = null; + } + try { + return document.evaluate('.' + xp, root, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } catch (_error) { + exception = _error; + console.log("XPath evaluation failed."); + console.log("Trying fallback..."); + return $.xpath(xp, root); + } + }; + if (!$.isXMLDoc(document.documentElement)) { + return evaluateXPath(xpath); + } else { + customResolver = document.createNSResolver(document.ownerDocument === null ? document.documentElement : document.ownerDocument.documentElement); + node = evaluateXPath(xpath, customResolver); + if (!node) { + xpath = ((function() { + var _k, _len2, _ref1, _results; + + _ref1 = xpath.split('/'); + _results = []; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + segment = _ref1[_k]; + if (segment && segment.indexOf(':') === -1) { + _results.push(segment.replace(/^([a-z]+)/, 'xhtml:$1')); + } else { + _results.push(segment); + } + } + return _results; + })()).join('/'); + namespace = document.lookupNamespaceURI(null); + customResolver = function(ns) { + if (ns === 'xhtml') { + return namespace; + } else { + return document.documentElement.getAttribute('xmlns:' + ns); + } + }; + node = evaluateXPath(xpath, customResolver); + } + return node; + } + }; + + Range.RangeError = (function(_super) { + __extends(RangeError, _super); + + function RangeError(type, message, parent) { + this.type = type; + this.message = message; + this.parent = parent != null ? parent : null; + RangeError.__super__.constructor.call(this, this.message); + } + + return RangeError; + + })(Error); + + Range.BrowserRange = (function() { + function BrowserRange(obj) { + this.commonAncestorContainer = obj.commonAncestorContainer; + this.startContainer = obj.startContainer; + this.startOffset = obj.startOffset; + this.endContainer = obj.endContainer; + this.endOffset = obj.endOffset; + } + + BrowserRange.prototype.normalize = function(root) { + var it, node, nr, offset, p, r, _k, _len2, _ref1; + + if (this.tainted) { + console.error(_t("You may only call normalize() once on a BrowserRange!")); + return false; + } else { + this.tainted = true; + } + r = {}; + nr = {}; + _ref1 = ['start', 'end']; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + p = _ref1[_k]; + node = this[p + 'Container']; + offset = this[p + 'Offset']; + if (node.nodeType === 1) { + it = node.childNodes[offset]; + node = it || node.childNodes[offset - 1]; + if (node.nodeType === 1 && !node.firstChild) { + it = null; + node = node.previousSibling; + } + while (node.nodeType !== 3) { + node = node.firstChild; + } + offset = it ? 0 : node.nodeValue.length; + } + r[p] = node; + r[p + 'Offset'] = offset; + } + nr.start = r.startOffset > 0 ? r.start.splitText(r.startOffset) : r.start; + if (r.start === r.end) { + if ((r.endOffset - r.startOffset) < nr.start.nodeValue.length) { + nr.start.splitText(r.endOffset - r.startOffset); + } + nr.end = nr.start; + } else { + if (r.endOffset < r.end.nodeValue.length) { + r.end.splitText(r.endOffset); + } + nr.end = r.end; + } + nr.commonAncestor = this.commonAncestorContainer; + while (nr.commonAncestor.nodeType !== 1) { + nr.commonAncestor = nr.commonAncestor.parentNode; + } + return new Range.NormalizedRange(nr); + }; + + BrowserRange.prototype.serialize = function(root, ignoreSelector) { + return this.normalize(root).serialize(root, ignoreSelector); + }; + + return BrowserRange; + + })(); + + Range.NormalizedRange = (function() { + function NormalizedRange(obj) { + this.commonAncestor = obj.commonAncestor; + this.start = obj.start; + this.end = obj.end; + } + + NormalizedRange.prototype.normalize = function(root) { + return this; + }; + + NormalizedRange.prototype.limit = function(bounds) { + var nodes, parent, startParents, _k, _len2, _ref1; + + nodes = $.grep(this.textNodes(), function(node) { + return node.parentNode === bounds || $.contains(bounds, node.parentNode); + }); + if (!nodes.length) { + return null; + } + this.start = nodes[0]; + this.end = nodes[nodes.length - 1]; + startParents = $(this.start).parents(); + _ref1 = $(this.end).parents(); + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + parent = _ref1[_k]; + if (startParents.index(parent) !== -1) { + this.commonAncestor = parent; + break; + } + } + return this; + }; + + NormalizedRange.prototype.serialize = function(root, ignoreSelector) { + var end, serialization, start; + + serialization = function(node, isEnd) { + var n, nodes, offset, origParent, textNodes, xpath, _k, _len2; + + if (ignoreSelector) { + origParent = $(node).parents(":not(" + ignoreSelector + ")").eq(0); + } else { + origParent = $(node).parent(); + } + xpath = origParent.xpath(root)[0]; + textNodes = origParent.textNodes(); + nodes = textNodes.slice(0, textNodes.index(node)); + offset = 0; + for (_k = 0, _len2 = nodes.length; _k < _len2; _k++) { + n = nodes[_k]; + offset += n.nodeValue.length; + } + if (isEnd) { + return [xpath, offset + node.nodeValue.length]; + } else { + return [xpath, offset]; + } + }; + start = serialization(this.start); + end = serialization(this.end, true); + return new Range.SerializedRange({ + start: start[0], + end: end[0], + startOffset: start[1], + endOffset: end[1] + }); + }; + + NormalizedRange.prototype.text = function() { + var node; + + return ((function() { + var _k, _len2, _ref1, _results; + + _ref1 = this.textNodes(); + _results = []; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + node = _ref1[_k]; + _results.push(node.nodeValue); + } + return _results; + }).call(this)).join(''); + }; + + NormalizedRange.prototype.textNodes = function() { + var end, start, textNodes, _ref1; + + textNodes = $(this.commonAncestor).textNodes(); + _ref1 = [textNodes.index(this.start), textNodes.index(this.end)], start = _ref1[0], end = _ref1[1]; + return $.makeArray(textNodes.slice(start, +end + 1 || 9e9)); + }; + + NormalizedRange.prototype.toRange = function() { + var range; + + range = document.createRange(); + range.setStartBefore(this.start); + range.setEndAfter(this.end); + return range; + }; + + return NormalizedRange; + + })(); + + Range.SerializedRange = (function() { + function SerializedRange(obj) { + this.start = obj.start; + this.startOffset = obj.startOffset; + this.end = obj.end; + this.endOffset = obj.endOffset; + } + + SerializedRange.prototype.normalize = function(root) { + var contains, e, length, node, p, range, tn, _k, _l, _len2, _len3, _ref1, _ref2; + + range = {}; + _ref1 = ['start', 'end']; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + p = _ref1[_k]; + try { + node = Range.nodeFromXPath(this[p], root); + } catch (_error) { + e = _error; + throw new Range.RangeError(p, ("Error while finding " + p + " node: " + this[p] + ": ") + e, e); + } + if (!node) { + throw new Range.RangeError(p, "Couldn't find " + p + " node: " + this[p]); + } + length = 0; + _ref2 = $(node).textNodes(); + for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) { + tn = _ref2[_l]; + if (length + tn.nodeValue.length >= this[p + 'Offset']) { + range[p + 'Container'] = tn; + range[p + 'Offset'] = this[p + 'Offset'] - length; + break; + } else { + length += tn.nodeValue.length; + } + } + if (range[p + 'Offset'] == null) { + throw new Range.RangeError("" + p + "offset", "Couldn't find offset " + this[p + 'Offset'] + " in element " + this[p]); + } + } + contains = document.compareDocumentPosition == null ? function(a, b) { + return a.contains(b); + } : function(a, b) { + return a.compareDocumentPosition(b) & 16; + }; + $(range.startContainer).parents().each(function() { + if (contains(this, range.endContainer)) { + range.commonAncestorContainer = this; + return false; + } + }); + return new Range.BrowserRange(range).normalize(root); + }; + + SerializedRange.prototype.serialize = function(root, ignoreSelector) { + return this.normalize(root).serialize(root, ignoreSelector); + }; + + SerializedRange.prototype.toObject = function() { + return { + start: this.start, + startOffset: this.startOffset, + end: this.end, + endOffset: this.endOffset + }; + }; + + return SerializedRange; + + })(); + + util = { + uuid: (function() { + var counter; + + counter = 0; + return function() { + return counter++; + }; + })(), + getGlobal: function() { + return (function() { + return this; + })(); + }, + maxZIndex: function($elements) { + var all, el; + + all = (function() { + var _k, _len2, _results; + + _results = []; + for (_k = 0, _len2 = $elements.length; _k < _len2; _k++) { + el = $elements[_k]; + if ($(el).css('position') === 'static') { + _results.push(-1); + } else { + _results.push(parseInt($(el).css('z-index'), 10) || -1); + } + } + return _results; + })(); + return Math.max.apply(Math, all); + }, + mousePosition: function(e, offsetEl) { + var offset; + + offset = $(offsetEl).position(); + return { + top: e.pageY - offset.top, + left: e.pageX - offset.left + }; + }, + preventEventDefault: function(event) { + return event != null ? typeof event.preventDefault === "function" ? event.preventDefault() : void 0 : void 0; + } + }; + + _Annotator = this.Annotator; + + Annotator = (function(_super) { + __extends(Annotator, _super); + + Annotator.prototype.events = { + ".annotator-adder button click": "onAdderClick", + ".annotator-adder button mousedown": "onAdderMousedown", + ".annotator-hl mouseover": "onHighlightMouseover", + ".annotator-hl mouseout": "startViewerHideTimer" + }; + + Annotator.prototype.html = { + adder: '
', + wrapper: '' + }; + + Annotator.prototype.options = { + readOnly: false + }; + + Annotator.prototype.plugins = {}; + + Annotator.prototype.editor = null; + + Annotator.prototype.viewer = null; + + Annotator.prototype.selectedRanges = null; + + Annotator.prototype.mouseIsDown = false; + + Annotator.prototype.ignoreMouseup = false; + + Annotator.prototype.viewerHideTimer = null; + + function Annotator(element, options) { + this.onDeleteAnnotation = __bind(this.onDeleteAnnotation, this); + this.onEditAnnotation = __bind(this.onEditAnnotation, this); + this.onAdderClick = __bind(this.onAdderClick, this); + this.onAdderMousedown = __bind(this.onAdderMousedown, this); + this.onHighlightMouseover = __bind(this.onHighlightMouseover, this); + this.checkForEndSelection = __bind(this.checkForEndSelection, this); + this.checkForStartSelection = __bind(this.checkForStartSelection, this); + this.clearViewerHideTimer = __bind(this.clearViewerHideTimer, this); + this.startViewerHideTimer = __bind(this.startViewerHideTimer, this); + this.showViewer = __bind(this.showViewer, this); + this.onEditorSubmit = __bind(this.onEditorSubmit, this); + this.onEditorHide = __bind(this.onEditorHide, this); + this.showEditor = __bind(this.showEditor, this); Annotator.__super__.constructor.apply(this, arguments); + this.plugins = {}; + if (!Annotator.supported()) { + return this; + } + if (!this.options.readOnly) { + this._setupDocumentEvents(); + } + this._setupWrapper()._setupViewer()._setupEditor(); + this._setupDynamicStyle(); + this.adder = $(this.html.adder).appendTo(this.wrapper).hide(); + } + + Annotator.prototype._setupWrapper = function() { + this.wrapper = $(this.html.wrapper); + this.element.find('script').remove(); + this.element.wrapInner(this.wrapper); + this.wrapper = this.element.find('.annotator-wrapper'); + return this; + }; + + Annotator.prototype._setupViewer = function() { + var _this = this; + + this.viewer = new Annotator.Viewer({ + readOnly: this.options.readOnly + }); + this.viewer.hide().on("edit", this.onEditAnnotation).on("delete", this.onDeleteAnnotation).addField({ + load: function(field, annotation) { + if (annotation.text) { + $(field).escape(annotation.text); + } else { + $(field).html("" + (_t('No Comment')) + ""); + } + return _this.publish('annotationViewerTextField', [field, annotation]); + } + }).element.appendTo(this.wrapper).bind({ + "mouseover": this.clearViewerHideTimer, + "mouseout": this.startViewerHideTimer + }); + return this; + }; + + Annotator.prototype._setupEditor = function() { + this.editor = new Annotator.Editor(); + this.editor.hide().on('hide', this.onEditorHide).on('save', this.onEditorSubmit).addField({ + type: 'textarea', + label: _t('Comments') + '\u2026', + load: function(field, annotation) { + return $(field).find('textarea').val(annotation.text || ''); + }, + submit: function(field, annotation) { + return annotation.text = $(field).find('textarea').val(); + } + }); + this.editor.element.appendTo(this.wrapper); + return this; + }; + + Annotator.prototype._setupDocumentEvents = function() { + $(document).bind({ + "mouseup": this.checkForEndSelection, + "mousedown": this.checkForStartSelection + }); + return this; + }; + + Annotator.prototype._setupDynamicStyle = function() { + var max, sel, style, x; + + style = $('#annotator-dynamic-style'); + if (!style.length) { + style = $('').appendTo(document.head); + } + sel = '*' + ((function() { + var _k, _len2, _ref1, _results; + + _ref1 = ['adder', 'outer', 'notice', 'filter']; + _results = []; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + x = _ref1[_k]; + _results.push(":not(.annotator-" + x + ")"); + } + return _results; + })()).join(''); + max = util.maxZIndex($(document.body).find(sel)); + max = Math.max(max, 1000); + style.text([".annotator-adder, .annotator-outer, .annotator-notice {", " z-index: " + (max + 20) + ";", "}", ".annotator-filter {", " z-index: " + (max + 10) + ";", "}"].join("\n")); + return this; + }; + + Annotator.prototype.getSelectedRanges = function() { + var browserRange, i, normedRange, r, ranges, rangesToIgnore, selection, _k, _len2; + + selection = util.getGlobal().getSelection(); + ranges = []; + rangesToIgnore = []; + if (!selection.isCollapsed) { + ranges = (function() { + var _k, _ref1, _results; + + _results = []; + for (i = _k = 0, _ref1 = selection.rangeCount; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) { + r = selection.getRangeAt(i); + browserRange = new Range.BrowserRange(r); + normedRange = browserRange.normalize().limit(this.wrapper[0]); + if (normedRange === null) { + rangesToIgnore.push(r); + } + _results.push(normedRange); + } + return _results; + }).call(this); + selection.removeAllRanges(); + } + for (_k = 0, _len2 = rangesToIgnore.length; _k < _len2; _k++) { + r = rangesToIgnore[_k]; + selection.addRange(r); + } + return $.grep(ranges, function(range) { + if (range) { + selection.addRange(range.toRange()); + } + return range; + }); + }; + + Annotator.prototype.createAnnotation = function() { + var annotation; + + annotation = {}; + this.publish('beforeAnnotationCreated', [annotation]); + return annotation; + }; + + Annotator.prototype.setupAnnotation = function(annotation) { + var e, normed, normedRanges, r, root, _k, _l, _len2, _len3, _ref1; + + root = this.wrapper[0]; + annotation.ranges || (annotation.ranges = this.selectedRanges); + normedRanges = []; + _ref1 = annotation.ranges; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + r = _ref1[_k]; + try { + normedRanges.push(Range.sniff(r).normalize(root)); + } catch (_error) { + e = _error; + if (e instanceof Range.RangeError) { + this.publish('rangeNormalizeFail', [annotation, r, e]); + } else { + throw e; + } + } + } + annotation.quote = []; + annotation.ranges = []; + annotation.highlights = []; + for (_l = 0, _len3 = normedRanges.length; _l < _len3; _l++) { + normed = normedRanges[_l]; + annotation.quote.push($.trim(normed.text())); + annotation.ranges.push(normed.serialize(this.wrapper[0], '.annotator-hl')); + $.merge(annotation.highlights, this.highlightRange(normed)); + } + annotation.quote = annotation.quote.join(' / '); + $(annotation.highlights).data('annotation', annotation); + return annotation; + }; + + Annotator.prototype.updateAnnotation = function(annotation) { + this.publish('beforeAnnotationUpdated', [annotation]); + this.publish('annotationUpdated', [annotation]); + return annotation; + }; + + Annotator.prototype.deleteAnnotation = function(annotation) { + var child, h, _k, _len2, _ref1; + + if (annotation.highlights != null) { + _ref1 = annotation.highlights; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + h = _ref1[_k]; + if (!(h.parentNode != null)) { + continue; + } + child = h.childNodes[0]; + $(h).replaceWith(h.childNodes); + } + } + this.publish('annotationDeleted', [annotation]); + return annotation; + }; + + Annotator.prototype.loadAnnotations = function(annotations) { + var clone, loader, + _this = this; + + if (annotations == null) { + annotations = []; + } + loader = function(annList) { + var n, now, _k, _len2; + + if (annList == null) { + annList = []; + } + now = annList.splice(0, 10); + for (_k = 0, _len2 = now.length; _k < _len2; _k++) { + n = now[_k]; + _this.setupAnnotation(n); + } + if (annList.length > 0) { + return setTimeout((function() { + return loader(annList); + }), 10); + } else { + return _this.publish('annotationsLoaded', [clone]); + } + }; + clone = annotations.slice(); + if (annotations.length) { + loader(annotations); + } + return this; + }; + + Annotator.prototype.dumpAnnotations = function() { + if (this.plugins['Store']) { + return this.plugins['Store'].dumpAnnotations(); + } else { + console.warn(_t("Can't dump annotations without Store plugin.")); + return false; + } + }; + + Annotator.prototype.highlightRange = function(normedRange, cssClass) { + var hl, node, white, _k, _len2, _ref1, _results; + + if (cssClass == null) { + cssClass = 'annotator-hl'; + } + white = /^\s*$/; + hl = $(""); + _ref1 = normedRange.textNodes(); + _results = []; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + node = _ref1[_k]; + if (!white.test(node.nodeValue)) { + _results.push($(node).wrapAll(hl).parent().show()[0]); + } + } + return _results; + }; + + Annotator.prototype.highlightRanges = function(normedRanges, cssClass) { + var highlights, r, _k, _len2; + + if (cssClass == null) { + cssClass = 'annotator-hl'; + } + highlights = []; + for (_k = 0, _len2 = normedRanges.length; _k < _len2; _k++) { + r = normedRanges[_k]; + $.merge(highlights, this.highlightRange(r, cssClass)); + } + return highlights; + }; + + Annotator.prototype.addPlugin = function(name, options) { + var klass, _base; + + if (this.plugins[name]) { + console.error(_t("You cannot have more than one instance of any plugin.")); + } else { + klass = Annotator.Plugin[name]; + if (typeof klass === 'function') { + this.plugins[name] = new klass(this.element[0], options); + this.plugins[name].annotator = this; + if (typeof (_base = this.plugins[name]).pluginInit === "function") { + _base.pluginInit(); + } + } else { + console.error(_t("Could not load ") + name + _t(" plugin. Have you included the appropriate +%block> + +<%include file="/courseware/course_navigation.html" args="active_page='notes'" /> + +${note.quote|h}+
You do not have any notes.
+ % endif +