Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: ES6 and global definitions fix (fixes #4) #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Loading