-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update: ES6 and global definitions fix (fixes #4)
- Loading branch information
1 parent
622d0a4
commit 9077098
Showing
4 changed files
with
167 additions
and
153 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,5 @@ | |
cursor: pointer; | ||
text-decoration: underline; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<span tabindex="0" role="button" definition="{{definition}}"> | ||
{{~ term ~}} | ||
</span> |