diff --git a/setup.py b/setup.py index b67eb52..cb91302 100755 --- a/setup.py +++ b/setup.py @@ -194,6 +194,7 @@ def package_data(pkg, roots, sub_roots): entry_points={ "xblock.v1": [ # _xblock suffix is added for testing only. + "annotatable_xblock = xblocks_contrib:AnnotatableXBlock", "poll_xblock = xblocks_contrib:PollXBlock", ] }, diff --git a/tests/test_annotatable.py b/tests/test_annotatable.py new file mode 100644 index 0000000..e69de29 diff --git a/xblocks_contrib/__init__.py b/xblocks_contrib/__init__.py index de30a8e..9115c00 100644 --- a/xblocks_contrib/__init__.py +++ b/xblocks_contrib/__init__.py @@ -2,6 +2,7 @@ Init for the xblocks_contrib package. """ +from .annotatable import AnnotatableXBlock from .poll import PollXBlock __version__ = "0.1.0" diff --git a/xblocks_contrib/annotatable/.tx/config b/xblocks_contrib/annotatable/.tx/config new file mode 100644 index 0000000..5f1f3f6 --- /dev/null +++ b/xblocks_contrib/annotatable/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[o:open-edx:p:p:xblocks:r:annotatable] +file_filter = annotatable/translations//LC_MESSAGES/text.po +source_file = annotatable/translations/en/LC_MESSAGES/text.po +source_lang = en +type = PO diff --git a/xblocks_contrib/annotatable/__init__.py b/xblocks_contrib/annotatable/__init__.py new file mode 100644 index 0000000..3c64eef --- /dev/null +++ b/xblocks_contrib/annotatable/__init__.py @@ -0,0 +1,5 @@ +""" +Init for the AnnotatableXBlock package. +""" + +from .annotatable import AnnotatableXBlock diff --git a/xblocks_contrib/annotatable/annotatable.py b/xblocks_contrib/annotatable/annotatable.py new file mode 100644 index 0000000..b792892 --- /dev/null +++ b/xblocks_contrib/annotatable/annotatable.py @@ -0,0 +1,207 @@ +import textwrap +from importlib.resources import files + +import markupsafe +from lxml import etree +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.fields import Scope, String +from xblock.utils.resources import ResourceLoader +from xblock.utils.studio_editable import StudioEditableXBlockMixin + +resource_loader = ResourceLoader(__name__) + + +def _(text): + """Make '_' a no-op, so we can scrape strings""" + return text + + +class AnnotatableXBlock(StudioEditableXBlockMixin, XBlock): + """ + Annotatable XBlock. + """ + + display_name = String( + display_name=_("Display Name"), + help=_("The display name for this component."), + scope=Scope.settings, + default=_("Annotation"), + ) + + data = String( + help=_("XML data for the annotation"), + scope=Scope.content, + default=textwrap.dedent( + markupsafe.Markup( + """ + + +

Enter your (optional) instructions for the exercise in HTML format.

+

Annotations are specified by an {}annotation{} tag which may may have the following attributes:

+
    +
  • title (optional). Title of the annotation. Defaults to Commentary if omitted.
  • +
  • body (required). Text of the annotation.
  • +
  • problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".
  • +
  • highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.
  • +
+
+

Add your HTML with annotation spans here.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sodales laoreet est, egestas gravida felis egestas nec. Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.

+

Nulla facilisi. Pellentesque id vestibulum libero. Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.

+
+ """ + ).format(markupsafe.escape("<"), markupsafe.escape(">")) + ), + ) + + instructions_html = String( + help=_("Instructions HTML"), + scope=Scope.user_state, + default="", + enforce_type=True + ) + + content_html = String( + help=_("Content HTML"), + scope=Scope.user_state, + default="", + enforce_type=True + ) + + editable_fields = ["display_name"] + HIGHLIGHT_COLORS = ["yellow", "orange", "purple", "blue", "green"] + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + return files(__package__).joinpath(path).read_text() + + def _get_annotation_class_attr(self, index, el): + """Returns a dict with the CSS class attribute to set on the annotation + and an XML key to delete from the element. + """ + + attr = {} + cls = ["annotatable-span", "highlight"] + highlight_key = "highlight" + color = el.get(highlight_key) + + if color is not None: + if color in self.HIGHLIGHT_COLORS: + cls.append("highlight-" + color) + attr["_delete"] = highlight_key + attr["value"] = " ".join(cls) + + return {"class": attr} + + def _get_annotation_data_attr(self, index, el): + """Returns a dict in which the keys are the HTML data attributes + to set on the annotation element. Each data attribute has a + corresponding 'value' and (optional) '_delete' key to specify + an XML attribute to delete. + """ + + data_attrs = {} + attrs_map = { + "body": "data-comment-body", + "title": "data-comment-title", + "problem": "data-problem-id", + } + + for xml_key, html_key in attrs_map.items(): + if xml_key in el.attrib: + value = el.get(xml_key, "") + data_attrs[html_key] = {"value": value, "_delete": xml_key} + + return data_attrs + + def _render_annotation(self, index, el): + """Renders an annotation element for HTML output.""" + attr = {} + attr.update(self._get_annotation_class_attr(index, el)) + attr.update(self._get_annotation_data_attr(index, el)) + + el.tag = "span" + + for key, value_dict in attr.items(): + el.set(key, value_dict["value"]) + if "_delete" in value_dict and value_dict["_delete"] is not None: + delete_key = value_dict["_delete"] + del el.attrib[delete_key] + + def _render_content(self): + """Renders annotatable content with annotation spans and returns HTML.""" + + xmltree = etree.fromstring(self.data) + content = etree.tostring(xmltree, encoding="unicode") + + xmltree = etree.fromstring(content) + xmltree.tag = "div" + if "display_name" in xmltree.attrib: + del xmltree.attrib["display_name"] + + index = 0 + for el in xmltree.findall(".//annotation"): + self._render_annotation(index, el) + index += 1 + + return etree.tostring(xmltree, encoding="unicode") + + def _extract_instructions(self, xmltree): + """Removes from the xmltree and returns them as a string, otherwise None.""" + instructions = xmltree.find("instructions") + if instructions is not None: + instructions.tag = "div" + xmltree.remove(instructions) + return etree.tostring(instructions, encoding="unicode") + return None + + def student_view(self, context=None): + """ + Create primary view of the AnnotatableXBlock, shown to students when viewing courses. + """ + if context: + pass # TO-DO: do something based on the context. + xmltree = etree.fromstring(self.data) + self.instructions_html = self._extract_instructions(xmltree) + self.content_html = self._render_content() + + html = self.resource_string("static/html/annotatable.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/annotatable.css")) + + frag.add_javascript(self.resource_string("static/js/src/annotatable.js")) + frag.initialize_js("AnnotatableXBlock") + return frag + + def studio_view(self, _context=None): + """ + Return the studio view. + """ + html = self.resource_string("static/html/annotatable_editor.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/annotatable_editor.css")) + + frag.add_javascript(self.resource_string("static/js/src/annotatable_editor.js")) + frag.initialize_js("XMLEditor") + return frag + + @staticmethod + def workbench_scenarios(): + """Create canned scenario for display in the workbench.""" + return [ + ( + "AnnotatableXBlock", + """ + """, + ), + ( + "Multiple AnnotatableXBlock", + """ + + + + + """, + ), + ] diff --git a/xblocks_contrib/annotatable/conf/locale/__init__.py b/xblocks_contrib/annotatable/conf/locale/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xblocks_contrib/annotatable/conf/locale/config.yaml b/xblocks_contrib/annotatable/conf/locale/config.yaml new file mode 100644 index 0000000..4bfb0ca --- /dev/null +++ b/xblocks_contrib/annotatable/conf/locale/config.yaml @@ -0,0 +1,93 @@ +# Configuration for i18n workflow. + +locales: + - en # English - Source Language + # - am # Amharic + - ar # Arabic + # - az # Azerbaijani + # - bg_BG # Bulgarian (Bulgaria) + # - bn_BD # Bengali (Bangladesh) + # - bn_IN # Bengali (India) + # - bs # Bosnian + # - ca # Catalan + # - ca@valencia # Catalan (Valencia) + # - cs # Czech + # - cy # Welsh + # - da # Danish + # - de_DE # German (Germany) + # - el # Greek + # - en_GB # English (United Kingdom) + # # Don't pull these until we figure out why pages randomly display in these locales, + # # when the user's browser is in English and the user is not logged in. + # #- en@lolcat # LOLCAT English + # #- en@pirate # Pirate English + - es_419 # Spanish (Latin America) + # - es_AR # Spanish (Argentina) + # - es_EC # Spanish (Ecuador) + # - es_ES # Spanish (Spain) + # - es_MX # Spanish (Mexico) + # - es_PE # Spanish (Peru) + # - et_EE # Estonian (Estonia) + # - eu_ES # Basque (Spain) + # - fa # Persian + # - fa_IR # Persian (Iran) + # - fi_FI # Finnish (Finland) + # - fil # Filipino + - fr # French + # - gl # Galician + # - gu # Gujarati + - he # Hebrew + - hi # Hindi + # - hr # Croatian + # - hu # Hungarian + # - hy_AM # Armenian (Armenia) + # - id # Indonesian + # - it_IT # Italian (Italy) + # - ja_JP # Japanese (Japan) + # - kk_KZ # Kazakh (Kazakhstan) + # - km_KH # Khmer (Cambodia) + # - kn # Kannada + - ko_KR # Korean (Korea) + # - lt_LT # Lithuanian (Lithuania) + # - ml # Malayalam + # - mn # Mongolian + # - mr # Marathi + # - ms # Malay + # - nb # Norwegian Bokmål + # - ne # Nepali + # - nl_NL # Dutch (Netherlands) + # - or # Oriya + # - pl # Polish + - pt_BR # Portuguese (Brazil) + # - pt_PT # Portuguese (Portugal) + # - ro # Romanian + - ru # Russian + # - si # Sinhala + # - sk # Slovak + # - sl # Slovenian + # - sq # Albanian + # - sr # Serbian + # - sv # Swedish + # - sw # Swahili + # - ta # Tamil + # - te # Telugu + # - th # Thai + # - tr_TR # Turkish (Turkey) + # - uk # Ukranian + # - ur # Urdu + # - uz # Uzbek + # - vi # Vietnamese + - zh_CN # Chinese (China) + # - zh_HK # Chinese (Hong Kong) + # - zh_TW # Chinese (Taiwan) + + +# The locales used for fake-accented English, for testing. +dummy_locales: + - eo + - rtl # Fake testing language for Arabic + +# Directories we don't search for strings. +ignore_dirs: + - '*/css' + - 'public/js/translations' diff --git a/xblocks_contrib/annotatable/static/README.txt b/xblocks_contrib/annotatable/static/README.txt new file mode 100644 index 0000000..0472ef6 --- /dev/null +++ b/xblocks_contrib/annotatable/static/README.txt @@ -0,0 +1,19 @@ +This static directory is for files that should be included in your kit as plain +static files. + +You can ask the runtime for a URL that will retrieve these files with: + + url = self.runtime.local_resource_url(self, "static/js/lib.js") + +The default implementation is very strict though, and will not serve files from +the static directory. It will serve files from a directory named "public". +Create a directory alongside this one named "public", and put files there. +Then you can get a url with code like this: + + url = self.runtime.local_resource_url(self, "public/js/lib.js") + +The sample code includes a function you can use to read the content of files +in the static directory, like this: + + frag.add_javascript(self.resource_string("static/js/my_block.js")) + diff --git a/xblocks_contrib/annotatable/static/css/annotatable.css b/xblocks_contrib/annotatable/static/css/annotatable.css new file mode 100644 index 0000000..c60bfb4 --- /dev/null +++ b/xblocks_contrib/annotatable/static/css/annotatable.css @@ -0,0 +1,245 @@ +/* CSS for AnnotatableXBlock */ + +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700"); +/* line 1, /openedx/edx-platform/xmodule/assets/AnnotatableBlockDisplay.scss */ +.annotatable_xblock { + /* TODO: move top-level variables to a common _variables.scss. + * NOTE: These variables were only added here because when this was integrated with the CMS, + * SASS compilation errors were triggered because the CMS didn't have the same variables defined + * that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS. + * -Abarrett and Vshnayder + */ + /* stylelint-disable-line */ + /* stylelint-disable-line */ +} +/* line 15, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-wrapper { + position: relative; +} +/* line 19, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-header { + margin-bottom: 0.5em; +} +/* line 23, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section { + position: relative; + padding: 0.5em 1em; + border: 1px solid #c8c8c8; + border-radius: 0.5em; + margin-bottom: 0.5em; +} +/* line 30, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section.shaded { + background-color: #ededed; +} +/* line 32, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section .annotatable-section-title { + font-weight: bold; +} +/* line 34, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section .annotatable-section-title a { + font-weight: normal; +} +/* line 37, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section .annotatable-section-body { + border-top: 1px solid #c8c8c8; + margin-top: 0.5em; + padding-top: 0.5em; +} +/* line 15, /openedx/edx-platform/common/static/sass/bourbon/addons/_clearfix.scss */ +.annotatable_xblock .annotatable-section .annotatable-section-body:after { + content: ""; + display: table; + clear: both; +} +/* line 45, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section ul.instructions-template { + list-style: disc; + margin-left: 4em; +} +/* line 48, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section ul.instructions-template b { + font-weight: bold; +} +/* line 49, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section ul.instructions-template i { + font-style: italic; +} +/* line 51, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-section ul.instructions-template code { + display: inline; + white-space: pre; + font-family: Courier New, monospace; +} +/* line 59, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-toggle { + position: absolute; + right: 0; + margin: 2px 1em 2px 0; +} +/* line 63, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-toggle.expanded::after { + content: " \2191"; +} +/* line 64, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-toggle.collapsed::after { + content: " \2193"; +} +/* line 67, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span { + display: inline; +} +/* line 88, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight { + background-color: rgba(255, 255, 10, 0.3); +} +/* line 90, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight.selected { + background-color: rgba(255, 255, 10, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-yellow { + background-color: rgba(255, 255, 10, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-yellow.selected { + background-color: rgba(255, 255, 10, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-red { + background-color: rgba(178, 19, 16, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-red.selected { + background-color: rgba(178, 19, 16, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-orange { + background-color: rgba(255, 165, 0, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-orange.selected { + background-color: rgba(255, 165, 0, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-green { + background-color: rgba(25, 255, 132, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-green.selected { + background-color: rgba(25, 255, 132, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-blue { + background-color: rgba(35, 163, 255, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-blue.selected { + background-color: rgba(35, 163, 255, 0.9); +} +/* line 94, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-purple { + background-color: rgba(115, 9, 178, 0.3); +} +/* line 96, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.highlight-purple.selected { + background-color: rgba(115, 9, 178, 0.9); +} +/* line 100, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.hide { + cursor: none; + background-color: inherit; +} +/* line 104, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span.hide .annotatable-icon { + display: none; +} +/* line 109, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .annotatable-span .annotatable-comment { + display: none; +} +/* line 114, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip { + font-size: 0.875em; + border: 1px solid #333; + border-radius: 1em; + background-color: rgba(0, 0, 0, 0.85); + color: #fff; + -webkit-font-smoothing: antialiased; +} +/* line 122, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar { + font-size: 1em; + color: inherit; + background-color: transparent; + padding: 5px 10px; + border: none; +} +/* line 129, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-title { + padding: 5px 0; + border-bottom: 2px solid #333; + font-weight: bold; +} +/* line 135, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-icon { + right: 10px; + background: #333; +} +/* line 140, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-state-hover { + color: inherit; + border: 1px solid #c8c8c8; +} +/* line 146, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-content { + color: inherit; + font-size: 0.875em; + text-align: left; + font-weight: 400; + padding: 0 10px 10px 10px; + background-color: transparent; + border-color: transparent; +} +/* line 156, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip p { + color: inherit; + line-height: normal; +} +/* line 162, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip-annotatable { + max-width: 375px; +} +/* line 165, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content { + padding: 0 10px; +} +/* line 168, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-comment { + display: block; + margin: 0 0 10px 0; + max-height: 225px; + overflow: auto; + line-height: normal; +} +/* line 176, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-reply { + display: block; + border-top: 2px solid #333; + padding: 5px 0; + margin: 0; + text-align: center; +} +/* line 185, /openedx/edx-platform/xmodule/assets/annotatable/_display.scss */ +.annotatable_xblock .ui-tooltip.qtip.ui-tooltip-annotatable::after { + content: ""; + display: inline-block; + position: absolute; + bottom: -20px; + left: 50%; + height: 0; + width: 0; + margin-left: -5px; + border: 10px solid transparent; + border-top-color: rgba(0, 0, 0, 0.85); +} diff --git a/xblocks_contrib/annotatable/static/css/annotatable_editor.css b/xblocks_contrib/annotatable/static/css/annotatable_editor.css new file mode 100644 index 0000000..2eb2327 --- /dev/null +++ b/xblocks_contrib/annotatable/static/css/annotatable_editor.css @@ -0,0 +1,26 @@ +/* line 564, /openedx/edx-platform/cms/static/sass/elements/_xblocks.scss */ +.wrapper-comp-editor { + display: block; +} +/* line 569, /openedx/edx-platform/cms/static/sass/elements/_xblocks.scss */ +.wrapper-comp-editor.is-inactive { + display: none; +} + +.wrapper-comp-editor.latex-problem { + margin-top: 50px; +} + +/* line 967, /openedx/edx-platform/cms/static/sass/elements/_xblocks.scss */ +div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler { + display: none; +} + +/* line 82, /openedx/edx-platform/cms/static/sass/_shame.scss */ +.wrapper-comp-editor.latex-problem { + margin-top: 50px; +} + +div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler { + display: none; +} diff --git a/xblocks_contrib/annotatable/static/html/annotatable.html b/xblocks_contrib/annotatable/static/html/annotatable.html new file mode 100644 index 0000000..e4f5e41 --- /dev/null +++ b/xblocks_contrib/annotatable/static/html/annotatable.html @@ -0,0 +1,27 @@ +
+
+
+

{self.display_name}

+
+ +
+
+ Instructions + Collapse Instructions +
+
{self.instructions_html}
+
+ +
+
+ Guided Discussion + Hide Annotations +
+
{self.content_html}
+
+
+
diff --git a/xblocks_contrib/annotatable/static/html/annotatable_editor.html b/xblocks_contrib/annotatable/static/html/annotatable_editor.html new file mode 100644 index 0000000..18408ff --- /dev/null +++ b/xblocks_contrib/annotatable/static/html/annotatable_editor.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/xblocks_contrib/annotatable/static/js/src/annotatable.js b/xblocks_contrib/annotatable/static/js/src/annotatable.js new file mode 100644 index 0000000..ca1dd39 --- /dev/null +++ b/xblocks_contrib/annotatable/static/js/src/annotatable.js @@ -0,0 +1,304 @@ +function AnnotatableXBlock(runtime, element, data) { + class Annotatable { + constructor(el) { + this.onMoveTip = this.onMoveTip.bind(this); + this.onShowTip = this.onShowTip.bind(this); + this.onClickReturn = this.onClickReturn.bind(this); + this.onClickReply = this.onClickReply.bind(this); + this.onClickToggleInstructions = this.onClickToggleInstructions.bind(this); + this.onClickToggleAnnotations = this.onClickToggleAnnotations.bind(this); + + if (this._debug) { + console.log('loaded Annotatable'); + } + + this.el = el; + this.$el = $(el); + this.init(); + } + + _debug = false; + wrapperSelector = '.annotatable-wrapper'; + toggleAnnotationsSelector = '.annotatable-toggle-annotations'; + toggleInstructionsSelector = '.annotatable-toggle-instructions'; + instructionsSelector = '.annotatable-instructions'; + sectionSelector = '.annotatable-section'; + spanSelector = '.annotatable-span'; + replySelector = '.annotatable-reply'; + problemSelector = 'div.problem'; + problemInputSelector = 'div.problem .annotation-input'; + problemReturnSelector = 'div.problem .annotation-return'; + + $(selector) { + return $(selector, this.el); + } + + init() { + this.initEvents(); + this.initTips(); + } + + initEvents() { + const ref = [false, false]; + [this.annotationsHidden, this.instructionsHidden] = ref; + this.$(this.toggleAnnotationsSelector).on('click', this.onClickToggleAnnotations); + this.$(this.toggleInstructionsSelector).on('click', this.onClickToggleInstructions); + this.$el.on('click', this.replySelector, this.onClickReply); + $(document).on('click', this.problemReturnSelector, this.onClickReturn); + } + + initTips() { + this.$(this.spanSelector).each((index, el) => { + $(el).qtip(this.getSpanTipOptions(el)); + }); + } + + getSpanTipOptions(el) { + return { + content: { + title: { + text: this.makeTipTitle(el), + }, + text: this.makeTipContent(el), + }, + position: { + my: 'bottom center', + at: 'top center', + target: $(el), + container: this.$(this.wrapperSelector), + adjust: { + y: -5, + }, + }, + show: { + event: 'click mouseenter', + solo: true, + }, + hide: { + event: 'click mouseleave', + delay: 500, + fixed: true, + }, + style: { + classes: 'ui-tooltip-annotatable', + }, + events: { + show: this.onShowTip, + move: this.onMoveTip, + }, + }; + } + + onClickToggleAnnotations() { + this.toggleAnnotations(); + } + + onClickToggleInstructions() { + this.toggleInstructions(); + } + + onClickReply(e) { + this.replyTo(e.currentTarget); + } + + onClickReturn(e) { + this.returnFrom(e.currentTarget); + } + + onShowTip(event) { + if (this.annotationsHidden) { + event.preventDefault(); + } + } + + onMoveTip(event, api, position) { + const tip = api.elements.tooltip; + const adjustY = api.options.position?.adjust?.y || 0; + const container = api.options.position?.container || $('body'); + const target = api.elements.target; + const rects = $(target).get(0).getClientRects(); + const isNonOverlapping = rects?.length === 2 && rects[0].left > rects[1].right; + const focusRect = isNonOverlapping ? (rects[0].width > rects[1].width ? rects[0] : rects[1]) : rects[0]; + const rectCenter = focusRect.left + focusRect.width / 2; + const rectTop = focusRect.top; + const tipWidth = $(tip).width(); + const tipHeight = $(tip).height(); + const containerOffset = $(container).offset(); + const offsetLeft = -containerOffset.left; + const offsetTop = $(document).scrollTop() - containerOffset.top; + let tipLeft = offsetLeft + rectCenter - tipWidth / 2; + let tipTop = offsetTop + rectTop - tipHeight + adjustY; + const winWidth = $(window).width(); + + if (tipLeft < offsetLeft) { + tipLeft = offsetLeft; + } else if (tipLeft + tipWidth > winWidth + offsetLeft) { + tipLeft = winWidth + offsetLeft - tipWidth; + } + + $.extend(position, { + left: tipLeft, + top: tipTop, + }); + } + + getSpanForProblemReturn(el) { + const problemId = $(this.problemReturnSelector).index(el); + return this.$(this.spanSelector).filter(`[data-problem-id='${problemId}']`); + } + + getProblem(el) { + const problemId = this.getProblemId(el); + return $(this.problemInputSelector).eq(problemId); + } + + getProblemId(el) { + return $(el).data('problem-id'); + } + + toggleAnnotations() { + const hide = (this.annotationsHidden = !this.annotationsHidden); + this.toggleAnnotationButtonText(hide); + this.toggleSpans(hide); + this.toggleTips(hide); + } + + toggleTips(hide) { + const visible = this.findVisibleTips(); + this.hideTips(visible); + } + + toggleAnnotationButtonText(hide) { + const buttonText = hide ? gettext('Show Annotations') : gettext('Hide Annotations'); + this.$(this.toggleAnnotationsSelector).text(buttonText); + } + + toggleInstructions() { + const hide = (this.instructionsHidden = !this.instructionsHidden); + this.toggleInstructionsButton(hide); + this.toggleInstructionsText(hide); + } + + toggleInstructionsButton(hide) { + const txt = hide ? gettext('Expand Instructions') : gettext('Collapse Instructions'); + const cls = hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']; + this.$(this.toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1]); + } + + toggleInstructionsText(hide) { + const slideMethod = hide ? 'slideUp' : 'slideDown'; + this.$(this.instructionsSelector)[slideMethod](); + } + + toggleSpans(hide) { + this.$(this.spanSelector).toggleClass('hide', hide, 250); + } + + replyTo(buttonEl) { + const offset = -20; + const el = this.getProblem(buttonEl); + + if (el.length > 0) { + this.scrollTo(el, this.afterScrollToProblem, offset); + } else if (this._debug) { + console.log('problem not found. event: ', e); + } + } + + returnFrom(buttonEl) { + const offset = -200; + const el = this.getSpanForProblemReturn(buttonEl); + + if (el.length > 0) { + this.scrollTo(el, this.afterScrollToSpan, offset); + } else if (this._debug) { + console.log('span not found. event:', e); + } + } + + scrollTo(el, after, offset = -20) { + if ($(el).length > 0) { + $('html, body').scrollTo(el, { + duration: 500, + onAfter: () => { + if (after) { + after.call(this, el); + } + }, + offset, + }); + } + } + + afterScrollToProblem(problemEl) { + problemEl.effect('highlight', {}, 500); + } + + afterScrollToSpan(spanEl) { + spanEl.addClass('selected', 400, 'swing', () => { + spanEl.removeClass('selected', 400, 'swing'); + }); + } + + makeTipContent(el) { + return () => { + const text = $(el).data('comment-body'); + const comment = this.createComment(text); + const problemId = this.getProblemId(el); + const reply = this.createReplyLink(problemId); + return $(comment).add(reply); + }; + } + + makeTipTitle(el) { + return () => { + const title = $(el).data('comment-title'); + return title || gettext('Commentary'); + }; + } + + createComment(text) { + return $('
' + text + '
'); // xss-lint: disable=javascript-concat-html + } + + createReplyLink(problemId) { + const linkTxt = gettext('Reply to Annotation'); + return $( + '' + + linkTxt + + "" + ); // xss-lint: disable=javascript-concat-html + } + + findVisibleTips() { + const visible = []; + this.$(this.spanSelector).each((index, el) => { + const api = $(el).qtip('api'); + const tip = $(api?.elements.tooltip); + + if (tip.is(':visible')) { + visible.push(el); + } + }); + return visible; + } + + hideTips(elements) { + $(elements).qtip('hide'); + } + + _once(fn) { + let done = false; + return () => { + if (!done) { + fn.call(this); + done = true; + } + }; + } + } + + new Annotatable(element); +} \ No newline at end of file diff --git a/xblocks_contrib/annotatable/static/js/src/annotatable_editor.js b/xblocks_contrib/annotatable/static/js/src/annotatable_editor.js new file mode 100644 index 0000000..f231e02 --- /dev/null +++ b/xblocks_contrib/annotatable/static/js/src/annotatable_editor.js @@ -0,0 +1,26 @@ +function XMLEditor(element) { + class XMLEditingDescriptor { + constructor(element) { + this.element = element; + this.editBox = this.createEditBox(); + } + + createEditBox() { + const textArea = document.querySelector('.edit-box', this.element); + const editor = CodeMirror.fromTextArea(textArea, { + mode: 'xml', + lineNumbers: true, + lineWrapping: true, + }); + return editor; + } + + save() { + return { + data: this.editBox.getValue(), + }; + } + } + + return new XMLEditingDescriptor(element); +} diff --git a/xblocks_contrib/annotatable/translation b/xblocks_contrib/annotatable/translation new file mode 120000 index 0000000..717deaa --- /dev/null +++ b/xblocks_contrib/annotatable/translation @@ -0,0 +1 @@ +/Users/irtaza.akram/openedx/edx-platform/src/xblocks-contrib/xblocks-contrib copy/xblocks-contrib/conf/locale \ No newline at end of file