Skip to content

Commit

Permalink
Update: ES6 and global definitions fix (fixes #4)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverfoster authored Nov 26, 2024
1 parent 622d0a4 commit 9077098
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 153 deletions.
93 changes: 93 additions & 0 deletions js/Injector.js
Original file line number Diff line number Diff line change
@@ -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();
222 changes: 69 additions & 153 deletions js/adapt-definitions.js
Original file line number Diff line number Diff line change
@@ -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= $("<div>", {
html: html
});
$html.find("*").add($html).each(function(index, node) {

if ($(node).is("[definition], [no-definition]")){
return;
}

var newChildNodes = [];
var wasChanged = false;

for (var nc = 0, ncl = node.childNodes.length; nc < ncl; nc++) {

var child = node.childNodes[nc];

if (child.nodeType !== 3) {
newChildNodes.push(child);
continue;
}

var text = child.textContent;
if (text.search(this._regexp) < 0) {
newChildNodes.push(child);
continue;
}

wasChanged = true;

text = text.replace(this._regexp, function(match, offset, string) {
for (var d = 0, dl = this._items.length; d < dl; d++) {
var item = this._items[d];
if (!match.match(item._regexp)) continue;

return "<span definition='"+item.definition+"'>"+match+"</span>";
}
}.bind(this));

var $html2 = $("<div>", {
html: text
});

$html2[0].childNodes.forEach(function(childNode) {
newChildNodes.push(childNode);
});

}

if (!wasChanged) return;

for (var i = node.childNodes.length-1; i > -1; i--) {
node.removeChild(node.childNodes[i]);
}

newChildNodes.forEach(function(child) {
node.appendChild(child);
});

}.bind(this));

return a11y_text($html[0].outerHTML);
},

onAbbrClick: function(event) {
Adapt.trigger("definition:remove");
var $target = $(event.target);
$('body').on('click', '[definition]', this.onAbbrClick);
$('body').on('keypress', '[definition]', e => {
if (e.which !== 13) return;
this.onAbbrClick(e);
});
}

loadData() {
this.model = new Backbone.Model(Adapt.course.get('_definitions') || { _isEnabled: false });
if (!this.model.get('_isEnabled')) return;
this._items = this.model.get('_items');
if (!this._items?.length) return;
this.setUpRegExps();
this.setUpTable();
}

setUpRegExps() {
const allWords = [];
this._items.forEach(function(item, index) {
item._index = index;
const words = [];
item.words.forEach(function(find) {
const escaped = escapeRegExp(find);
words.push(escaped);
allWords.push(escaped);
});
item._regexp = new RegExp('\\b' + words.join('\\b|\\b') + '\\b', 'g');
});
this._regexp = new RegExp('\\b' + allWords.join('\\b|\\b') + '\\b', 'g');
}

setUpTable() {
this._table = {};
this._items.forEach(item => {
item.words.forEach(word => {
this._table[word] = item.definition;
});
});
}

var word = $target.text();
var definition = $target.attr("definition");
onAbbrClick(event) {
const $target = $(event.target);

var json = _.extend({}, this.model.toJSON(), {word: word, definition: definition});
const word = $target.text();
const definition = $target.attr('definition');

var title = Handlebars.compile(this.model.get("title"))(json);
var body = Handlebars.compile(this.model.get("body"))(json);
const json = _.extend({}, this.model.toJSON(), { word, definition });

Adapt.notify.popup({
"title": title,
"body": "<div no-definition=\"true\">"+body+"</div>",
"_prompts": [
{
promptText: this.model.get("confirmText") || "Close"
}
],
"_showIcon": this.model.get("_showIcon")
});
const title = Handlebars.compile(this.model.get('title'))(json);
const body = Handlebars.compile(this.model.get('body'))(json);

notify.popup({
title,
body: '<div no-definition="true">' + body + '</div>',
_prompts: [
{
promptText: this.model.get('confirmText') || 'Close'
}

],
_showIcon: this.model.get('_showIcon')
});

return new Definitions();
}

}

});
export default (Adapt.definitions = new Definitions());
2 changes: 2 additions & 0 deletions less/definition.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
cursor: pointer;
text-decoration: underline;
}


3 changes: 3 additions & 0 deletions templates/definition.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<span tabindex="0" role="button" definition="{{definition}}">
{{~ term ~}}
</span>

0 comments on commit 9077098

Please sign in to comment.