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 @@ - +