diff --git a/.jshintrc b/.jshintrc
index 56db1fe..60bcc78 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,6 +1,16 @@
{
- "browser": true,
- "devel": true,
- "jasmine": true,
- "predef": [ "Sub" ]
+ "browser": true,
+ "devel": true,
+ "jasmine": true,
+ "predef": ["Sub"],
+
+ "node": true,
+ "indent": 2,
+ "latedef": true,
+ "noempty": false,
+ "quotmark": "double",
+ "unused": true,
+ "strict": true,
+ "esversion": 6,
+ "camelcase": false
}
\ No newline at end of file
diff --git a/dist/substituteteacher.js b/dist/substituteteacher.js
new file mode 100644
index 0000000..3ac17e6
--- /dev/null
+++ b/dist/substituteteacher.js
@@ -0,0 +1,1295 @@
+"use strict";
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+(function (window) {
+ "use strict";
+
+ var NBSP = " ";
+
+ var TRANSITIONS = {
+ "WebkitTransition": "webkitTransitionEnd",
+ "MozTransition": "transitionend",
+ "MSTransition": "msTransitionEnd",
+ "OTransition": "otransitionend",
+ "transition": "transitionend"
+ };
+
+ /**
+ * Compare function for sorting annotated actions, used to fine the pair of
+ * sentences with the minimum edit distance.
+ *
+ * @param {Object} annotatedAction1 - the annotated action in question
+ * @param {Object} annotatedAction1.action - the action in question
+ * @param {int} annotatedAction1.action.cost - the action's cost
+ * @param {Object} annotatedAction2 - the annotated action to compare to
+ * @param {Object} annotatedAction2.action - the action to compare to
+ * @param {int} annotatedAction1.action.cost - the action to compare to's cost
+ *
+ * @return {int} difference in cost - positive if 1 > 2, negative if 2 > 1,
+ * 0 if 1 === 2
+ */
+ var _sortAnnotatedAction = function _sortAnnotatedAction(annotatedAction1, annotatedAction2) {
+ return annotatedAction1.action.cost - annotatedAction2.action.cost;
+ };
+
+ var shuffleArr = function shuffleArr(arr) {
+ var tempArr = arr.slice();
+
+ for (var i = tempArr.length; i; i--) {
+ var j = Math.floor(Math.random() * i);
+
+ var _ref = [tempArr[j], tempArr[i - 1]];
+ tempArr[i - 1] = _ref[0];
+ tempArr[j] = _ref[1];
+ }
+
+ return tempArr;
+ };
+
+ /**
+ * Parse the raw sentence into an array of words.
+ *
+ * Separate the sentence by spaces, and then go along each word and pull
+ * punctuation off words that end with a punctuation symbol:
+ *
+ * "We're here (in Wilkes-Barre), finally!" =>
+ * ["We're", "here", "(", "in", "Wilkes-Barre", ")", ",", "finally", "!"]
+ *
+ * TODO: figure out some way to annotate puncatation so that it can be
+ * rendered without a space in it.
+ *
+ * @param {string[]} rawSentences the sentences to parse
+ * @returns {string[][]} sentences the sentences split up into tokens
+ */
+ function _parseSentence(rawSentence) {
+ if (!rawSentence || typeof rawSentence !== "string") {
+ throw "rawSentence must be a string.";
+ }
+
+ var components = [];
+ var start = 0;
+ var end = 0;
+
+ for (; end < rawSentence.length; end++) {
+ var endChar = rawSentence.charAt(end);
+
+ /**
+ * Characters that should "detach" from strings are:
+ * ().,/![]*;:{}=?"+ or whitespace
+ * Characters that remain that remain a part of the word include:
+ * -#$%^&_`~'
+ */
+ if (endChar.match(/[\.,"\/!\?\*\+;:{}=()\[\]\s]/g)) {
+ // Append the word we've been building
+ if (end > start) {
+ components.push(rawSentence.slice(start, end) + (endChar.match(/\s/g) ? NBSP : ""));
+ }
+
+ // If the character is not whitespace, then it is a special character
+ // and should be split off into its own string
+ if (!endChar.match(/\s/g)) {
+ components.push(endChar + (end + 1 < rawSentence.length && rawSentence[end + 1].match(/\s/g) ? NBSP : ""));
+ }
+
+ // The start of the next word is the next character to be seen.
+ start = end + 1;
+ }
+ }
+
+ if (start < end) {
+ components.push(rawSentence.slice(start, end));
+ }
+
+ return components;
+ }
+
+ function _whichTransitionEndEvent() {
+ var el = document.createElement("fakeelement");
+
+ for (var t in TRANSITIONS) {
+ if (el.style[t] !== undefined) {
+ return TRANSITIONS[t];
+ }
+ }
+ }
+
+ /**
+ * Generate the HTML associated with each word.
+ *
+ * @param {string} namespace - the namespace associated with this library,
+ * which should be prepended to classnames.
+ * @param {int} idx - the index of this word in the sentence.
+ *
+ * @returns {string} template - the HTML to inject.
+ */
+ var _wordTemplate = function _wordTemplate(namespace, idx) {
+ return "
\n \n \n
";
+ };
+
+ /**
+ * Inject CSS needed to make the transitions work in the .
+ *
+ * @param {string} namespace - the namespace associated with this library,
+ * which should be prepended to classnames.
+ * @param {number} transitionSpeed - the speed for CSS transitions.
+ * @param {number} height - the outerHeight of the wrapper.
+ */
+ var _injectStyle = function _injectStyle(namespace, transitionSpeed, height, fontFamily) {
+ var head = document.head || document.getElementsByTagName("head")[0];
+ var style = document.createElement("style");
+
+ var css = "@font-face {\n font-family: " + namespace + "-empty;\n src: url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAQ0AAoAAAAAA+wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAJ4AAACeXQ48j09TLzIAAAGUAAAAYAAAAGAIIgbWY21hcAAAAfQAAABEAAAARAAyAGlnYXNwAAACOAAAAAgAAAAIAAAAEGhlYWQAAAJAAAAANgAAADb9mzB5aGhlYQAAAngAAAAkAAAAJAHiAeVobXR4AAACnAAAABAAAAAQAAAAAG1heHAAAAKsAAAABgAAAAYABFAAbmFtZQAAArQAAAFdAAABXVqZXRlwb3N0AAAEFAAAACAAAAAgAAMAAAEABAQAAQEBDHNwYWNlLWVtcHR5AAECAAEAOvgcAvgbA/gYBB4KABlT/4uLHgoAGVP/i4sMB4tr+JT4dAUdAAAAfA8dAAAAgREdAAAACR0AAACVEgAFAQEMFxkbHnNwYWNlLWVtcHR5c3BhY2UtZW1wdHl1MHUxdTIwAAACAYkAAgAEAQEEBwoN/JQO/JQO/JQO/JQO+JQU+JQViwwKAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAAAAIAHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADAAAAAIAAgAAgAAAAEAIP/9//8AAAAAACD//f//AAH/4wADAAEAAAAAAAAAAAABAAH//wAPAAEAAAABAAAAeR2GXw889QALAgAAAAAAzz54vgAAAADPPni+AAAAAAAAAAAAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABQAAAEAAAAAAAOAK4AAQAAAAAAAQAWAAAAAQAAAAAAAgAOAGMAAQAAAAAAAwAWACwAAQAAAAAABAAWAHEAAQAAAAAABQAWABYAAQAAAAAABgALAEIAAQAAAAAACgAoAIcAAwABBAkAAQAWAAAAAwABBAkAAgAOAGMAAwABBAkAAwAWACwAAwABBAkABAAWAHEAAwABBAkABQAWABYAAwABBAkABgAWAE0AAwABBAkACgAoAIcAcwBwAGEAYwBlAC0AZQBtAHAAdAB5AFYAZQByAHMAaQBvAG4AIAAxAC4AMABzAHAAYQBjAGUALQBlAG0AcAB0AHlzcGFjZS1lbXB0eQBzAHAAYQBjAGUALQBlAG0AcAB0AHkAUgBlAGcAdQBsAGEAcgBzAHAAYQBjAGUALQBlAG0AcAB0AHkARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff');\n }\n ." + namespace + "-invisible { visibility: hidden; }\n ." + namespace + "-animating {\n -webkit-transition: " + transitionSpeed + "s all linear;\n -moz-transition: " + transitionSpeed + "s all linear;\n -o-transition: " + transitionSpeed + "s all linear;\n transition: " + transitionSpeed + "s all linear;\n }\n ." + namespace + " {\n position: relative;\n font-family: " + namespace + "-empty;\n margin:\n }\n ." + namespace + ":after {\n content: ' ';\n display: block;\n clear: both;\n }\n ." + namespace + "-text-width-calculation {\n position: absolute;\n visibility: hidden;\n font-family: " + fontFamily + ";\n height: auto;\n width: auto;\n display: inline-block;\n white-space: nowrap;\n }\n ." + namespace + " ." + namespace + "-old-content {\n font-family: " + fontFamily + ";\n position: absolute;\n left: 0;\n width: 100%;\n top: 0;\n height: 100%;\n }\n ." + namespace + "." + namespace + "-loaded ." + namespace + "-old-content {\n display: none;\n }\n ." + namespace + "." + namespace + "-loaded ." + namespace + "-word {\n opacity: 1;\n }\n ." + namespace + " ." + namespace + "-punctuation { margin-left: -0.3rem; }\n ." + namespace + " ." + namespace + "-word {\n display: inline-block;\n position: relative;\n float: left;\n opacity: 0;\n font-family: " + fontFamily + ";\n text-align: center;\n height: " + height + ";\n white-space: nowrap;\n overflow: hidden;\n }\n ." + namespace + " ." + namespace + "-word span {\n top: 0;\n position: relative;\n overflow: hidden;\n height: 1px;\n display: inline-block;\n }\n ." + namespace + " ." + namespace + "-word ." + namespace + "-visible {\n position: absolute;\n display: inline-block;\n height: " + height + ";\n top: 0;\n bottom: 0;\n right:0;\n left: 0;\n }";
+
+ style.type = "text/css";
+
+ if (style.styleSheet) {
+ style.styleSheet.cssText = css;
+ } else {
+ style.appendChild(document.createTextNode(css));
+ }
+
+ head.appendChild(style);
+ };
+
+ /***************************************************************************
+ * *
+ * Animation() *
+ * *
+ ***************************************************************************/
+
+ /**
+ * A privately used class for creating animations. It allows for animations
+ * to have state associated with them, without passing arguments to callback
+ * functions.
+ *
+ * @param {string} animation - one of "remove", "sub", "insert", or
+ * "keep". Indicates the animation to perform,
+ * and forcasts the contents of animationContext.
+ * @param {Object} sub - the instance of the Sub class associated
+ * with this animation.
+ * @param {Object} animationContext - any context that is needed by the
+ * passed animation.
+ */
+
+ var Animation = function () {
+ function Animation(animation, sub, animationContext) {
+ var _this = this;
+
+ _classCallCheck(this, Animation);
+
+ this.sub = sub;
+ this.ctx = animationContext;
+ this.transitionEnd = _whichTransitionEndEvent();
+ this.animatingClass = sub.settings.namespace + "-animating";
+
+ this.steps = function () {
+ if (animation === "remove") {
+ return [function () {
+ _this._fadeOut();
+ }, function () {
+ _this._setWidth();
+ }, function () {
+ _this._removeElement();
+ }];
+ } else if (animation === "sub") {
+ return [function () {
+ _this._reIndex();
+ }, function () {
+ _this._fadeOut();
+ }, function () {
+ _this._setWidth();
+ }, function () {
+ _this._setTextAndFadeIn();
+ }, function () {
+ _this._cleanUp();
+ }];
+ } else if (animation === "insert") {
+ return [function () {
+ _this._setWidth();
+ }, function () {
+ _this._setTextAndFadeIn();
+ }, function () {
+ _this._cleanUp();
+ }];
+ } else if (animation === "keep") {
+ return [function () {
+ _this._reIndex();
+ }];
+ } else {
+ console.error("Unknown animation: ", animation);
+ }
+ }();
+
+ this.steps[0](); // dequeue an run the first task.
+ }
+
+ /**
+ * Change the index class of the word.
+ */
+
+
+ _createClass(Animation, [{
+ key: "_reIndex",
+ value: function _reIndex() {
+ var _ctx = this["ctx"],
+ word = _ctx.word,
+ fromIndexClass = _ctx.fromIndexClass,
+ toIndexClass = _ctx.toIndexClass,
+ steps = this.steps,
+ sub = this.sub;
+
+ // if (this.sub.settings.verbose) { console.log("_reIndex"); }
+
+ // Perform substitution if needed
+
+ if (sub.settings.verbose) {
+ console.log("_reIndex ", word.innerText, " from ", fromIndexClass, " to ", toIndexClass);
+ }
+
+ word.classList.remove(fromIndexClass);
+ word.classList.add(toIndexClass);
+
+ // run next step if there is one
+ steps.shift(); // pop _reIndex
+
+ if (steps.length > 0) {
+ steps[0]();
+ }
+ }
+
+ /**
+ * Fade out this word
+ */
+
+ }, {
+ key: "_fadeOut",
+ value: function _fadeOut() {
+ var _ctx2 = this["ctx"],
+ visible = _ctx2.visible,
+ invisible = _ctx2.invisible,
+ steps = this.steps,
+ sub = this.sub,
+ animatingClass = this.animatingClass,
+ transitionEnd = this.transitionEnd;
+
+
+ if (sub.settings.verbose) {
+ console.log("_fadeOut");
+ }
+
+ /* Hold the containerId width, and fade out */
+ visible.classList.add(animatingClass);
+ steps.shift(); // pop _fadeOut
+
+ visible.addEventListener(transitionEnd, steps[0], false);
+
+ invisible.style.width = invisible.offsetWidth + "px";
+ visible.style.opacity = 0;
+ }
+
+ /**
+ * Set with width of this word to the width of ctx.newText.
+ */
+
+ }, {
+ key: "_setWidth",
+ value: function _setWidth() {
+ var _ctx3 = this["ctx"],
+ visible = _ctx3.visible,
+ invisible = _ctx3.invisible,
+ newText = _ctx3.newText,
+ steps = this.steps,
+ sub = this.sub,
+ animatingClass = this.animatingClass,
+ transitionEnd = this.transitionEnd;
+
+
+ if (sub.settings.verbose) {
+ console.log("_setWidth");
+ }
+ /* Animate the width */
+ visible.classList.remove(animatingClass);
+ invisible.classList.add(animatingClass);
+
+ visible.removeEventListener(transitionEnd, steps[0], false);
+
+ steps.shift(); // pop _setWidth
+
+ invisible.addEventListener(transitionEnd, steps[0], false);
+
+ var newWidth = this._calculateWordWidth(newText, sub.wrapper.tagName, sub.wrapper.className.split(" "));
+
+ setTimeout(function () {
+ invisible.style.width = newWidth + "px";
+ }, 5);
+ }
+
+ /**
+ * Remove this element from the DOM
+ */
+
+ }, {
+ key: "_removeElement",
+ value: function _removeElement() {
+ var _ctx4 = this["ctx"],
+ invisible = _ctx4.invisible,
+ word = _ctx4.word,
+ steps = this.steps,
+ sub = this.sub,
+ transitionEnd = this.transitionEnd;
+
+
+ if (sub.settings.verbose) {
+ console.log("_removeElement");
+ }
+
+ /* Remove this word */
+ invisible.removeEventListener(transitionEnd, steps[0], false);
+ sub.wrapper.removeChild(word);
+ }
+
+ /**
+ * Set the text of this element to ctx.newText and fade it in.
+ */
+
+ }, {
+ key: "_setTextAndFadeIn",
+ value: function _setTextAndFadeIn() {
+ var _ctx5 = this["ctx"],
+ visible = _ctx5.visible,
+ invisible = _ctx5.invisible,
+ newText = _ctx5.newText,
+ steps = this.steps,
+ sub = this.sub,
+ animatingClass = this.animatingClass,
+ transitionEnd = this.transitionEnd;
+
+
+ if (sub.settings.verbose) {
+ console.log("_setTextAndFadeIn");
+ }
+
+ /* Sub the text then fade in */
+ invisible.classList.remove(animatingClass);
+ visible.classList.add(animatingClass);
+
+ invisible.removeEventListener(transitionEnd, steps[0], false);
+
+ steps.shift(); // pop _setTextAndFadeIn
+
+ visible.addEventListener(transitionEnd, steps[0], false);
+
+ visible.innerHTML = newText;
+ invisible.innerHTML = newText;
+ visible.style.opacity = 1;
+ }
+
+ /**
+ * Remove animation classes, remove event listeners, and set widths to "auto"
+ */
+
+ }, {
+ key: "_cleanUp",
+ value: function _cleanUp() {
+ var _ctx6 = this["ctx"],
+ visible = _ctx6.visible,
+ invisible = _ctx6.invisible,
+ steps = this.steps,
+ sub = this.sub,
+ animatingClass = this.animatingClass,
+ transitionEnd = this.transitionEnd;
+
+
+ if (sub.settings.verbose) {
+ console.log("_cleanUp");
+ }
+
+ /* Clean Up */
+ invisible.classList.remove(animatingClass);
+ visible.classList.remove(animatingClass);
+
+ visible.removeEventListener(transitionEnd, steps[0], false);
+ invisible.style.width = "auto";
+ }
+
+ /**
+ * Find the width that an element with a given tag and classes would have if
+ * it contained the passed text.
+ *
+ * @param {string} text - the text to get the width of
+ * @param {string} tag - the tag that the text will be put in
+ * @param {string[]} classes - an array of classes associated with this
+ * element.
+ */
+
+ }, {
+ key: "_calculateWordWidth",
+ value: function _calculateWordWidth(text, tag) {
+ var classes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
+
+ var elem = document.createElement(tag);
+
+ classes.push(this.sub.settings.namespace + "-text-width-calculation");
+
+ elem.setAttribute("class", classes.join(" "));
+ elem.innerHTML = text;
+
+ document.body.appendChild(elem);
+ /* Get a decimal number of the form 12.455 */
+ var width = parseFloat(window.getComputedStyle(elem, null).width);
+
+ elem.parentNode.removeChild(elem);
+
+ return width;
+ }
+ }]);
+
+ return Animation;
+ }();
+
+ /***************************************************************************
+ * *
+ * Sub() *
+ * *
+ ***************************************************************************/
+
+ /**
+ * Sub() - the exposed API for substituteteacher.js
+ *
+ * @param {string[]} rawSentences - An array of sentences to loop between.
+ * @param {Object} options - Configuration options
+ * @param {string} options.containerId - id of the injection point for HTML
+ * default: "sub"
+ * @param {string} options.namespace - namespace to prepend to classes used
+ * internally
+ * default: "sub"
+ * @param {int} options.interval - number of milliseconds between each change
+ * default: 5000
+ * @param {int} options.speed - number of milliseconds that each step of the
+ * animation should take
+ * default: 200
+ * @param {bool} options.verbose - true to enable console logging
+ * default: false
+ * @param {bool} options.random - true if the first sentence to appear should
+ * be random
+ * default: false
+ * @param {bool} options.best - true if the sentences should be ordered to
+ * minimize the number of changes performed
+ * default: true
+ * @param {bool} options.mobileWidth - if defined, the sentence loop will stop
+ * at screen sizes smaller than the width.
+ * defulat: null
+ * @param {bool} options.clearOriginalContent - true if the contents of the
+ * container should be removed
+ * before we inject our elements.
+ * If it is set to false, the
+ * original content will remain
+ * until after the first sentence
+ * is inserted, at which time it
+ * will be hidden
+ * default: true
+ * @param {bool} options._testing - true if testing. sentences will be
+ * ignored
+ */
+
+
+ var Sub = function () {
+ function Sub(rawSentences) {
+ var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ _classCallCheck(this, Sub);
+
+ var settings = {
+ containerId: opts.containerId || "sub",
+ namespace: opts.namespace || "sub",
+ interval: opts.interval || 5000,
+ speed: opts.speed || 200,
+ mobileWidth: opts.mobileWidth || null,
+ verbose: opts.verbose !== undefined ? opts.verbose : false,
+ random: opts.random !== undefined ? opts.random : false,
+ best: opts.best !== undefined ? opts.best : true,
+ clearOriginalContent: opts.clearOriginalContent !== undefined ? opts.clearOriginalContent : true,
+ _testing: opts._testing !== undefined ? opts._testing : false
+ };
+
+ this.wrapper = document.getElementById(settings.containerId);
+ this.settings = settings;
+
+ var namespace = settings.namespace;
+ var wrapperStyle = window.getComputedStyle(this.wrapper);
+
+ _injectStyle(namespace, settings.speed / 1000, wrapperStyle.height, wrapperStyle.fontFamily);
+
+ this.highestTimeoutId = 0;
+ this.currentState = null;
+ this.actions = [];
+ this.invisibleClass = " ." + namespace + "-invisible";
+ this.visibleClass = " ." + namespace + "-visible";
+ this.fromClass = namespace + "-from-idx-";
+ this.toClass = namespace + "-to-idx-";
+ this.wrapperSelector = "#" + namespace;
+ this.isEmpty = true;
+
+ this._setupContainer();
+
+ if (!settings._testing) {
+ this._setSentences(this._parseSentences(rawSentences));
+ }
+ }
+
+ /**
+ * Parse the array of raw sentence strings into an array of arrays of words.
+ *
+ * @param {string[]} rawSentences the sentences to parse
+ * @returns {string[][]} sentences the
+ */
+
+
+ _createClass(Sub, [{
+ key: "_parseSentences",
+ value: function _parseSentences(rawSentences) {
+ if (!rawSentences || (typeof rawSentences === "undefined" ? "undefined" : _typeof(rawSentences)) !== "object") {
+ throw "rawSentences must be an array of strings.";
+ }
+
+ return rawSentences.map(_parseSentence);
+ }
+
+ /**
+ * Find the container for the sentences, empty out any HTML that might be
+ * inside, and then give it the namespace class. It will be the root element
+ * for any changes we might make.
+ */
+
+ }, {
+ key: "_setupContainer",
+ value: function _setupContainer() {
+ var _settings = this.settings,
+ containerId = _settings.containerId,
+ clearOriginalContent = _settings.clearOriginalContent,
+ namespace = _settings.namespace;
+
+
+ var container = document.getElementById(containerId);
+
+ if (!container) {
+ throw "Cannot find element with id:" + containerId;
+ }
+
+ var originalStyle = window.getComputedStyle(container);
+
+ container.style.height = originalStyle.height;
+
+ if (clearOriginalContent) {
+ container.innerHTML = "";
+ } else {
+ container.style.width = originalStyle.width;
+ container.innerHTML = "" + container.innerHTML.replace(" ", NBSP) + "";
+ }
+
+ container.className = namespace;
+ }
+ }, {
+ key: "_getOnResize",
+ value: function _getOnResize() {
+ var _this2 = this;
+
+ this.isStopped = false;
+
+ var onResize = function onResize() {
+ _this2.lastWindowWidth = window.innerWidth;
+
+ // Disable on small screens, if that parameter is provided.
+ if (_this2.settings.mobileWidth !== null) {
+ if (!_this2.isStopped && _this2.lastWindowWidth < _this2.settings.mobileWidth) {
+ // stop on small screens
+ _this2._stop();
+ _this2.isStopped = true;
+ } else if (_this2.isStopped && _this2.lastWindowWidth > _this2.settings.mobileWidth) {
+ // start up again
+ _this2._run();
+ _this2.isStopped = false;
+ }
+ }
+ };
+
+ return onResize;
+ }
+
+ /**
+ * Run the sentence loop. If we haven't successfully populated self.actions,
+ * we delay the running until we have.
+ *
+ * This function should only be called internally.
+ */
+
+ }, {
+ key: "_run",
+ value: function _run() {
+ var _this3 = this;
+
+ var actions = this.actions,
+ wrapper = this.wrapper,
+ _settings2 = this["settings"],
+ namespace = _settings2.namespace,
+ interval = _settings2.interval;
+
+ // We haven't finished generating self.actions yet, so delay running
+
+ if (!this.actions) {
+ setTimeout(function () {
+ this.run();
+ }, 20);
+ return;
+ }
+
+ if (this.isEmpty) {
+ this.isEmpty = false;
+
+ var action = this._computeActionsToChange([], actions[0].from);
+
+ if (!action) {
+ console.log(action);
+
+ throw "returned null action";
+ }
+
+ this._applyAction(action);
+ }
+
+ this.highestTimeoutId = setTimeout(function () {
+ wrapper.classList.add(namespace + "-loaded");
+ wrapper.style.height = "";
+
+ _this3._sentenceLoop();
+ }, interval);
+ }
+
+ /**
+ * Run the sentence loop and add resize handlers. If we haven't successfully
+ * populated self.actions, we delay the running until we have.
+ */
+
+ }, {
+ key: "run",
+ value: function run() {
+ this.onResize = this._getOnResize();
+
+ window.addEventListener("resize", this.onResize, false);
+ window.addEventListener("orientationchange", this.onResize, false);
+
+ this._run();
+
+ return this;
+ }
+
+ /**
+ * Stop the sentence loop. This will stop all animations.
+ *
+ * This function should only be called internally.
+ */
+
+ }, {
+ key: "_stop",
+ value: function _stop() {
+ var self = this;
+
+ clearTimeout(self.highestTimeoutId);
+ }
+
+ /**
+ * Stop the sentence loop. This will stop all animations and remove event
+ * listeners.
+ */
+
+ }, {
+ key: "stop",
+ value: function stop() {
+ window.removeEventListener("resize", this.onResize, false);
+ window.removeEventListener("orientationchange", this.onResize, false);
+
+ this._stop();
+
+ return this;
+ }
+
+ /**
+ * Compute the actions required to transform `from` into `to`.
+ *
+ * Example:
+ * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."]
+ * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"]
+ * output:
+ * {
+ * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."],
+ * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"],
+ * sub:[
+ * { fromWord: "fox", toWord: "color", fromIndex: 3, toIndex: 2 },
+ * { fromWord: "cool", toWord: "very", fromIndex: 6, toIndex: 5 },
+ * { fromWord: "supposedly", toWord: "no", fromIndex: 8, toIndex: 8 },
+ * { fromWord: ".", toWord: "?", fromIndex: 9, toIndex: 9 } ],
+ * remove: [
+ * { fromWord: "quick", fromIndex: 1 } ],
+ * insert: [
+ * { toWord: "pretty", toIndex: 6 } ],
+ * keep: [
+ * { fromWord: "The", toWord: "The", fromIndex: 0, toIndex: 0 },
+ * { fromWord: "brown", toWord: "brown", fromIndex: 2, toIndex: 1 },
+ * { fromWord: "is", toWord: "is", fromIndex: 4, toIndex: 3 },
+ * { fromWord: "very", toWord: "very", fromIndex: 5, toIndex: 4 },
+ * { fromWord: ",", toWord: ",", fromIndex: 7, toIndex: 7 } ],
+ * cost: 6
+ * }
+ *
+ * @param {string[]} from - the sentence to change from
+ * @param {string[]} to - the sentence to change to
+ *
+ * @returns {object} actions - comamnds to perform
+ * @returns {string[]} actions.from - the from sentence
+ * @returns {string[]} actions.to - the to sentence
+ * @returns {object[]} actions.sub - substitutions to do
+ * @returns {string} actions.sub.fromWord - word to sub
+ * @returns {string} actions.sub.toWord - word to sub with
+ * @returns {int} actions.sub.fromIndex - index of word to sub
+ * @returns {int} actions.sub.toIndex - index of word to sub with
+ * @returns {object[]} actions.remove - removals to do
+ * @returns {string} actions.remove.fromWord - word to remove
+ * @returns {int} actions.remove.fromIndex - index of word to remove
+ * @returns {object[]} actions.insert - insertions to do
+ * @returns {string} actions.insert.toWord - word to insert
+ * @returns {int} actions.insert.toIndex - index of word to insert
+ * @returns {object[]} actions.keep - words to keep (no-ops)
+ * @returns {string} actions.keep.fromWord - word to keep (from)
+ * @returns {string} actions.keep.toWord - word to keep (to)
+ * @returns {int} actions.keep.fromIndex - index in from of word to keep
+ * @returns {int} actions.keep.toIndex - index in to of word to keep
+ * @returns {int} actions.cost - total cost of action =
+ * removals + substitutions + insertions
+ */
+
+ }, {
+ key: "_computeActionsToChange",
+ value: function _computeActionsToChange(from, to) {
+ if (this.settings.verbose) {
+ console.log("_computeActionsToChange: ", from, to);
+ }
+
+ var actions = {
+ from: from,
+ to: to,
+ sub: [],
+ remove: [],
+ insert: [],
+ keep: [],
+ cost: 0
+ };
+
+ /**
+ * Recursively creates `actions`, given a start index for each sentence
+ *
+ * @param {int} fromIndex - index of first word to consider in from sentence
+ * @param {int} toIndex - index of first word to consider in to sentence
+ * @param {bool} lookAhead - true if we are looking ahead at other
+ * possible solutions. Actions will not be
+ * modified. false if actions should be modified.
+ * @returns {int} cost - the recursively built cost of actions to take.
+ */
+ var __computeActionsToCange = function __computeActionsToCange(fromIndex, toIndex) {
+ var lookAhead = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ // End of from list
+ if (fromIndex >= from.length) {
+ if (!lookAhead) {
+ actions.insert = actions.insert.concat(to.slice(toIndex).map(function (x, i) {
+ return {
+ toWord: x,
+ toIndex: i + toIndex
+ };
+ }));
+ }
+ // base case, each insert costs 1
+ return to.length - toIndex;
+ }
+
+ // End of to list
+ if (toIndex >= to.length) {
+ if (!lookAhead) {
+ actions.remove = actions.remove.concat(from.slice(fromIndex).map(function (x, i) {
+ return {
+ fromWord: x,
+ fromIndex: i + fromIndex
+ };
+ }));
+ }
+ // base case, each remove costs 1
+ return from.length - toIndex;
+ }
+
+ // Easy Case: a match!
+ if (from[fromIndex] === to[toIndex]) {
+ if (lookAhead) {
+ return 0;
+ }
+
+ actions.keep.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+
+ // keep is free
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1);
+ }
+
+ var foundIndex = from.indexOf(to[toIndex], fromIndex);
+
+ if (lookAhead) {
+ return foundIndex;
+ }
+
+ if (fromIndex + 1 == from.length) {
+ // Can't look ahead, make a move now
+ if (foundIndex === -1) {
+ actions.sub.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+ // Sub costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
+ }
+ }
+
+ var futureIndex = __computeActionsToCange(fromIndex, toIndex + 1, true);
+
+ if (foundIndex === -1) {
+ if (futureIndex === 0) {
+ actions.insert.push({
+ toWord: to[toIndex],
+ toIndex: toIndex
+ });
+ // insert costs 1
+ return __computeActionsToCange(fromIndex, toIndex + 1) + 1;
+ }
+
+ actions.sub.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+ // sub costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
+ }
+
+ if (foundIndex === fromIndex + 1 && futureIndex === fromIndex || foundIndex === futureIndex) {
+ var fromLeft = from.length - fromIndex;
+ var toLeft = to.length - toIndex;
+
+ if (fromLeft > toLeft) {
+ actions.insert.push({
+ toWord: to[toIndex],
+ toIndex: toIndex
+ });
+ // Insert costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex) + 1;
+ }
+
+ // toLeft >= fromLeft
+ actions.remove.push({
+ fromWord: from[fromIndex],
+ fromIndex: fromIndex
+ });
+ // remove costs 1
+ return __computeActionsToCange(fromIndex, toIndex + 1) + 1;
+ }
+
+ if (foundIndex > futureIndex && futureIndex !== -1) {
+ actions.sub.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+ // Sub costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
+ }
+
+ // from.slice
+ // foundIndex < futureIndex
+
+ actions.remove = actions.remove.concat(from.slice(fromIndex, foundIndex).map(function (x, i) {
+ return {
+ fromWord: x,
+ fromIndex: i + fromIndex
+ };
+ }));
+
+ actions.keep.push({
+ fromWord: from[foundIndex],
+ toWord: to[toIndex],
+ fromIndex: foundIndex,
+ toIndex: toIndex
+ });
+
+ // Each remove costs 1, the keep is free
+ return __computeActionsToCange(foundIndex + 1, toIndex + 1) + (foundIndex - fromIndex);
+ };
+
+ // Initalize the recursive call, the final result is the cost.
+ actions.cost = __computeActionsToCange(0, 0);
+
+ return actions;
+ }
+
+ /**
+ * Generate self.actions. If self.settings.best is true, we order the
+ * actions to rotate between sentences with minimal insertions, removals, and
+ * changes. If self.settings.random is true, the sentences will appear in a
+ * random order. If both are set, the sequence will be optimal, but will
+ * start from a random position in the sequence.
+ *
+ * @param {string[][]} sentences - sentences to be converted to actions
+ */
+
+ }, {
+ key: "_setSentences",
+ value: function _setSentences(sentences) {
+ var _this4 = this;
+
+ var sensLen = sentences.length;
+
+ if (sentences.length === 0) {
+ this.actions = [];
+ }
+
+ if (this.settings.best) {
+ /* Because who says the Traveling Salesman Problem isn't releveant? */
+
+ // compute a table of values table[fromIndex][toIndex] = {
+ // fromIndex: fromIndex,
+ // toIndex: toIndex,
+ // action: the action from sentences[fromIndex] to sentences[toIndex]
+ // }
+ var table = sentences.map(function (from, fromIndex) {
+ return sentences.map(function (to, toIndex) {
+ if (fromIndex === toIndex) {
+ return {
+ action: {
+ cost: Number.MAX_VALUE
+ },
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ };
+ }
+
+ var action = _this4._computeActionsToChange(sentences[fromIndex], sentences[toIndex]);
+
+ return {
+ action: action,
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ };
+ });
+ });
+
+ // sort each rows by cost, then sort the rows by lowest cost in that row
+ table.sort(function (row1, row2) {
+ row1.sort(_sortAnnotatedAction);
+ row2.sort(_sortAnnotatedAction);
+
+ return row1[0].cost - row2[0].cost;
+ });
+
+ var from = 0;
+ var i = 0;
+
+ var usedFromIndexes = [];
+ var first = table[0][0].fromIndex;
+
+ // Start with table[0][0], the lowest cost action. Then, find the lowest
+ // cost actions starting from table[0][0].toIndex, and so forth.
+ for (i = 0; i < sensLen; i++) {
+ for (var j = 0; j < sensLen; j++) {
+ if (i === sensLen - 1 && table[from][j].toIndex === first || i !== sensLen - 1 && usedFromIndexes.indexOf(table[from][j].toIndex) === -1) {
+
+ this.actions.push(table[from][j].action);
+ usedFromIndexes.push(from);
+
+ from = table[from][j].toIndex;
+
+ break;
+ }
+ }
+ }
+
+ if (this.settings.random) {
+ // start from somewhere other than the beginning.
+ var start = Math.floor(Math.random() * sensLen);
+
+ for (i = 0; i < start; i++) {
+ this.actions.push(this.actions.shift());
+ }
+ }
+ } else {
+ var sens = this.settings.random ? shuffleArr(sentences) : sentences;
+
+ this.actions = this.actions.concat(sens.map(function (x, i) {
+ var prevIndex = i === 0 ? sensLen - 1 : i - 1;
+
+ return _this4._computeActionsToChange(sens[prevIndex], x);
+ }));
+ }
+ }
+
+ /**
+ * Called in an infinite setTimeout loop. Dequeues an action, performs it,
+ * and enqueues it onto the end of the self.actions array.
+ * Then calls setTimeout on itself, with self.settings.interval.
+ */
+
+ }, {
+ key: "_sentenceLoop",
+ value: function _sentenceLoop() {
+ var _this5 = this;
+
+ var actions = this.actions,
+ interval = this["settings"].interval;
+
+
+ var nextAction = this.actions.shift();
+
+ if (!nextAction) {
+ console.log(nextAction, actions);
+ throw "returned null action";
+ }
+
+ this._applyAction(nextAction);
+ actions.push(nextAction);
+
+ clearTimeout(this.highestTimeoutId);
+
+ this.highestTimeoutId = setTimeout(function () {
+ _this5._sentenceLoop();
+ }, interval);
+ }
+
+ /**
+ * Removes the word from the sentence.
+ *
+ * @param {Object} removeAction - the removal to perform
+ * @param {int} removeAction.fromIndex - the index of the existing word
+ */
+
+ }, {
+ key: "_removeAction",
+ value: function _removeAction(removeAction) {
+ var fromClass = this.fromClass,
+ wrapperSelector = this.wrapperSelector,
+ visibleClass = this.visibleClass,
+ invisibleClass = this.invisibleClass,
+ verbose = this["settings"].verbose;
+
+
+ var fromIndexClass = fromClass + removeAction.fromIndex;
+
+ var animationContext = {
+ fromIndexClass: fromIndexClass,
+ word: document.querySelector(wrapperSelector + " ." + fromIndexClass),
+ visible: document.querySelector(wrapperSelector + " ." + fromIndexClass + visibleClass),
+ invisible: document.querySelector(wrapperSelector + " ." + fromIndexClass + invisibleClass),
+ newText: "" // We'll animate to zero width
+ };
+
+ if (verbose) {
+ console.log("remove", animationContext);
+ }
+
+ new Animation("remove", this, animationContext);
+ }
+
+ /**
+ * Perform the given insertions
+ *
+ * @param {Object[]} insertions - the insertions to perform
+ * @param {int} insertions.toIndex - the index of the element to add
+ * @param {string} insertions.toWord - the word to insert
+ */
+
+ }, {
+ key: "_performInsertions",
+ value: function _performInsertions(insertions) {
+ var _this6 = this;
+
+ var toClass = this.toClass,
+ wrapper = this.wrapper,
+ wrapperSelector = this.wrapperSelector,
+ visibleClass = this.visibleClass,
+ invisibleClass = this.invisibleClass,
+ _settings3 = this["settings"],
+ namespace = _settings3.namespace,
+ speed = _settings3.speed,
+ verbose = _settings3.verbose;
+
+
+ setTimeout(function () {
+ insertions.forEach(function (insertAction) {
+
+ /* Insert new node (no text yet) */
+ var html = _wordTemplate(namespace, insertAction.toIndex);
+
+ if (insertAction.toIndex === 0) {
+ wrapper.insertAdjacentHTML("afterbegin", html);
+ } else {
+ var selector = wrapperSelector + " ." + toClass + (insertAction.toIndex - 1);
+ var prevSibling = document.querySelector(selector);
+
+ prevSibling.insertAdjacentHTML("afterend", html);
+ }
+
+ /* Startup animations */
+ var toIndexClass = toClass + insertAction.toIndex;
+
+ var animationContext = {
+ toIndexClass: toIndexClass,
+ word: document.querySelector(wrapperSelector + " ." + toIndexClass),
+ visible: document.querySelector(wrapperSelector + " ." + toIndexClass + visibleClass),
+ invisible: document.querySelector(wrapperSelector + " ." + toIndexClass + invisibleClass),
+ newText: insertAction.toWord
+ };
+
+ if (verbose) {
+ console.log("insert", animationContext);
+ }
+
+ new Animation("insert", _this6, animationContext);
+ });
+ }, speed);
+ }
+
+ /**
+ * Perform the given substitution
+ *
+ * @param {Object} subAction - the substitution to perform
+ * @param {int} subAction.fromIndex - the index of the element to change
+ * @param {string} subAction.fromWord - the word to sub
+ * @param {int} subAction.toIndex - the index to give the new word
+ * @param {string} subAction.toWord - the word to sub with
+ */
+
+ }, {
+ key: "_subAction",
+ value: function _subAction(subAction) {
+ var fromClass = this.fromClass,
+ toClass = this.toClass,
+ wrapperSelector = this.wrapperSelector,
+ visibleClass = this.visibleClass,
+ invisibleClass = this.invisibleClass,
+ verbose = this["settings"].verbose;
+
+
+ var fromIndexClass = fromClass + subAction.fromIndex;
+
+ var animationContext = {
+ fromIndexClass: fromIndexClass,
+ toIndexClass: toClass + subAction.toIndex,
+ word: document.querySelector(wrapperSelector + " ." + fromIndexClass),
+ visible: document.querySelector(wrapperSelector + " ." + fromIndexClass + visibleClass),
+ invisible: document.querySelector(wrapperSelector + " ." + fromIndexClass + invisibleClass),
+ newText: subAction.toWord
+ };
+
+ if (verbose) {
+ console.log("sub", animationContext);
+ }
+
+ new Animation("sub", this, animationContext);
+ }
+
+ /**
+ * Perform the given keep action.
+ *
+ * @param {Object} keepAction - the keep action to perform
+ * @param {int} keepAction.fromIndex - the index of the word to re-label
+ * @param {int} keepAction.toIndex - the index to label this word
+ */
+
+ }, {
+ key: "_keepAction",
+ value: function _keepAction(keepAction) {
+ var fromClass = this.fromClass,
+ toClass = this.toClass,
+ wrapperSelector = this.wrapperSelector,
+ verbose = this["settings"].verbose;
+
+
+ var fromIndexClass = fromClass + keepAction.fromIndex;
+
+ var animationContext = {
+ fromIndexClass: fromIndexClass,
+ toIndexClass: toClass + keepAction.toIndex,
+ word: document.querySelector(wrapperSelector + " ." + fromIndexClass)
+ };
+
+ if (verbose) {
+ console.log("keep", animationContext);
+ }
+
+ new Animation("keep", this, animationContext);
+ }
+
+ /**
+ * Apply `action`, by performing the necessary substitutions, removals, keeps,
+ * and insertions.
+ */
+
+ }, {
+ key: "_applyAction",
+ value: function _applyAction(action) {
+ var _this7 = this;
+
+ var fromClass = this.fromClass,
+ toClass = this.toClass,
+ _settings4 = this["settings"],
+ namespace = _settings4.namespace,
+ verbose = _settings4.verbose;
+
+
+ var words = document.getElementsByClassName(namespace + "-word");
+
+ Array.from(words).forEach(function (elem) {
+ if (verbose) {
+ console.log("replacing to- with from- for:", elem);
+ }
+
+ elem.className = elem.className.replace(toClass, fromClass);
+ });
+
+ action.sub.map(function (subAction) {
+ _this7._subAction(subAction);
+ });
+
+ action.remove.map(function (removeAction) {
+ _this7._removeAction(removeAction);
+ });
+
+ action.keep.map(function (keepAction) {
+ _this7._keepAction(keepAction);
+ });
+
+ this._performInsertions(action.insert);
+ }
+ }]);
+
+ return Sub;
+ }();
+
+ window.Sub = Sub;
+})(window);
diff --git a/dist/substituteteacher.min.js b/dist/substituteteacher.min.js
new file mode 100644
index 0000000..3a0208e
--- /dev/null
+++ b/dist/substituteteacher.min.js
@@ -0,0 +1 @@
+"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},_createClass=function(){function a(a,b){for(var c=0;cc&&b.push(a.slice(c,e)+(f.match(/\s/g)?d:"")),f.match(/\s/g)||b.push(f+(e+1\n \n \n '},i=function(a,b,c,d){var e=document.head||document.getElementsByTagName("head")[0],f=document.createElement("style"),g="@font-face {\n font-family: "+a+"-empty;\n src: url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAQ0AAoAAAAAA+wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAJ4AAACeXQ48j09TLzIAAAGUAAAAYAAAAGAIIgbWY21hcAAAAfQAAABEAAAARAAyAGlnYXNwAAACOAAAAAgAAAAIAAAAEGhlYWQAAAJAAAAANgAAADb9mzB5aGhlYQAAAngAAAAkAAAAJAHiAeVobXR4AAACnAAAABAAAAAQAAAAAG1heHAAAAKsAAAABgAAAAYABFAAbmFtZQAAArQAAAFdAAABXVqZXRlwb3N0AAAEFAAAACAAAAAgAAMAAAEABAQAAQEBDHNwYWNlLWVtcHR5AAECAAEAOvgcAvgbA/gYBB4KABlT/4uLHgoAGVP/i4sMB4tr+JT4dAUdAAAAfA8dAAAAgREdAAAACR0AAACVEgAFAQEMFxkbHnNwYWNlLWVtcHR5c3BhY2UtZW1wdHl1MHUxdTIwAAACAYkAAgAEAQEEBwoN/JQO/JQO/JQO/JQO+JQU+JQViwwKAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAAAAIAHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADAAAAAIAAgAAgAAAAEAIP/9//8AAAAAACD//f//AAH/4wADAAEAAAAAAAAAAAABAAH//wAPAAEAAAABAAAAeR2GXw889QALAgAAAAAAzz54vgAAAADPPni+AAAAAAAAAAAAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABQAAAEAAAAAAAOAK4AAQAAAAAAAQAWAAAAAQAAAAAAAgAOAGMAAQAAAAAAAwAWACwAAQAAAAAABAAWAHEAAQAAAAAABQAWABYAAQAAAAAABgALAEIAAQAAAAAACgAoAIcAAwABBAkAAQAWAAAAAwABBAkAAgAOAGMAAwABBAkAAwAWACwAAwABBAkABAAWAHEAAwABBAkABQAWABYAAwABBAkABgAWAE0AAwABBAkACgAoAIcAcwBwAGEAYwBlAC0AZQBtAHAAdAB5AFYAZQByAHMAaQBvAG4AIAAxAC4AMABzAHAAYQBjAGUALQBlAG0AcAB0AHlzcGFjZS1lbXB0eQBzAHAAYQBjAGUALQBlAG0AcAB0AHkAUgBlAGcAdQBsAGEAcgBzAHAAYQBjAGUALQBlAG0AcAB0AHkARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff');\n }\n ."+a+"-invisible { visibility: hidden; }\n ."+a+"-animating {\n -webkit-transition: "+b+"s all linear;\n -moz-transition: "+b+"s all linear;\n -o-transition: "+b+"s all linear;\n transition: "+b+"s all linear;\n }\n ."+a+" {\n position: relative;\n font-family: "+a+"-empty;\n margin:\n }\n ."+a+":after {\n content: ' ';\n display: block;\n clear: both;\n }\n ."+a+"-text-width-calculation {\n position: absolute;\n visibility: hidden;\n font-family: "+d+";\n height: auto;\n width: auto;\n display: inline-block;\n white-space: nowrap;\n }\n ."+a+" ."+a+"-old-content {\n font-family: "+d+";\n position: absolute;\n left: 0;\n width: 100%;\n top: 0;\n height: 100%;\n }\n ."+a+"."+a+"-loaded ."+a+"-old-content {\n display: none;\n }\n ."+a+"."+a+"-loaded ."+a+"-word {\n opacity: 1;\n }\n ."+a+" ."+a+"-punctuation { margin-left: -0.3rem; }\n ."+a+" ."+a+"-word {\n display: inline-block;\n position: relative;\n float: left;\n opacity: 0;\n font-family: "+d+";\n text-align: center;\n height: "+c+";\n white-space: nowrap;\n overflow: hidden;\n }\n ."+a+" ."+a+"-word span {\n top: 0;\n position: relative;\n overflow: hidden;\n height: 1px;\n display: inline-block;\n }\n ."+a+" ."+a+"-word ."+a+"-visible {\n position: absolute;\n display: inline-block;\n height: "+c+";\n top: 0;\n bottom: 0;\n right:0;\n left: 0;\n }";f.type="text/css",f.styleSheet?f.styleSheet.cssText=g:f.appendChild(document.createTextNode(g)),e.appendChild(f)},j=function(){function b(a,d,e){var f=this;_classCallCheck(this,b),this.sub=d,this.ctx=e,this.transitionEnd=c(),this.animatingClass=d.settings.namespace+"-animating",this.steps=function(){return"remove"===a?[function(){f._fadeOut()},function(){f._setWidth()},function(){f._removeElement()}]:"sub"===a?[function(){f._reIndex()},function(){f._fadeOut()},function(){f._setWidth()},function(){f._setTextAndFadeIn()},function(){f._cleanUp()}]:"insert"===a?[function(){f._setWidth()},function(){f._setTextAndFadeIn()},function(){f._cleanUp()}]:"keep"===a?[function(){f._reIndex()}]:void console.error("Unknown animation: ",a)}(),this.steps[0]()}return _createClass(b,[{key:"_reIndex",value:function(){var a=this.ctx,b=a.word,c=a.fromIndexClass,d=a.toIndexClass,e=this.steps;this.sub.settings.verbose&&console.log("_reIndex ",b.innerText," from ",c," to ",d),b.classList.remove(c),b.classList.add(d),e.shift(),e.length>0&&e[0]()}},{key:"_fadeOut",value:function(){var a=this.ctx,b=a.visible,c=a.invisible,d=this.steps,e=this.sub,f=this.animatingClass,g=this.transitionEnd;e.settings.verbose&&console.log("_fadeOut"),b.classList.add(f),d.shift(),b.addEventListener(g,d[0],!1),c.style.width=c.offsetWidth+"px",b.style.opacity=0}},{key:"_setWidth",value:function(){var a=this.ctx,b=a.visible,c=a.invisible,d=a.newText,e=this.steps,f=this.sub,g=this.animatingClass,h=this.transitionEnd;f.settings.verbose&&console.log("_setWidth"),b.classList.remove(g),c.classList.add(g),b.removeEventListener(h,e[0],!1),e.shift(),c.addEventListener(h,e[0],!1);var i=this._calculateWordWidth(d,f.wrapper.tagName,f.wrapper.className.split(" "));setTimeout(function(){c.style.width=i+"px"},5)}},{key:"_removeElement",value:function(){var a=this.ctx,b=a.invisible,c=a.word,d=this.steps,e=this.sub,f=this.transitionEnd;e.settings.verbose&&console.log("_removeElement"),b.removeEventListener(f,d[0],!1),e.wrapper.removeChild(c)}},{key:"_setTextAndFadeIn",value:function(){var a=this.ctx,b=a.visible,c=a.invisible,d=a.newText,e=this.steps,f=this.sub,g=this.animatingClass,h=this.transitionEnd;f.settings.verbose&&console.log("_setTextAndFadeIn"),c.classList.remove(g),b.classList.add(g),c.removeEventListener(h,e[0],!1),e.shift(),b.addEventListener(h,e[0],!1),b.innerHTML=d,c.innerHTML=d,b.style.opacity=1}},{key:"_cleanUp",value:function(){var a=this.ctx,b=a.visible,c=a.invisible,d=this.steps,e=this.sub,f=this.animatingClass,g=this.transitionEnd;e.settings.verbose&&console.log("_cleanUp"),c.classList.remove(f),b.classList.remove(f),b.removeEventListener(g,d[0],!1),c.style.width="auto"}},{key:"_calculateWordWidth",value:function(b,c){var d=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],e=document.createElement(c);d.push(this.sub.settings.namespace+"-text-width-calculation"),e.setAttribute("class",d.join(" ")),e.innerHTML=b,document.body.appendChild(e);var f=parseFloat(a.getComputedStyle(e,null).width);return e.parentNode.removeChild(e),f}}]),b}(),k=function(){function c(b){var d=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};_classCallCheck(this,c);var e={containerId:d.containerId||"sub",namespace:d.namespace||"sub",interval:d.interval||5e3,speed:d.speed||200,mobileWidth:d.mobileWidth||null,verbose:void 0!==d.verbose&&d.verbose,random:void 0!==d.random&&d.random,best:void 0===d.best||d.best,clearOriginalContent:void 0===d.clearOriginalContent||d.clearOriginalContent,_testing:void 0!==d._testing&&d._testing};this.wrapper=document.getElementById(e.containerId),this.settings=e;var f=e.namespace,g=a.getComputedStyle(this.wrapper);i(f,e.speed/1e3,g.height,g.fontFamily),this.highestTimeoutId=0,this.currentState=null,this.actions=[],this.invisibleClass=" ."+f+"-invisible",this.visibleClass=" ."+f+"-visible",this.fromClass=f+"-from-idx-",this.toClass=f+"-to-idx-",this.wrapperSelector="#"+f,this.isEmpty=!0,this._setupContainer(),e._testing||this._setSentences(this._parseSentences(b))}return _createClass(c,[{key:"_parseSentences",value:function(a){if(!a||"object"!==(void 0===a?"undefined":_typeof(a)))throw"rawSentences must be an array of strings.";return a.map(b)}},{key:"_setupContainer",value:function(){var b=this.settings,c=b.containerId,e=b.clearOriginalContent,f=b.namespace,g=document.getElementById(c);if(!g)throw"Cannot find element with id:"+c;var h=a.getComputedStyle(g);g.style.height=h.height,e?g.innerHTML="":(g.style.width=h.width,g.innerHTML=''+g.innerHTML.replace(" ",d)+""),g.className=f}},{key:"_getOnResize",value:function(){var b=this;return this.isStopped=!1,function(){b.lastWindowWidth=a.innerWidth,null!==b.settings.mobileWidth&&(!b.isStopped&&b.lastWindowWidthb.settings.mobileWidth&&(b._run(),b.isStopped=!1))}}},{key:"_run",value:function(){var a=this,b=this.actions,c=this.wrapper,d=this.settings,e=d.namespace,f=d.interval;if(!this.actions)return void setTimeout(function(){this.run()},20);if(this.isEmpty){this.isEmpty=!1;var g=this._computeActionsToChange([],b[0].from);if(!g)throw console.log(g),"returned null action";this._applyAction(g)}this.highestTimeoutId=setTimeout(function(){c.classList.add(e+"-loaded"),c.style.height="",a._sentenceLoop()},f)}},{key:"run",value:function(){return this.onResize=this._getOnResize(),a.addEventListener("resize",this.onResize,!1),a.addEventListener("orientationchange",this.onResize,!1),this._run(),this}},{key:"_stop",value:function(){var a=this;clearTimeout(a.highestTimeoutId)}},{key:"stop",value:function(){return a.removeEventListener("resize",this.onResize,!1),a.removeEventListener("orientationchange",this.onResize,!1),this._stop(),this}},{key:"_computeActionsToChange",value:function(a,b){this.settings.verbose&&console.log("_computeActionsToChange: ",a,b);var c={from:a,to:b,sub:[],remove:[],insert:[],keep:[],cost:0},d=function d(e,f){var g=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(e>=a.length)return g||(c.insert=c.insert.concat(b.slice(f).map(function(a,b){return{toWord:a,toIndex:b+f}}))),b.length-f;if(f>=b.length)return g||(c.remove=c.remove.concat(a.slice(e).map(function(a,b){return{fromWord:a,fromIndex:b+e}}))),a.length-f;if(a[e]===b[f])return g?0:(c.keep.push({fromWord:a[e],toWord:b[f],fromIndex:e,toIndex:f}),d(e+1,f+1));var h=a.indexOf(b[f],e);if(g)return h;if(e+1==a.length&&-1===h)return c.sub.push({fromWord:a[e],toWord:b[f],fromIndex:e,toIndex:f}),d(e+1,f+1)+1;var i=d(e,f+1,!0);if(-1===h)return 0===i?(c.insert.push({toWord:b[f],toIndex:f}),d(e,f+1)+1):(c.sub.push({fromWord:a[e],toWord:b[f],fromIndex:e,toIndex:f}),d(e+1,f+1)+1);if(h===e+1&&i===e||h===i){return a.length-e>b.length-f?(c.insert.push({toWord:b[f],toIndex:f}),d(e+1,f)+1):(c.remove.push({fromWord:a[e],fromIndex:e}),d(e,f+1)+1)}return h>i&&-1!==i?(c.sub.push({fromWord:a[e],toWord:b[f],fromIndex:e,toIndex:f}),d(e+1,f+1)+1):(c.remove=c.remove.concat(a.slice(e,h).map(function(a,b){return{fromWord:a,fromIndex:b+e}})),c.keep.push({fromWord:a[h],toWord:b[f],fromIndex:h,toIndex:f}),d(h+1,f+1)+(h-e))};return c.cost=d(0,0),c}},{key:"_setSentences",value:function(a){var b=this,c=a.length;if(0===a.length&&(this.actions=[]),this.settings.best){var d=a.map(function(c,d){return a.map(function(c,e){return d===e?{action:{cost:Number.MAX_VALUE},fromIndex:d,toIndex:e}:{action:b._computeActionsToChange(a[d],a[e]),fromIndex:d,toIndex:e}})});d.sort(function(a,b){return a.sort(f),b.sort(f),a[0].cost-b[0].cost});var e=0,h=0,i=[],j=d[0][0].fromIndex;for(h=0;h {
"use strict";
+ const NBSP = " ";
+
+ const TRANSITIONS = {
+ "WebkitTransition": "webkitTransitionEnd",
+ "MozTransition": "transitionend",
+ "MSTransition": "msTransitionEnd",
+ "OTransition": "otransitionend",
+ "transition": "transitionend",
+ };
+
/**
* Compare function for sorting annotated actions, used to fine the pair of
* sentences with the minimum edit distance.
@@ -16,9 +25,21 @@
* @return {int} difference in cost - positive if 1 > 2, negative if 2 > 1,
* 0 if 1 === 2
*/
- function _sortAnnotatedAction(annotatedAction1, annotatedAction2) {
+ const _sortAnnotatedAction = (annotatedAction1, annotatedAction2) => {
return annotatedAction1.action.cost - annotatedAction2.action.cost;
- }
+ };
+
+ const shuffleArr = arr => {
+ const tempArr = arr.slice();
+
+ for (let i = tempArr.length; i; i--) {
+ const j = Math.floor(Math.random() * i);
+
+ [tempArr[i - 1], tempArr[j]] = [tempArr[j], tempArr[i - 1]];
+ }
+
+ return tempArr;
+ };
/**
* Parse the raw sentence into an array of words.
@@ -39,10 +60,13 @@
if (!rawSentence || typeof rawSentence !== "string") {
throw "rawSentence must be a string.";
}
+
var components = [];
- var start, end, endChar;
- for (start = 0, end = 0; end < rawSentence.length; end++) {
- endChar = rawSentence.charAt(end);
+ let start = 0;
+ let end = 0;
+
+ for (; end < rawSentence.length; end++) {
+ const endChar = rawSentence.charAt(end);
/**
* Characters that should "detach" from strings are:
@@ -53,53 +77,33 @@
if (endChar.match(/[\.,"\/!\?\*\+;:{}=()\[\]\s]/g)) {
// Append the word we've been building
if (end > start) {
- if (endChar.match(/\s/g)) {
- components.push(rawSentence.slice(start, end) + " ");
- } else {
- components.push(rawSentence.slice(start, end));
- }
+ components.push(rawSentence.slice(start, end) + (endChar.match(/\s/g) ? NBSP : ""));
}
// If the character is not whitespace, then it is a special character
// and should be split off into its own string
if (!endChar.match(/\s/g)) {
- if (end +1 < rawSentence.length && rawSentence.charAt(end + 1).match(/\s/g)) {
- components.push(endChar + " ");
- } else {
- components.push(endChar);
- }
+ components.push(endChar + (end + 1 < rawSentence.length && rawSentence[end + 1].match(/\s/g) ? NBSP : ""));
}
// The start of the next word is the next character to be seen.
start = end + 1;
}
}
+
if (start < end) {
components.push(rawSentence.slice(start, end));
}
+
return components;
}
- /**
- * Find the CSS transition end event that we should listen for.
- *
- * @returns {string} t - the transition string
- */
function _whichTransitionEndEvent() {
- var t;
- var el = document.createElement("fakeelement");
- var transitions = {
- "WebkitTransition": "webkitTransitionEnd",
- "MozTransition": "transitionend",
- "MSTransition": "msTransitionEnd",
- "OTransition": "otransitionend",
- "transition": "transitionend",
- };
- for (t in transitions) {
- if (transitions.hasOwnProperty(t)) {
- if (el.style[t] !== undefined) {
- return transitions[t];
- }
+ const el = document.createElement("fakeelement");
+
+ for (let t in TRANSITIONS) {
+ if (el.style[t] !== undefined) {
+ return TRANSITIONS[t];
}
}
}
@@ -113,14 +117,14 @@
*
* @returns {string} template - the HTML to inject.
*/
- function _wordTemplate(namespace, idx) {
+ const _wordTemplate = (namespace, idx) => {
return (
- "" +
- "" +
- "" +
- "
"
+ `
+
+
+
`
);
- }
+ };
/**
* Inject CSS needed to make the transitions work in the .
@@ -130,78 +134,86 @@
* @param {number} transitionSpeed - the speed for CSS transitions.
* @param {number} height - the outerHeight of the wrapper.
*/
- function _injectStyle(namespace, transitionSpeed, height, fontFamily) {
- var css =
- "@font-face {\n" +
- " font-family: " + namespace + "-empty;\n" +
- " src: url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAQ0AAoAAAAAA+wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAJ4AAACeXQ48j09TLzIAAAGUAAAAYAAAAGAIIgbWY21hcAAAAfQAAABEAAAARAAyAGlnYXNwAAACOAAAAAgAAAAIAAAAEGhlYWQAAAJAAAAANgAAADb9mzB5aGhlYQAAAngAAAAkAAAAJAHiAeVobXR4AAACnAAAABAAAAAQAAAAAG1heHAAAAKsAAAABgAAAAYABFAAbmFtZQAAArQAAAFdAAABXVqZXRlwb3N0AAAEFAAAACAAAAAgAAMAAAEABAQAAQEBDHNwYWNlLWVtcHR5AAECAAEAOvgcAvgbA/gYBB4KABlT/4uLHgoAGVP/i4sMB4tr+JT4dAUdAAAAfA8dAAAAgREdAAAACR0AAACVEgAFAQEMFxkbHnNwYWNlLWVtcHR5c3BhY2UtZW1wdHl1MHUxdTIwAAACAYkAAgAEAQEEBwoN/JQO/JQO/JQO/JQO+JQU+JQViwwKAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAAAAIAHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADAAAAAIAAgAAgAAAAEAIP/9//8AAAAAACD//f//AAH/4wADAAEAAAAAAAAAAAABAAH//wAPAAEAAAABAAAAeR2GXw889QALAgAAAAAAzz54vgAAAADPPni+AAAAAAAAAAAAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABQAAAEAAAAAAAOAK4AAQAAAAAAAQAWAAAAAQAAAAAAAgAOAGMAAQAAAAAAAwAWACwAAQAAAAAABAAWAHEAAQAAAAAABQAWABYAAQAAAAAABgALAEIAAQAAAAAACgAoAIcAAwABBAkAAQAWAAAAAwABBAkAAgAOAGMAAwABBAkAAwAWACwAAwABBAkABAAWAHEAAwABBAkABQAWABYAAwABBAkABgAWAE0AAwABBAkACgAoAIcAcwBwAGEAYwBlAC0AZQBtAHAAdAB5AFYAZQByAHMAaQBvAG4AIAAxAC4AMABzAHAAYQBjAGUALQBlAG0AcAB0AHlzcGFjZS1lbXB0eQBzAHAAYQBjAGUALQBlAG0AcAB0AHkAUgBlAGcAdQBsAGEAcgBzAHAAYQBjAGUALQBlAG0AcAB0AHkARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff');\n" +
- "}\n" +
- "." + namespace + "-invisible { visibility: hidden; }\n" +
- "." + namespace + "-animating {\n" +
- " -webkit-transition: " + transitionSpeed + "s all linear;\n" +
- " -moz-transition: " + transitionSpeed + "s all linear;\n" +
- " -o-transition: " + transitionSpeed + "s all linear;\n" +
- " transition: " + transitionSpeed + "s all linear; }\n" +
- "." + namespace + " {\n" +
- " position: relative;\n" +
- " font-family: " + namespace + "-empty;\n" +
- " margin: 0;}\n" +
- "." + namespace + ":after {\n" +
- " content: ' ';\n" +
- " display: block;\n" +
- " clear: both;}\n" +
- "." + namespace + "-text-width-calculation {\n" +
- " position: absolute;\n" +
- " visibility: hidden;\n" +
- " font-family: " + fontFamily + ";\n" +
- " height: auto;\n" +
- " width: auto;\n" +
- " display: inline-block;\n" +
- " white-space: nowrap; }\n" +
- " ." + namespace + " ." + namespace + "-old-content {\n" +
- " font-family: " + fontFamily + ";\n" +
- " position: absolute;\n" +
- " left: 0;\n" +
- " width: 100%;\n" +
- " top: 0;\n" +
- " height: 100%;\n" +
- " }\n" +
- " ." + namespace + "." + namespace + "-loaded ." + namespace + "-old-content {" +
- " display: none;" +
- " }\n" +
- " ." + namespace + "." + namespace + "-loaded ." + namespace + "-word {" +
- " opacity: 1;" +
- " }\n" +
- " ." + namespace + " ." + namespace + "-punctuation { margin-left: -0.3rem; }\n" +
- " ." + namespace + " ." + namespace + "-word {\n" +
- " display: inline-block;\n" +
- " position: relative;\n" +
- " float: left;\n" +
- " opacity: 0;\n" +
- " font-family: " + fontFamily + ";\n" +
- " text-align: center;\n" +
- " height: " + height + ";\n" +
- " white-space: nowrap;\n" +
- " overflow: hidden;}\n" +
- " ." + namespace + " ." + namespace + "-word span {\n" +
- " top: 0;\n" +
- " position: relative;\n" +
- " overflow: hidden;\n" +
- " height: 1px;\n" +
- // " white-space: nowrap;\n" +
- " display: inline-block;}\n" +
- " ." + namespace + " ." + namespace + "-word ." + namespace + "-visible {\n" +
- " position: absolute;\n" +
- " display: inline-block;\n" +
- " height: " + height + ";\n" +
- " top: 0;\n" +
- " bottom: 0;\n" +
- " right:0;\n" +
- " left: 0;}";
- var head = document.head || document.getElementsByTagName("head")[0];
- var style = document.createElement("style");
+ const _injectStyle = (namespace, transitionSpeed, height, fontFamily) => {
+ const head = document.head || document.getElementsByTagName("head")[0];
+ const style = document.createElement("style");
+
+ const css =
+ `@font-face {
+ font-family: ${namespace}-empty;
+ src: url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAQ0AAoAAAAAA+wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAJ4AAACeXQ48j09TLzIAAAGUAAAAYAAAAGAIIgbWY21hcAAAAfQAAABEAAAARAAyAGlnYXNwAAACOAAAAAgAAAAIAAAAEGhlYWQAAAJAAAAANgAAADb9mzB5aGhlYQAAAngAAAAkAAAAJAHiAeVobXR4AAACnAAAABAAAAAQAAAAAG1heHAAAAKsAAAABgAAAAYABFAAbmFtZQAAArQAAAFdAAABXVqZXRlwb3N0AAAEFAAAACAAAAAgAAMAAAEABAQAAQEBDHNwYWNlLWVtcHR5AAECAAEAOvgcAvgbA/gYBB4KABlT/4uLHgoAGVP/i4sMB4tr+JT4dAUdAAAAfA8dAAAAgREdAAAACR0AAACVEgAFAQEMFxkbHnNwYWNlLWVtcHR5c3BhY2UtZW1wdHl1MHUxdTIwAAACAYkAAgAEAQEEBwoN/JQO/JQO/JQO/JQO+JQU+JQViwwKAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABAAAAAIAHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADAAAAAIAAgAAgAAAAEAIP/9//8AAAAAACD//f//AAH/4wADAAEAAAAAAAAAAAABAAH//wAPAAEAAAABAAAAeR2GXw889QALAgAAAAAAzz54vgAAAADPPni+AAAAAAAAAAAAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABQAAAEAAAAAAAOAK4AAQAAAAAAAQAWAAAAAQAAAAAAAgAOAGMAAQAAAAAAAwAWACwAAQAAAAAABAAWAHEAAQAAAAAABQAWABYAAQAAAAAABgALAEIAAQAAAAAACgAoAIcAAwABBAkAAQAWAAAAAwABBAkAAgAOAGMAAwABBAkAAwAWACwAAwABBAkABAAWAHEAAwABBAkABQAWABYAAwABBAkABgAWAE0AAwABBAkACgAoAIcAcwBwAGEAYwBlAC0AZQBtAHAAdAB5AFYAZQByAHMAaQBvAG4AIAAxAC4AMABzAHAAYQBjAGUALQBlAG0AcAB0AHlzcGFjZS1lbXB0eQBzAHAAYQBjAGUALQBlAG0AcAB0AHkAUgBlAGcAdQBsAGEAcgBzAHAAYQBjAGUALQBlAG0AcAB0AHkARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff');
+ }
+ .${namespace}-invisible { visibility: hidden; }
+ .${namespace}-animating {
+ -webkit-transition: ${transitionSpeed}s all linear;
+ -moz-transition: ${transitionSpeed}s all linear;
+ -o-transition: ${transitionSpeed}s all linear;
+ transition: ${transitionSpeed}s all linear;
+ }
+ .${namespace} {
+ position: relative;
+ font-family: ${namespace}-empty;
+ margin:
+ }
+ .${namespace}:after {
+ content: ' ';
+ display: block;
+ clear: both;
+ }
+ .${namespace}-text-width-calculation {
+ position: absolute;
+ visibility: hidden;
+ font-family: ${fontFamily};
+ height: auto;
+ width: auto;
+ display: inline-block;
+ white-space: nowrap;
+ }
+ .${namespace} .${namespace}-old-content {
+ font-family: ${fontFamily};
+ position: absolute;
+ left: 0;
+ width: 100%;
+ top: 0;
+ height: 100%;
+ }
+ .${namespace}.${namespace}-loaded .${namespace}-old-content {
+ display: none;
+ }
+ .${namespace}.${namespace}-loaded .${namespace}-word {
+ opacity: 1;
+ }
+ .${namespace} .${namespace}-punctuation { margin-left: -0.3rem; }
+ .${namespace} .${namespace}-word {
+ display: inline-block;
+ position: relative;
+ float: left;
+ opacity: 0;
+ font-family: ${fontFamily};
+ text-align: center;
+ height: ${height};
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ .${namespace} .${namespace}-word span {
+ top: 0;
+ position: relative;
+ overflow: hidden;
+ height: 1px;
+ display: inline-block;
+ }
+ .${namespace} .${namespace}-word .${namespace}-visible {
+ position: absolute;
+ display: inline-block;
+ height: ${height};
+ top: 0;
+ bottom: 0;
+ right:0;
+ left: 0;
+ }`;
style.type = "text/css";
+
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
@@ -209,6 +221,302 @@
}
head.appendChild(style);
+ };
+
+ /***************************************************************************
+ * *
+ * Animation() *
+ * *
+ ***************************************************************************/
+
+ /**
+ * A privately used class for creating animations. It allows for animations
+ * to have state associated with them, without passing arguments to callback
+ * functions.
+ *
+ * @param {string} animation - one of "remove", "sub", "insert", or
+ * "keep". Indicates the animation to perform,
+ * and forcasts the contents of animationContext.
+ * @param {Object} sub - the instance of the Sub class associated
+ * with this animation.
+ * @param {Object} animationContext - any context that is needed by the
+ * passed animation.
+ */
+ class Animation {
+ constructor(animation, sub, animationContext) {
+ this.sub = sub;
+ this.ctx = animationContext;
+ this.transitionEnd = _whichTransitionEndEvent();
+ this.animatingClass = sub.settings.namespace + "-animating";
+
+ this.steps = (() => {
+ if (animation === "remove") {
+ return [
+ () => {
+ this._fadeOut();
+ },
+ () => {
+ this._setWidth();
+ },
+ () => {
+ this._removeElement();
+ }
+ ];
+ } else if (animation === "sub") {
+ return [
+ () => {
+ this._reIndex();
+ },
+ () => {
+ this._fadeOut();
+ },
+ () => {
+ this._setWidth();
+ },
+ () => {
+ this._setTextAndFadeIn();
+ },
+ () => {
+ this._cleanUp();
+ }
+ ];
+ } else if (animation === "insert") {
+ return [
+ () => {
+ this._setWidth();
+ },
+ () => {
+ this._setTextAndFadeIn();
+ },
+ () => {
+ this._cleanUp();
+ }
+ ];
+ } else if (animation === "keep") {
+ return [
+ () => {
+ this._reIndex();
+ }
+ ];
+ } else {
+ console.error("Unknown animation: ", animation);
+ }
+ })();
+
+ this.steps[0](); // dequeue an run the first task.
+ }
+
+ /**
+ * Change the index class of the word.
+ */
+ _reIndex() {
+ const {
+ "ctx": {
+ word,
+ fromIndexClass,
+ toIndexClass
+ },
+ steps,
+ sub
+ } = this;
+
+ // if (this.sub.settings.verbose) { console.log("_reIndex"); }
+
+ // Perform substitution if needed
+ if (sub.settings.verbose) {
+ console.log("_reIndex ", word.innerText, " from ", fromIndexClass, " to ", toIndexClass);
+ }
+
+ word.classList.remove(fromIndexClass);
+ word.classList.add(toIndexClass);
+
+ // run next step if there is one
+ steps.shift(); // pop _reIndex
+
+ if (steps.length > 0) {
+ steps[0]();
+ }
+ }
+
+ /**
+ * Fade out this word
+ */
+ _fadeOut() {
+ const {
+ "ctx": {
+ visible,
+ invisible
+ },
+ steps,
+ sub,
+ animatingClass,
+ transitionEnd
+ } = this;
+
+ if (sub.settings.verbose) {
+ console.log("_fadeOut");
+ }
+
+ /* Hold the containerId width, and fade out */
+ visible.classList.add(animatingClass);
+ steps.shift(); // pop _fadeOut
+
+ visible.addEventListener(transitionEnd, steps[0], false);
+
+ invisible.style.width = invisible.offsetWidth + "px";
+ visible.style.opacity = 0;
+ }
+
+ /**
+ * Set with width of this word to the width of ctx.newText.
+ */
+ _setWidth() {
+ const {
+ "ctx": {
+ visible,
+ invisible,
+ newText
+ },
+ steps,
+ sub,
+ animatingClass,
+ transitionEnd
+ } = this;
+
+ if (sub.settings.verbose) {
+ console.log("_setWidth");
+ }
+ /* Animate the width */
+ visible.classList.remove(animatingClass);
+ invisible.classList.add(animatingClass);
+
+ visible.removeEventListener(transitionEnd, steps[0], false);
+
+ steps.shift(); // pop _setWidth
+
+ invisible.addEventListener(transitionEnd, steps[0], false);
+
+ const newWidth = this._calculateWordWidth(
+ newText,
+ sub.wrapper.tagName,
+ sub.wrapper.className.split(" ")
+ );
+
+ setTimeout(() => {
+ invisible.style.width = newWidth + "px";
+ }, 5);
+ }
+
+ /**
+ * Remove this element from the DOM
+ */
+ _removeElement() {
+ const {
+ "ctx": {
+ invisible,
+ word
+ },
+ steps,
+ sub,
+ transitionEnd
+ } = this;
+
+ if (sub.settings.verbose) {
+ console.log("_removeElement");
+ }
+
+ /* Remove this word */
+ invisible.removeEventListener(transitionEnd, steps[0], false);
+ sub.wrapper.removeChild(word);
+ }
+
+ /**
+ * Set the text of this element to ctx.newText and fade it in.
+ */
+ _setTextAndFadeIn() {
+ const {
+ "ctx": {
+ visible,
+ invisible,
+ newText
+ },
+ steps,
+ sub,
+ animatingClass,
+ transitionEnd
+ } = this;
+
+ if (sub.settings.verbose) {
+ console.log("_setTextAndFadeIn");
+ }
+
+ /* Sub the text then fade in */
+ invisible.classList.remove(animatingClass);
+ visible.classList.add(animatingClass);
+
+ invisible.removeEventListener(transitionEnd, steps[0], false);
+
+ steps.shift(); // pop _setTextAndFadeIn
+
+ visible.addEventListener(transitionEnd, steps[0], false);
+
+ visible.innerHTML = newText;
+ invisible.innerHTML = newText;
+ visible.style.opacity = 1;
+ }
+
+ /**
+ * Remove animation classes, remove event listeners, and set widths to "auto"
+ */
+ _cleanUp() {
+ const {
+ "ctx": {
+ visible,
+ invisible
+ },
+ steps,
+ sub,
+ animatingClass,
+ transitionEnd
+ } = this;
+
+ if (sub.settings.verbose) {
+ console.log("_cleanUp");
+ }
+
+ /* Clean Up */
+ invisible.classList.remove(animatingClass);
+ visible.classList.remove(animatingClass);
+
+ visible.removeEventListener(transitionEnd, steps[0], false);
+ invisible.style.width = "auto";
+ }
+
+ /**
+ * Find the width that an element with a given tag and classes would have if
+ * it contained the passed text.
+ *
+ * @param {string} text - the text to get the width of
+ * @param {string} tag - the tag that the text will be put in
+ * @param {string[]} classes - an array of classes associated with this
+ * element.
+ */
+ _calculateWordWidth(text, tag, classes = []) {
+ const elem = document.createElement(tag);
+
+ classes.push(this.sub.settings.namespace + "-text-width-calculation");
+
+ elem.setAttribute("class", classes.join(" "));
+ elem.innerHTML = text;
+
+ document.body.appendChild(elem);
+ /* Get a decimal number of the form 12.455 */
+ const width = parseFloat(window.getComputedStyle(elem, null).width);
+
+ elem.parentNode.removeChild(elem);
+
+ return width;
+ }
}
/***************************************************************************
@@ -255,815 +563,760 @@
* @param {bool} options._testing - true if testing. sentences will be
* ignored
*/
- function Sub(rawSentences, options) {
- var self = this;
- var opts = options || {};
- self.settings = {
- containerId: opts.containerId || "sub",
- namespace: opts.namespace || "sub",
- interval: opts.interval || 5000,
- speed: opts.speed || 200,
- mobileWidth: opts.mobileWidth || null,
- verbose: (opts.verbose !== undefined) ? opts.verbose : false,
- random: (opts.random !== undefined) ? opts.random : false,
- best: (opts.best !== undefined) ? opts.best : true,
- clearOriginalContent: (opts.clearOriginalContent !== undefined) ? opts.clearOriginalContent : true,
- _testing: (opts._testing !== undefined) ? opts._testing : false,
- };
- self.wrapper = document.getElementById(self.settings.containerId);
- var wrapperStyle = window.getComputedStyle(self.wrapper);
-
- _injectStyle(
- self.settings.namespace,
- self.settings.speed / 1000,
- wrapperStyle.height,
- wrapperStyle.fontFamily);
-
- self.highestTimeoutId = 0;
- self.currentState = null;
- self.actions = [];
- self.invisibleClass = " ." + self.settings.namespace + "-invisible";
- self.visibleClass = " ." + self.settings.namespace + "-visible";
- self.fromClass = self.settings.namespace + "-from-idx-";
- self.toClass = self.settings.namespace + "-to-idx-";
- self.wrapperSelector = "#" + self.settings.namespace;
- self.isEmpty = true;
-
- self._setupContainer();
- if (!self.settings._testing) {
- self._setSentences(self._parseSentences(rawSentences));
+ class Sub {
+ constructor(rawSentences, opts = {}) {
+ const settings = {
+ containerId: opts.containerId || "sub",
+ namespace: opts.namespace || "sub",
+ interval: opts.interval || 5000,
+ speed: opts.speed || 200,
+ mobileWidth: opts.mobileWidth || null,
+ verbose: (opts.verbose !== undefined) ? opts.verbose : false,
+ random: (opts.random !== undefined) ? opts.random : false,
+ best: (opts.best !== undefined) ? opts.best : true,
+ clearOriginalContent: (opts.clearOriginalContent !== undefined) ? opts.clearOriginalContent : true,
+ _testing: (opts._testing !== undefined) ? opts._testing : false,
+ };
+
+ this.wrapper = document.getElementById(settings.containerId);
+ this.settings = settings;
+
+ const namespace = settings.namespace;
+ const wrapperStyle = window.getComputedStyle(this.wrapper);
+
+ _injectStyle(
+ namespace,
+ settings.speed / 1000,
+ wrapperStyle.height,
+ wrapperStyle.fontFamily
+ );
+
+ this.highestTimeoutId = 0;
+ this.currentState = null;
+ this.actions = [];
+ this.invisibleClass = " ." + namespace + "-invisible";
+ this.visibleClass = " ." + namespace + "-visible";
+ this.fromClass = namespace + "-from-idx-";
+ this.toClass = namespace + "-to-idx-";
+ this.wrapperSelector = "#" + namespace;
+ this.isEmpty = true;
+
+ this._setupContainer();
+
+ if (!settings._testing) {
+ this._setSentences(this._parseSentences(rawSentences));
+ }
}
- return self;
- }
- /**
- * Parse the array of raw sentence strings into an array of arrays of words.
- *
- * @param {string[]} rawSentences the sentences to parse
- * @returns {string[][]} sentences the
- */
- Sub.prototype._parseSentences = function(rawSentences) {
- if (!rawSentences || typeof rawSentences !== "object") {
- throw "rawSentences must be an array of strings.";
+ /**
+ * Parse the array of raw sentence strings into an array of arrays of words.
+ *
+ * @param {string[]} rawSentences the sentences to parse
+ * @returns {string[][]} sentences the
+ */
+ _parseSentences(rawSentences) {
+ if (!rawSentences || typeof rawSentences !== "object") {
+ throw "rawSentences must be an array of strings.";
+ }
+
+ return rawSentences.map(_parseSentence);
}
- return rawSentences.map(_parseSentence);
- };
- /**
- * Find the container for the sentences, empty out any HTML that might be
- * inside, and then give it the namespace class. It will be the root element
- * for any changes we might make.
- */
- Sub.prototype._setupContainer = function() {
- var self = this;
- var container = document.getElementById(self.settings.containerId);
- if (!container) {
- throw "Cannot find element with id:" + self.settings.containerId;
+ /**
+ * Find the container for the sentences, empty out any HTML that might be
+ * inside, and then give it the namespace class. It will be the root element
+ * for any changes we might make.
+ */
+ _setupContainer() {
+ const {
+ containerId,
+ clearOriginalContent,
+ namespace
+ } = this.settings;
+
+ const container = document.getElementById(containerId);
+
+ if (!container) {
+ throw "Cannot find element with id:" + containerId;
+ }
+
+ const originalStyle = window.getComputedStyle(container);
+
+ container.style.height = originalStyle.height;
+
+ if (clearOriginalContent) {
+ container.innerHTML = "";
+ } else {
+ container.style.width = originalStyle.width;
+ container.innerHTML = `${container.innerHTML.replace(" ", NBSP)}`;
+ }
+
+ container.className = namespace;
}
- var originalStyle = window.getComputedStyle(container);
- container.style.height = originalStyle.height;
- if (self.settings.clearOriginalContent) {
- container.innerHTML = '';
- } else {
- container.style.width = originalStyle.width;
- container.innerHTML = '' + container.innerHTML.replace(' ', ' ') + '';
+
+ _getOnResize() {
+ this.isStopped = false;
+
+ const onResize = () => {
+ this.lastWindowWidth = window.innerWidth;
+
+ // Disable on small screens, if that parameter is provided.
+ if (this.settings.mobileWidth !== null) {
+ if (!this.isStopped && this.lastWindowWidth < this.settings.mobileWidth) {
+ // stop on small screens
+ this._stop();
+ this.isStopped = true;
+ } else
+ if (this.isStopped && this.lastWindowWidth > this.settings.mobileWidth) {
+ // start up again
+ this._run();
+ this.isStopped = false;
+ }
+ }
+ };
+
+ return onResize;
}
- container.className = self.settings.namespace;
- };
- Sub.prototype._getOnResize = function() {
- var self = this;
- self.isStopped = false;
- var onResize = function(e) {
- self.lastWindowWidth = window.innerWidth;
-
- // Disable on small screens, if that parameter is provided.
- if (self.settings.mobileWidth !== null) {
- if (!self.isStopped && self.lastWindowWidth < self.settings.mobileWidth) {
- // stop on small screens
- self._stop();
- self.isStopped = true;
- } else
- if (self.isStopped && self.lastWindowWidth > self.settings.mobileWidth) {
- // start up again
- self._run();
- self.isStopped = false;
+ /**
+ * Run the sentence loop. If we haven't successfully populated self.actions,
+ * we delay the running until we have.
+ *
+ * This function should only be called internally.
+ */
+ _run() {
+ const {
+ actions,
+ wrapper,
+ "settings": {
+ namespace,
+ interval
}
+ } = this;
+
+ // We haven't finished generating self.actions yet, so delay running
+ if (!this.actions) {
+ setTimeout(function() {
+ this.run();
+ }, 20);
+ return;
}
- };
- return onResize;
- }
- /**
- * Run the sentence loop. If we haven't successfully populated self.actions,
- * we delay the running until we have.
- *
- * This function should only be called internally.
- */
- Sub.prototype._run = function() {
- var self = this;
-
- // We haven't finished generating self.actions yet, so delay running
- if (!self.actions) {
- setTimeout(function() {
- self.run();
- }, 20);
- return;
- }
+ if (this.isEmpty) {
+ this.isEmpty = false;
- if (self.isEmpty) {
- self.isEmpty = false;
- var action = self._computeActionsToChange([], self.actions[0].from);
- if (!action) {
- console.log(action);
- throw "returned null action";
+ const action = this._computeActionsToChange([], actions[0].from);
+
+ if (!action) {
+ console.log(action);
+
+ throw "returned null action";
+ }
+
+ this._applyAction(action);
}
- self._applyAction(action);
- }
- self.highestTimeoutId = setTimeout(function() {
- self.wrapper.className += ' ' + self.settings.namespace + '-loaded';
- self.wrapper.style.height = '';
- self._sentenceLoop();
- }, self.settings.interval);
- }
- /**
- * Run the sentence loop and add resize handlers. If we haven't successfully
- * populated self.actions, we delay the running until we have.
- */
- Sub.prototype.run = function() {
- var self = this;
+ this.highestTimeoutId = setTimeout(() => {
+ wrapper.classList.add(namespace + "-loaded");
+ wrapper.style.height = "";
- self.onResize = self._getOnResize();
- window.addEventListener('resize', self.onResize, false);
- window.addEventListener('orientationchange', self.onResize, false);
+ this._sentenceLoop();
+ }, interval);
+ }
- self._run();
+ /**
+ * Run the sentence loop and add resize handlers. If we haven't successfully
+ * populated self.actions, we delay the running until we have.
+ */
+ run() {
+ this.onResize = this._getOnResize();
- return self;
- };
+ window.addEventListener("resize", this.onResize, false);
+ window.addEventListener("orientationchange", this.onResize, false);
- /**
- * Stop the sentence loop. This will stop all animations.
- *
- * This function should only be called internally.
- */
- Sub.prototype._stop = function() {
- var self = this;
+ this._run();
- clearTimeout(self.highestTimeoutId);
- }
+ return this;
+ }
- /**
- * Stop the sentence loop. This will stop all animations and remove event
- * listeners.
- */
- Sub.prototype.stop = function() {
- var self = this;
+ /**
+ * Stop the sentence loop. This will stop all animations.
+ *
+ * This function should only be called internally.
+ */
+ _stop() {
+ var self = this;
- window.removeEventListener('resize', self.onResize, false);
- window.removeEventListener('orientationchange', self.onResize, false);
+ clearTimeout(self.highestTimeoutId);
+ }
- self._stop();
+ /**
+ * Stop the sentence loop. This will stop all animations and remove event
+ * listeners.
+ */
+ stop() {
+ window.removeEventListener("resize", this.onResize, false);
+ window.removeEventListener("orientationchange", this.onResize, false);
- return self;
- }
+ this._stop();
- /**
- * Compute the actions required to transform `from` into `to`.
- *
- * Example:
- * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."]
- * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"]
- * output:
- * {
- * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."],
- * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"],
- * sub:[
- * { fromWord: "fox", toWord: "color", fromIndex: 3, toIndex: 2 },
- * { fromWord: "cool", toWord: "very", fromIndex: 6, toIndex: 5 },
- * { fromWord: "supposedly", toWord: "no", fromIndex: 8, toIndex: 8 },
- * { fromWord: ".", toWord: "?", fromIndex: 9, toIndex: 9 } ],
- * remove: [
- * { fromWord: "quick", fromIndex: 1 } ],
- * insert: [
- * { toWord: "pretty", toIndex: 6 } ],
- * keep: [
- * { fromWord: "The", toWord: "The", fromIndex: 0, toIndex: 0 },
- * { fromWord: "brown", toWord: "brown", fromIndex: 2, toIndex: 1 },
- * { fromWord: "is", toWord: "is", fromIndex: 4, toIndex: 3 },
- * { fromWord: "very", toWord: "very", fromIndex: 5, toIndex: 4 },
- * { fromWord: ",", toWord: ",", fromIndex: 7, toIndex: 7 } ],
- * cost: 6
- * }
- *
- * @param {string[]} from - the sentence to change from
- * @param {string[]} to - the sentence to change to
- *
- * @returns {object} actions - comamnds to perform
- * @returns {string[]} actions.from - the from sentence
- * @returns {string[]} actions.to - the to sentence
- * @returns {object[]} actions.sub - substitutions to do
- * @returns {string} actions.sub.fromWord - word to sub
- * @returns {string} actions.sub.toWord - word to sub with
- * @returns {int} actions.sub.fromIndex - index of word to sub
- * @returns {int} actions.sub.toIndex - index of word to sub with
- * @returns {object[]} actions.remove - removals to do
- * @returns {string} actions.remove.fromWord - word to remove
- * @returns {int} actions.remove.fromIndex - index of word to remove
- * @returns {object[]} actions.insert - insertions to do
- * @returns {string} actions.insert.toWord - word to insert
- * @returns {int} actions.insert.toIndex - index of word to insert
- * @returns {object[]} actions.keep - words to keep (no-ops)
- * @returns {string} actions.keep.fromWord - word to keep (from)
- * @returns {string} actions.keep.toWord - word to keep (to)
- * @returns {int} actions.keep.fromIndex - index in from of word to keep
- * @returns {int} actions.keep.toIndex - index in to of word to keep
- * @returns {int} actions.cost - total cost of action =
- * removals + substitutions + insertions
- */
- Sub.prototype._computeActionsToChange = function(from, to) {
- var self = this;
- if (self.settings.verbose) { console.log("_computeActionsToChange: ", from, to); }
- var actions = {
- from: from,
- to: to,
- sub: [],
- remove: [],
- insert: [],
- keep: [],
- cost: 0
- };
+ return this;
+ }
/**
- * Recursively creates `actions`, given a start index for each sentence
+ * Compute the actions required to transform `from` into `to`.
*
- * @param {int} fromIndex - index of first word to consider in from sentence
- * @param {int} toIndex - index of first word to consider in to sentence
- * @param {bool} lookAhead - true if we are looking ahead at other
- * possible solutions. Actions will not be
- * modified. false if actions should be modified.
- * @returns {int} cost - the recursively built cost of actions to take.
+ * Example:
+ * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."]
+ * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"]
+ * output:
+ * {
+ * from: ["The", "quick", "brown", "fox", "is", "very", "cool", ",", "supposedly", "."],
+ * to: ["The", "brown", "color", "is", "very", "very", "pretty", ",", "no", "?"],
+ * sub:[
+ * { fromWord: "fox", toWord: "color", fromIndex: 3, toIndex: 2 },
+ * { fromWord: "cool", toWord: "very", fromIndex: 6, toIndex: 5 },
+ * { fromWord: "supposedly", toWord: "no", fromIndex: 8, toIndex: 8 },
+ * { fromWord: ".", toWord: "?", fromIndex: 9, toIndex: 9 } ],
+ * remove: [
+ * { fromWord: "quick", fromIndex: 1 } ],
+ * insert: [
+ * { toWord: "pretty", toIndex: 6 } ],
+ * keep: [
+ * { fromWord: "The", toWord: "The", fromIndex: 0, toIndex: 0 },
+ * { fromWord: "brown", toWord: "brown", fromIndex: 2, toIndex: 1 },
+ * { fromWord: "is", toWord: "is", fromIndex: 4, toIndex: 3 },
+ * { fromWord: "very", toWord: "very", fromIndex: 5, toIndex: 4 },
+ * { fromWord: ",", toWord: ",", fromIndex: 7, toIndex: 7 } ],
+ * cost: 6
+ * }
+ *
+ * @param {string[]} from - the sentence to change from
+ * @param {string[]} to - the sentence to change to
+ *
+ * @returns {object} actions - comamnds to perform
+ * @returns {string[]} actions.from - the from sentence
+ * @returns {string[]} actions.to - the to sentence
+ * @returns {object[]} actions.sub - substitutions to do
+ * @returns {string} actions.sub.fromWord - word to sub
+ * @returns {string} actions.sub.toWord - word to sub with
+ * @returns {int} actions.sub.fromIndex - index of word to sub
+ * @returns {int} actions.sub.toIndex - index of word to sub with
+ * @returns {object[]} actions.remove - removals to do
+ * @returns {string} actions.remove.fromWord - word to remove
+ * @returns {int} actions.remove.fromIndex - index of word to remove
+ * @returns {object[]} actions.insert - insertions to do
+ * @returns {string} actions.insert.toWord - word to insert
+ * @returns {int} actions.insert.toIndex - index of word to insert
+ * @returns {object[]} actions.keep - words to keep (no-ops)
+ * @returns {string} actions.keep.fromWord - word to keep (from)
+ * @returns {string} actions.keep.toWord - word to keep (to)
+ * @returns {int} actions.keep.fromIndex - index in from of word to keep
+ * @returns {int} actions.keep.toIndex - index in to of word to keep
+ * @returns {int} actions.cost - total cost of action =
+ * removals + substitutions + insertions
*/
- var __computeActionsToCange = function(fromIndex, toIndex, lookAhead) {
- var i;
- lookAhead = lookAhead || false;
-
- // End of from list
- if (fromIndex >= from.length) {
- if (!lookAhead) {
- for (i = toIndex; i < to.length; i++) {
- actions.insert.push({
- toWord: to[i],
- toIndex: i
- });
+ _computeActionsToChange(from, to) {
+ if (this.settings.verbose) {
+ console.log("_computeActionsToChange: ", from, to);
+ }
+
+ const actions = {
+ from: from,
+ to: to,
+ sub: [],
+ remove: [],
+ insert: [],
+ keep: [],
+ cost: 0
+ };
+
+ /**
+ * Recursively creates `actions`, given a start index for each sentence
+ *
+ * @param {int} fromIndex - index of first word to consider in from sentence
+ * @param {int} toIndex - index of first word to consider in to sentence
+ * @param {bool} lookAhead - true if we are looking ahead at other
+ * possible solutions. Actions will not be
+ * modified. false if actions should be modified.
+ * @returns {int} cost - the recursively built cost of actions to take.
+ */
+ const __computeActionsToCange = (fromIndex, toIndex, lookAhead = false) => {
+ // End of from list
+ if (fromIndex >= from.length) {
+ if (!lookAhead) {
+ actions.insert = actions.insert.concat(to.slice(toIndex).map((x, i) => {
+ return {
+ toWord: x,
+ toIndex: i + toIndex
+ };
+ }));
}
+ // base case, each insert costs 1
+ return to.length - toIndex;
}
- // base case, each insert costs 1
- return to.length - toIndex;
- }
- // End of to list
- if (toIndex >= to.length) {
- if (!lookAhead) {
- for (i = fromIndex; i < from.length; i++) {
- actions.remove.push({
- fromWord: from[i],
- fromIndex: i
- });
+ // End of to list
+ if (toIndex >= to.length) {
+ if (!lookAhead) {
+ actions.remove = actions.remove.concat(from.slice(fromIndex).map((x, i) => {
+ return {
+ fromWord: x,
+ fromIndex: i + fromIndex
+ };
+ }));
}
+ // base case, each remove costs 1
+ return from.length - toIndex;
}
- // base case, each remove costs 1
- return from.length - toIndex;
- }
- // Easy Case: a match!
- if (from[fromIndex] === to[toIndex]) {
+ // Easy Case: a match!
+ if (from[fromIndex] === to[toIndex]) {
+ if (lookAhead) {
+ return 0;
+ }
+
+ actions.keep.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+
+ // keep is free
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1);
+ }
+
+ const foundIndex = from.indexOf(to[toIndex], fromIndex);
+
if (lookAhead) {
- return 0;
+ return foundIndex;
}
- actions.keep.push({
- fromWord: from[fromIndex],
- toWord: to[toIndex],
- fromIndex: fromIndex,
- toIndex: toIndex
- });
- // keep is free
- return __computeActionsToCange(fromIndex + 1, toIndex + 1);
- }
- var foundIndex = from.indexOf(to[toIndex], fromIndex);
+ if (fromIndex + 1 == from.length) {
+ // Can't look ahead, make a move now
+ if (foundIndex === -1) {
+ actions.sub.push({
+ fromWord: from[fromIndex],
+ toWord: to[toIndex],
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+ // Sub costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
+ }
+ }
- if (lookAhead) {
- return foundIndex;
- }
+ const futureIndex = __computeActionsToCange(fromIndex, toIndex + 1, true);
+
+ if (foundIndex === -1) {
+ if (futureIndex === 0) {
+ actions.insert.push({
+ toWord: to[toIndex],
+ toIndex: toIndex
+ });
+ // insert costs 1
+ return __computeActionsToCange(fromIndex, toIndex + 1) + 1;
+ }
- if (fromIndex + 1 == from.length) {
- // Can't look ahead, make a move now
- if(foundIndex === -1) {
actions.sub.push({
fromWord: from[fromIndex],
toWord: to[toIndex],
fromIndex: fromIndex,
toIndex: toIndex
});
- // Sub costs 1
+ // sub costs 1
return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
}
- }
- var futureIndex = __computeActionsToCange(fromIndex, toIndex + 1, true);
+ if (foundIndex === fromIndex + 1 && futureIndex === fromIndex || foundIndex === futureIndex) {
+ const fromLeft = from.length - fromIndex;
+ const toLeft = to.length - toIndex;
- if (foundIndex === -1) {
- if (futureIndex === 0) {
- actions.insert.push({
- toWord: to[toIndex],
- toIndex: toIndex
+ if (fromLeft > toLeft) {
+ actions.insert.push({
+ toWord: to[toIndex],
+ toIndex: toIndex
+ });
+ // Insert costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex) + 1;
+ }
+
+ // toLeft >= fromLeft
+ actions.remove.push({
+ fromWord: from[fromIndex],
+ fromIndex: fromIndex
});
- // insert costs 1
+ // remove costs 1
return __computeActionsToCange(fromIndex, toIndex + 1) + 1;
}
- actions.sub.push({
- fromWord: from[fromIndex],
- toWord: to[toIndex],
- fromIndex: fromIndex,
- toIndex: toIndex
- });
- // sub costs 1
- return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
- }
- if (foundIndex === fromIndex + 1 && futureIndex === fromIndex || foundIndex === futureIndex) {
- var fromLeft = from.length - fromIndex;
- var toLeft = to.length - toIndex;
-
- if (fromLeft > toLeft) {
- actions.insert.push({
+ if (foundIndex > futureIndex && futureIndex !== -1) {
+ actions.sub.push({
+ fromWord: from[fromIndex],
toWord: to[toIndex],
+ fromIndex: fromIndex,
toIndex: toIndex
});
- // Insert costs 1
- return __computeActionsToCange(fromIndex + 1, toIndex) + 1;
+ // Sub costs 1
+ return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
}
- // toLeft >= fromLeft
- actions.remove.push({
- fromWord: from[fromIndex],
- fromIndex: fromIndex
- });
- // remove costs 1
- return __computeActionsToCange(fromIndex, toIndex + 1) + 1;
- }
+ // from.slice
+ // foundIndex < futureIndex
+
+ actions.remove = actions.remove.concat(from.slice(fromIndex, foundIndex).map((x, i) => {
+ return {
+ fromWord: x,
+ fromIndex: i + fromIndex
+ };
+ }));
- if (foundIndex > futureIndex && futureIndex !== -1 ) {
- actions.sub.push({
- fromWord: from[fromIndex],
+ actions.keep.push({
+ fromWord: from[foundIndex],
toWord: to[toIndex],
- fromIndex: fromIndex,
+ fromIndex: foundIndex,
toIndex: toIndex
});
- // Sub costs 1
- return __computeActionsToCange(fromIndex + 1, toIndex + 1) + 1;
- }
- // foundIndex < futureIndex
- for (i = fromIndex; i < foundIndex; i++) {
- actions.remove.push({
- fromWord: from[i],
- fromIndex: i
- });
- }
- actions.keep.push({
- fromWord: from[foundIndex],
- toWord: to[toIndex],
- fromIndex: foundIndex,
- toIndex: toIndex
- });
- // Each remove costs 1, the keep is free
- return __computeActionsToCange(foundIndex + 1, toIndex + 1) + (foundIndex - fromIndex);
- };
+ // Each remove costs 1, the keep is free
+ return __computeActionsToCange(foundIndex + 1, toIndex + 1) + (foundIndex - fromIndex);
+ };
- // Initalize the recursive call, the final result is the cost.
- actions.cost = __computeActionsToCange(0, 0);
- return actions;
- };
+ // Initalize the recursive call, the final result is the cost.
+ actions.cost = __computeActionsToCange(0, 0);
- /**
- * Generate self.actions. If self.settings.best is true, we order the
- * actions to rotate between sentences with minimal insertions, removals, and
- * changes. If self.settings.random is true, the sentences will appear in a
- * random order. If both are set, the sequence will be optimal, but will
- * start from a random position in the sequence.
- *
- * @param {string[][]} sentences - sentences to be converted to actions
- */
- Sub.prototype._setSentences = function(sentences) {
- var self = this;
- var i, j, prevIndex;
- if (sentences.length === 0) {
- self.actions = [];
+ return actions;
}
- if (self.settings.best) {
- /* Because who says the Traveling Salesman Problem isn't releveant? */
-
- // compute a table of values table[fromIndex][toIndex] = {
- // fromIndex: fromIndex,
- // toIndex: toIndex,
- // action: the action from sentences[fromIndex] to sentences[toIndex]
- // }
- var table = sentences.map(function(from, fromIndex) {
- return sentences.map(function(to, toIndex) {
- if (fromIndex === toIndex) {
+ /**
+ * Generate self.actions. If self.settings.best is true, we order the
+ * actions to rotate between sentences with minimal insertions, removals, and
+ * changes. If self.settings.random is true, the sentences will appear in a
+ * random order. If both are set, the sequence will be optimal, but will
+ * start from a random position in the sequence.
+ *
+ * @param {string[][]} sentences - sentences to be converted to actions
+ */
+ _setSentences(sentences) {
+ const sensLen = sentences.length;
+
+ if (sentences.length === 0) {
+ this.actions = [];
+ }
+
+ if (this.settings.best) {
+ /* Because who says the Traveling Salesman Problem isn't releveant? */
+
+ // compute a table of values table[fromIndex][toIndex] = {
+ // fromIndex: fromIndex,
+ // toIndex: toIndex,
+ // action: the action from sentences[fromIndex] to sentences[toIndex]
+ // }
+ const table = sentences.map((from, fromIndex) => {
+ return sentences.map((to, toIndex) => {
+ if (fromIndex === toIndex) {
+ return {
+ action: {
+ cost: Number.MAX_VALUE
+ },
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ };
+ }
+
+ const action = this._computeActionsToChange(sentences[fromIndex], sentences[toIndex]);
+
return {
- action: { cost: Number.MAX_VALUE },
+ action: action,
fromIndex: fromIndex,
toIndex: toIndex
};
- }
- var action = self._computeActionsToChange(sentences[fromIndex],
- sentences[toIndex]);
- return {
- action: action,
- fromIndex: fromIndex,
- toIndex: toIndex
- };
+ });
});
- });
- var usedFromIndexes = [];
- var from = 0;
-
- // sort each rows by cost, then sort the rows by lowest cost in that row
- table.sort(function(row1, row2) {
- row1.sort(_sortAnnotatedAction);
- row2.sort(_sortAnnotatedAction);
- return row1[0].cost - row2[0].cost;
- });
- var first = table[0][0].fromIndex;
-
- // Start with table[0][0], the lowest cost action. Then, find the lowest
- // cost actions starting from table[0][0].toIndex, and so forth.
- for (i = 0; i < sentences.length; i++) {
- for (j = 0; j < sentences.length; j++) {
- if ((i === sentences.length - 1 && table[from][j].toIndex === first) ||
- (i !== sentences.length - 1 && usedFromIndexes.indexOf(table[from][j].toIndex) === -1)) {
- self.actions.push(table[from][j].action);
- usedFromIndexes.push(from);
- from = table[from][j].toIndex;
- break;
+ // sort each rows by cost, then sort the rows by lowest cost in that row
+ table.sort((row1, row2) => {
+ row1.sort(_sortAnnotatedAction);
+ row2.sort(_sortAnnotatedAction);
+
+ return row1[0].cost - row2[0].cost;
+ });
+
+ let from = 0;
+ let i = 0;
+
+ const usedFromIndexes = [];
+ const first = table[0][0].fromIndex;
+
+ // Start with table[0][0], the lowest cost action. Then, find the lowest
+ // cost actions starting from table[0][0].toIndex, and so forth.
+ for (i = 0; i < sensLen; i++) {
+ for (let j = 0; j < sensLen; j++) {
+ if ((i === sensLen - 1 && table[from][j].toIndex === first) ||
+ (i !== sensLen - 1 && usedFromIndexes.indexOf(table[from][j].toIndex) === -1)) {
+
+ this.actions.push(table[from][j].action);
+ usedFromIndexes.push(from);
+
+ from = table[from][j].toIndex;
+
+ break;
+ }
}
}
- }
- if(self.settings.random) {
- // start from somewhere other than the beginning.
- var start = Math.floor(Math.random() * (sentences.length));
- for (i = 0; i < start; i++) {
- self.actions.push(self.actions.shift());
+ if (this.settings.random) {
+ // start from somewhere other than the beginning.
+ const start = Math.floor(Math.random() * (sensLen));
+
+ for (i = 0; i < start; i++) {
+ this.actions.push(this.actions.shift());
+ }
}
- }
- } else {
+ } else {
+ const sens = this.settings.random ? shuffleArr(sentences) : sentences;
- if (self.settings.random) {
- // shuffle the sentences
- sentences.sort(function() { return 0.5 - Math.random(); });
- }
+ this.actions = this.actions.concat(sens.map((x, i) => {
+ const prevIndex = (i === 0) ? (sensLen - 1) : i - 1;
- for (i = 0; i < sentences.length; i++) {
- prevIndex = (i === 0) ? (sentences.length - 1) : i - 1;
- self.actions.push(self._computeActionsToChange(sentences[prevIndex],
- sentences[i]));
+ return this._computeActionsToChange(sens[prevIndex], x);
+ }));
}
}
- };
- /**
- * Called in an infinite setTimeout loop. Dequeues an action, performs it,
- * and enqueues it onto the end of the self.actions array.
- * Then calls setTimeout on itself, with self.settings.interval.
- */
- Sub.prototype._sentenceLoop = function() {
- var self = this;
- var nextAction = self.actions.shift();
- if (!nextAction) {
- console.log(nextAction, self.actions);
- throw "returned null action";
+ /**
+ * Called in an infinite setTimeout loop. Dequeues an action, performs it,
+ * and enqueues it onto the end of the self.actions array.
+ * Then calls setTimeout on itself, with self.settings.interval.
+ */
+ _sentenceLoop() {
+ const {
+ actions,
+ "settings": {
+ interval
+ }
+ } = this;
+
+ const nextAction = this.actions.shift();
+
+ if (!nextAction) {
+ console.log(nextAction, actions);
+ throw "returned null action";
+ }
+
+ this._applyAction(nextAction);
+ actions.push(nextAction);
+
+ clearTimeout(this.highestTimeoutId);
+
+ this.highestTimeoutId = setTimeout(() => {
+ this._sentenceLoop();
+ }, interval);
}
- self._applyAction(nextAction);
- self.actions.push(nextAction);
- clearTimeout(self.highestTimeoutId);
- self.highestTimeoutId = setTimeout(function() {
- self._sentenceLoop();
- }, self.settings.interval);
- };
- /**
- * Apply `action`, by performing the necessary substitutions, removals, keeps,
- * and insertions.
- */
- Sub.prototype._applyAction = function(action) {
- var self = this;
- var words = document.getElementsByClassName(self.settings.namespace + '-word');
- [].forEach.call(words, function(elem) {
- if (self.settings.verbose) { console.log('replacing to- with from- for:', elem)}
- elem.className = elem.className.replace(self.toClass, self.fromClass);
- });
- action.sub.map(function(subAction) {
- self._subAction(subAction);
- });
- action.remove.map(function(removeAction) {
- self._removeAction(removeAction);
- });
- action.keep.map(function(keepAction) {
- self._keepAction(keepAction);
- });
- self._performInsertions(action.insert);
- };
+ /**
+ * Removes the word from the sentence.
+ *
+ * @param {Object} removeAction - the removal to perform
+ * @param {int} removeAction.fromIndex - the index of the existing word
+ */
+ _removeAction(removeAction) {
+ const {
+ fromClass,
+ wrapperSelector,
+ visibleClass,
+ invisibleClass,
+ "settings": {
+ verbose
+ }
+ } = this;
- /**
- * Removes the word from the sentence.
- *
- * @param {Object} removeAction - the removal to perform
- * @param {int} removeAction.fromIndex - the index of the existing word
- */
- Sub.prototype._removeAction = function(removeAction) {
- var self = this;
- var fromIndexClass = self.fromClass + removeAction.fromIndex;
- var animationContext = {
- fromIndexClass: fromIndexClass,
- word: document.querySelector(self.wrapperSelector + " ." + fromIndexClass),
- visible: document.querySelector(self.wrapperSelector + " ." + fromIndexClass + self.visibleClass),
- invisible: document.querySelector(self.wrapperSelector + " ." + fromIndexClass + self.invisibleClass),
- newText: "" // We'll animate to zero width
- };
- if (self.settings.verbose) { console.log("remove", animationContext); }
- new Animation("remove", self, animationContext);
- };
+ var fromIndexClass = fromClass + removeAction.fromIndex;
- /**
- * Perform the given insertions
- *
- * @param {Object[]} insertions - the insertions to perform
- * @param {int} insertions.toIndex - the index of the element to add
- * @param {string} insertions.toWord - the word to insert
- */
- Sub.prototype._performInsertions = function(insertions) {
- var self = this;
- setTimeout(function () {
- insertions.forEach(function(insertAction) {
-
- /* Insert new node (no text yet) */
- var html = _wordTemplate(self.settings.namespace, insertAction.toIndex);
- if (insertAction.toIndex === 0) {
- self.wrapper.insertAdjacentHTML("afterbegin", html);
- } else {
- var selector = self.wrapperSelector + " ." + self.toClass + (insertAction.toIndex - 1);
- var prevSibling = document.querySelector(selector);
- prevSibling.insertAdjacentHTML("afterend", html);
+ var animationContext = {
+ fromIndexClass: fromIndexClass,
+ word: document.querySelector(`${wrapperSelector} .${fromIndexClass}`),
+ visible: document.querySelector(`${wrapperSelector} .${fromIndexClass}${visibleClass}`),
+ invisible: document.querySelector(`${wrapperSelector} .${fromIndexClass}${invisibleClass}`),
+ newText: "" // We'll animate to zero width
+ };
+
+ if (verbose) {
+ console.log("remove", animationContext);
+ }
+
+ new Animation("remove", this, animationContext);
+ }
+
+ /**
+ * Perform the given insertions
+ *
+ * @param {Object[]} insertions - the insertions to perform
+ * @param {int} insertions.toIndex - the index of the element to add
+ * @param {string} insertions.toWord - the word to insert
+ */
+ _performInsertions(insertions) {
+ const {
+ toClass,
+ wrapper,
+ wrapperSelector,
+ visibleClass,
+ invisibleClass,
+ "settings": {
+ namespace,
+ speed,
+ verbose
}
+ } = this;
- /* Startup animations */
- var toIndexClass = self.toClass + insertAction.toIndex;
- var animationContext = {
- toIndexClass: toIndexClass,
- word: document.querySelector(self.wrapperSelector + " ." + toIndexClass),
- visible: document.querySelector(self.wrapperSelector + " ." + toIndexClass + self.visibleClass),
- invisible: document.querySelector(self.wrapperSelector + " ." + toIndexClass + self.invisibleClass),
- newText: insertAction.toWord
- };
-
- if (self.settings.verbose) { console.log("insert", animationContext); }
- new Animation("insert", self, animationContext);
- });
- }, self.settings.speed);
- };
+ setTimeout(() => {
+ insertions.forEach(insertAction => {
- /**
- * Perform the given substitution
- *
- * @param {Object} subAction - the substitution to perform
- * @param {int} subAction.fromIndex - the index of the element to change
- * @param {string} subAction.fromWord - the word to sub
- * @param {int} subAction.toIndex - the index to give the new word
- * @param {string} subAction.toWord - the word to sub with
- */
- Sub.prototype._subAction = function(subAction) {
- var self = this;
- var fromIndexClass = self.fromClass + subAction.fromIndex;
- var animationContext = {
- fromIndexClass: fromIndexClass,
- toIndexClass: self.toClass + subAction.toIndex,
- word: document.querySelector(self.wrapperSelector + " ." + fromIndexClass),
- visible: document.querySelector(self.wrapperSelector + " ." + fromIndexClass + self.visibleClass),
- invisible: document.querySelector(self.wrapperSelector + " ." + fromIndexClass + self.invisibleClass),
- newText: subAction.toWord
- };
- if (self.settings.verbose) { console.log("sub", animationContext); }
- new Animation("sub", self, animationContext);
- };
+ /* Insert new node (no text yet) */
+ const html = _wordTemplate(namespace, insertAction.toIndex);
- /**
- * Perform the given keep action.
- *
- * @param {Object} keepAction - the keep action to perform
- * @param {int} keepAction.fromIndex - the index of the word to re-label
- * @param {int} keepAction.toIndex - the index to label this word
- */
- Sub.prototype._keepAction = function(keepAction) {
- var self = this;
- var fromIndexClass = self.fromClass + keepAction.fromIndex;
- var animationContext = {
- fromIndexClass: fromIndexClass,
- toIndexClass: self.toClass + keepAction.toIndex,
- word: document.querySelector(self.wrapperSelector + " ." + fromIndexClass),
- };
-
- if (self.settings.verbose) { console.log("keep", animationContext); }
- new Animation("keep", self, animationContext);
- };
+ if (insertAction.toIndex === 0) {
+ wrapper.insertAdjacentHTML("afterbegin", html);
+ } else {
+ const selector = `${wrapperSelector} .${toClass}${insertAction.toIndex - 1}`;
+ const prevSibling = document.querySelector(selector);
- /***************************************************************************
- * *
- * Animation() *
- * *
- ***************************************************************************/
+ prevSibling.insertAdjacentHTML("afterend", html);
+ }
- /**
- * A privately used class for creating animations. It allows for animations
- * to have state associated with them, without passing arguments to callback
- * functions.
- *
- * @param {string} animation - one of "remove", "sub", "insert", or
- * "keep". Indicates the animation to perform,
- * and forcasts the contents of animationContext.
- * @param {Object} sub - the instance of the Sub class associated
- * with this animation.
- * @param {Object} animationContext - any context that is needed by the
- * passed animation.
- */
- function Animation(animation, sub, animationContext) {
- var self = this;
- self.sub = sub;
- self.ctx = animationContext;
- self.transitionEnd = _whichTransitionEndEvent();
- self.animatingClass = " " + self.sub.settings.namespace + "-animating";
- if (animation === "remove") {
- self.steps = [
- function() {self._fadeOut();},
- function() {self._setWidth();},
- function() {self._removeElement();}
- ];
- } else if (animation === "sub") {
- self.steps = [
- function() {self._reIndex();},
- function() {self._fadeOut();},
- function() {self._setWidth();},
- function() {self._setTextAndFadeIn();},
- function() {self._cleanUp();}];
- } else if (animation === "insert") {
- self.steps = [
- function() {self._setWidth();},
- function() {self._setTextAndFadeIn();},
- function() {self._cleanUp();}];
- } else if (animation === "keep") {
- self.steps = [
- function() {self._reIndex();}
- ];
- } else {
- console.error("Unknown animation: ", animation);
+ /* Startup animations */
+ const toIndexClass = toClass + insertAction.toIndex;
+
+ const animationContext = {
+ toIndexClass: toIndexClass,
+ word: document.querySelector(`${wrapperSelector} .${toIndexClass}`),
+ visible: document.querySelector(`${wrapperSelector} .${toIndexClass}${visibleClass}`),
+ invisible: document.querySelector(`${wrapperSelector} .${toIndexClass}${invisibleClass}`),
+ newText: insertAction.toWord
+ };
+
+ if (verbose) {
+ console.log("insert", animationContext);
+ }
+
+ new Animation("insert", this, animationContext);
+ });
+ }, speed);
}
- self.steps[0](); // dequeue an run the first task.
- }
- /**
- * Change the index class of the word.
- */
- Animation.prototype._reIndex = function() {
- var self = this;
- var ctx = self.ctx;
- // if (self.sub.settings.verbose) { console.log("_reIndex"); }
-
- // Perform substitution if needed
- if (self.sub.settings.verbose) {console.log("_reIndex ", ctx.word.innerText, " from ", ctx.fromIndexClass, " to ", ctx.toIndexClass); }
- ctx.word.className = ctx.word.className.replace(ctx.fromIndexClass, ctx.toIndexClass);
-
- // run next step if there is one
- self.steps.shift(); // pop _reIndex
- if (self.steps.length > 0) {
- self.steps[0]();
+ /**
+ * Perform the given substitution
+ *
+ * @param {Object} subAction - the substitution to perform
+ * @param {int} subAction.fromIndex - the index of the element to change
+ * @param {string} subAction.fromWord - the word to sub
+ * @param {int} subAction.toIndex - the index to give the new word
+ * @param {string} subAction.toWord - the word to sub with
+ */
+ _subAction(subAction) {
+ const {
+ fromClass,
+ toClass,
+ wrapperSelector,
+ visibleClass,
+ invisibleClass,
+ "settings": {
+ verbose
+ }
+ } = this;
+
+ const fromIndexClass = fromClass + subAction.fromIndex;
+
+ const animationContext = {
+ fromIndexClass: fromIndexClass,
+ toIndexClass: toClass + subAction.toIndex,
+ word: document.querySelector(`${wrapperSelector} .${fromIndexClass}`),
+ visible: document.querySelector(`${wrapperSelector} .${fromIndexClass}${visibleClass}`),
+ invisible: document.querySelector(`${wrapperSelector} .${fromIndexClass}${invisibleClass}`),
+ newText: subAction.toWord
+ };
+
+ if (verbose) {
+ console.log("sub", animationContext);
+ }
+
+ new Animation("sub", this, animationContext);
}
- };
- /**
- * Fade out this word
- */
- Animation.prototype._fadeOut = function() {
- var self = this;
- var ctx = self.ctx;
- if (self.sub.settings.verbose) { console.log("_fadeOut"); }
-
- /* Hold the containerId width, and fade out */
- ctx.visible.className += self.animatingClass;
- self.steps.shift(); // pop _fadeOut
- ctx.visible.addEventListener(self.transitionEnd, self.steps[0], false);
- ctx.invisible.style.width = ctx.invisible.offsetWidth + "px";
- ctx.visible.style.opacity = 0;
- };
+ /**
+ * Perform the given keep action.
+ *
+ * @param {Object} keepAction - the keep action to perform
+ * @param {int} keepAction.fromIndex - the index of the word to re-label
+ * @param {int} keepAction.toIndex - the index to label this word
+ */
+ _keepAction(keepAction) {
+ const {
+ fromClass,
+ toClass,
+ wrapperSelector,
+ "settings": {
+ verbose
+ }
+ } = this;
- /**
- * Set with width of this word to the width of ctx.newText.
- */
- Animation.prototype._setWidth = function() {
- var self = this;
- var ctx = self.ctx;
- if (self.sub.settings.verbose) { console.log("_setWidth"); }
- /* Animate the width */
- ctx.visible.className = ctx.visible.className.replace(self.animatingClass, "");
- ctx.invisible.className += self.animatingClass;
- ctx.visible.removeEventListener(self.transitionEnd, self.steps[0], false);
- self.steps.shift(); // pop _setWidth
- ctx.invisible.addEventListener(self.transitionEnd, self.steps[0], false);
- var newWidth = self._calculateWordWidth(
- ctx.newText,
- self.sub.wrapper.tagName,
- self.sub.wrapper.className.split(" ")
- );
- setTimeout(function() {
- ctx.invisible.style.width = newWidth + "px";
- }, 5);
- };
+ const fromIndexClass = fromClass + keepAction.fromIndex;
- /**
- * Remove this element from the DOM
- */
- Animation.prototype._removeElement = function() {
- var self = this;
- var ctx = self.ctx;
- if (self.sub.settings.verbose) { console.log("_removeElement"); }
-
- /* Remove this word */
- ctx.invisible.removeEventListener(self.transitionEnd, self.steps[0], false);
- self.sub.wrapper.removeChild(ctx.word);
- };
+ const animationContext = {
+ fromIndexClass: fromIndexClass,
+ toIndexClass: toClass + keepAction.toIndex,
+ word: document.querySelector(`${wrapperSelector} .${fromIndexClass}`),
+ };
- /**
- * Set the text of this element to ctx.newText and fade it in.
- */
- Animation.prototype._setTextAndFadeIn = function() {
- var self = this;
- var ctx = self.ctx;
- if (self.sub.settings.verbose) { console.log("_setTextAndFadeIn"); }
- /* Sub the text then fade in */
- ctx.invisible.className = ctx.invisible.className.replace(self.animatingClass, "");
- ctx.visible.className += self.animatingClass;
- ctx.invisible.removeEventListener(self.transitionEnd, self.steps[0], false);
- self.steps.shift(); // pop _setTextAndFadeIn
- ctx.visible.addEventListener(self.transitionEnd, self.steps[0], false);
- ctx.visible.innerHTML = ctx.newText;
- ctx.invisible.innerHTML = ctx.newText;
- ctx.visible.style.opacity = 1;
- };
+ if (verbose) {
+ console.log("keep", animationContext);
+ }
- /**
- * Remove animation classes, remove event listeners, and set widths to "auto"
- */
- Animation.prototype._cleanUp = function() {
- var self = this;
- var ctx = self.ctx;
- if (self.sub.settings.verbose) { console.log("_cleanUp"); }
-
- /* Clean Up */
- ctx.invisible.className = ctx.invisible.className.replace(self.animatingClass, "");
- ctx.visible.className = ctx.visible.className.replace(self.animatingClass, "");
- ctx.visible.removeEventListener(self.transitionEnd, self.steps[0], false);
- ctx.invisible.style.width = "auto";
- };
+ new Animation("keep", this, animationContext);
+ }
- /**
- * Find the width that an element with a given tag and classes would have if
- * it contained the passed text.
- *
- * @param {string} text - the text to get the width of
- * @param {string} tag - the tag that the text will be put in
- * @param {string[]} classes - an array of classes associated with this
- * element.
- */
- Animation.prototype._calculateWordWidth = function(text, tag, classes) {
- var self = this;
- var elem = document.createElement(tag);
- classes = classes || [];
- classes.push(self.sub.settings.namespace + "-text-width-calculation");
- elem.setAttribute("class", classes.join(" "));
- elem.innerHTML = text;
- document.body.appendChild(elem);
- /* Get a decimal number of the form 12.455 */
- var width = parseFloat(window.getComputedStyle(elem, null).width);
- elem.parentNode.removeChild(elem);
- return width;
- };
+ /**
+ * Apply `action`, by performing the necessary substitutions, removals, keeps,
+ * and insertions.
+ */
+ _applyAction(action) {
+ const {
+ fromClass,
+ toClass,
+ "settings": {
+ namespace,
+ verbose
+ }
+ } = this;
+
+ const words = document.getElementsByClassName(namespace + "-word");
+
+ Array.from(words).forEach(elem => {
+ if (verbose) {
+ console.log("replacing to- with from- for:", elem);
+ }
+
+ elem.className = elem.className.replace(toClass, fromClass);
+ });
+
+ action.sub.map(subAction => {
+ this._subAction(subAction);
+ });
+
+ action.remove.map(removeAction => {
+ this._removeAction(removeAction);
+ });
+
+ action.keep.map(keepAction => {
+ this._keepAction(keepAction);
+ });
+
+ this._performInsertions(action.insert);
+ }
+ }
window.Sub = Sub;
-}(window));
+})(window);
\ No newline at end of file
diff --git a/test/test.html b/test/test.html
index 9211d18..b538da7 100644
--- a/test/test.html
+++ b/test/test.html
@@ -3,7 +3,7 @@
-
+