diff --git a/js/Injector.js b/js/Injector.js new file mode 100644 index 0000000..38e2131 --- /dev/null +++ b/js/Injector.js @@ -0,0 +1,93 @@ +import Adapt from 'core/js/adapt'; +import documentModifications from 'core/js/DOMElementModifications'; +import Handlebars from 'handlebars'; + +const forbiddenParents = 'button, a, [role=dialog], [role=heading], header, span[definition], [no-definition]'; +const requireParents = '.contentobject'; + +class Injector extends Backbone.Controller { + + initialize() { + this.listenTo(Adapt, 'app:dataReady', this.onLoaded); + this._isWatching = false; + this.definitionId = 0; + } + + onLoaded() { + this.processDocument(); + this.startWatching(); + } + + startWatching () { + if (this._isWatching) return; + this._isWatching = true; + this.listenTo(documentModifications, 'added', this.onMutation); + } + + onMutation(event) { + setTimeout(() => { + this.processNode(event.target); + }); + } + + processDocument() { + const nodes = [...document.querySelectorAll('body *:not(script, style, svg)')]; + nodes.forEach(this.processNode); + } + + processNode(node) { + if (node._isDefinitioned) return; + const textNodes = [...node.childNodes] + .filter(node => node.nodeType === Node.TEXT_NODE) + .filter(node => node.nodeValue.trim()); + if (!textNodes.length) return; + textNodes.forEach(node => { + const parentElement = node.parentNode; + const isInsideForbiddenParents = Boolean($(parentElement).closest(forbiddenParents).length); + if (isInsideForbiddenParents) return; + const isInsideRequiredParents = Boolean($(parentElement).closest(requireParents).length); + if (!isInsideRequiredParents) return; + parentElement._isDefinitioned = true; + const value = String(node.nodeValue); + const children = []; + const keywords = [...value.matchAll(Adapt.definitions._regexp)]; + if (!keywords.length) return; + let last = null; + const selected = keywords.reduce((parts, entry) => { + const keyword = entry[0]; + const nextStart = entry.index; + const nextLength = keyword.length; + const lastEnd = last + ? last.index + last[0].length + : null; + if (!last && keywords[0].index - 1 > 0) { + parts.push(document.createTextNode(value.substring(0, keywords[0].index))); + } + if (last && lastEnd < nextStart) { + parts.push(document.createTextNode(value.substring(lastEnd, nextStart))); + + } + const term = value.substring(nextStart, nextStart + nextLength); + const definition = Adapt.definitions._table[keyword]; + this.definitionId++; + const elements = $(Handlebars.templates.definition({ term, definition, id: this.definitionId })); + parts.push(...elements); + last = entry; + return parts; + }, []).filter(Boolean); + last = keywords[keywords.length - 1]; + if (last.index + last[0].length < value.length) { + selected.push(document.createTextNode(value.substring(last.index + last[0].length))); + } + children.push(...selected.map(copy => { + parentElement.insertBefore(copy, node); + copy._isDefinitioned = true; + return copy; + })); + parentElement.removeChild(node); + }); + } + +} + +export default new Injector(); diff --git a/js/adapt-definitions.js b/js/adapt-definitions.js index aa31249..8f1fe5e 100644 --- a/js/adapt-definitions.js +++ b/js/adapt-definitions.js @@ -1,166 +1,82 @@ -define([ - 'core/js/adapt', - 'handlebars' -],function(Adapt, Handlebars) { +import './Injector'; +import Adapt from 'core/js/adapt'; +import Handlebars from 'handlebars'; +import notify from 'core/js/notify'; - function escapeRegExp(text) { - return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - } +function escapeRegExp(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} - function chain(subject, func_name, callback) { - var original = subject[func_name]; - subject[func_name] = function() { - var args = Array.prototype.slice.call(arguments, 0); - args.unshift(function() { - var args = Array.prototype.slice.call(arguments, 0); - return original.apply(this, args); - }.bind(this)); - return callback.apply(this, args); - }; - } +class Definitions extends Backbone.Controller { - var Definitions = Backbone.Controller.extend({ + initialize () { + _.bindAll(this, 'onAbbrClick'); + this.listenTo(Adapt, 'app:dataLoaded', this.loadData); - initialize: function() { - _.bindAll(this, "a11y_text", "onAbbrClick"); - this.listenTo(Adapt, "app:dataLoaded", this.loadData); - - $('body').on('click', "[definition]", this.onAbbrClick); - $('body').on('keypress', "[definition]", e => { - if (e.which !== 13 ) return; - this.onAbbrClick(e); - }); - }, - - loadData: function() { - this.model = new Backbone.Model(Adapt.course.get("_definitions") || {_isEnabled: false}); - - if (!this.model.get("_isEnabled")) { - return; - } - - var items = this.model.get("_items"); - if (!items || !items.length) { - return; - } - - this.setUpRegExps(); - this.setUpA11yTextHook(); - - }, - - setUpRegExps: function() { - this._items = this.model.get("_items"); - var allWords = []; - this._items.forEach(function(item, index) { - item._index = index; - var words = []; - item.words.forEach(function(find) { - var escaped = escapeRegExp(find); - words.push(escaped); - allWords.push(escaped); - }); - item._regexp = new RegExp("\\b"+words.join("\\b|\\b")+"\\b", "gi"); - }); - this._regexp = new RegExp("\\b"+allWords.join("\\b|\\b")+"\\b", "gi"); - }, - - setUpA11yTextHook: function() { - chain(Handlebars.helpers, "a11y_text", this.a11y_text); - }, - - a11y_text: function a11y_text(a11y_text, html) { - - var $html= $("