From 2222a3b1c6f0f51c23173559dfb90cf0d9277212 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Fri, 10 Nov 2017 10:48:34 +0700 Subject: [PATCH 1/2] Version bump and script --- package.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e457703..3256d55 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "chat-bubble", - "version": "1.2.2", - "description": "Simple chatbot bubble creator based on JSON conversation structure", + "version": "1.2.3", + "description": "Simple chatbot UI for Web with JSON scripting. Zero dependencies.", "main": "component/Bubble.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "playground": "open ./component/examples/4-advanced-keyboard.html" }, "repository": { "type": "git", @@ -12,10 +12,11 @@ }, "keywords": [ "chatbot", - "texft", - "bubbles", "chat", - "bot" + "bot", + "chatui", + "conversation", + "javascript" ], "author": "dmitrizzle", "license": "MIT", From 068ee7688e4d002fada7cd5516e4d2b85fdb5996 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Fri, 10 Nov 2017 12:53:27 +0700 Subject: [PATCH 2/2] Exports for ES6 created --- component/Bubbles.js | 416 +++++++++++++++++++++++++------------------ 1 file changed, 238 insertions(+), 178 deletions(-) diff --git a/component/Bubbles.js b/component/Bubbles.js index 72008cc..8576ad9 100644 --- a/component/Bubbles.js +++ b/component/Bubbles.js @@ -1,196 +1,256 @@ +// core function function Bubbles(container, self, options) { + // options + options = typeof options !== "undefined" ? options : {} + animationTime = options.animationTime || 200 // how long it takes to animate chat bubble, also set in CSS + typeSpeed = options.typeSpeed || 5 // delay per character, to simulate the machine "typing" + widerBy = options.widerBy || 2 // add a little extra width to bubbles to make sure they don't break + sidePadding = options.sidePadding || 6 // padding on both sides of chat bubbles + inputCallbackFn = options.inputCallbackFn || false // should we display an input field? - // options - options = typeof options !== "undefined" ? options : {}; - animationTime = options.animationTime || 200; // how long it takes to animate chat bubble, also set in CSS - typeSpeed = options.typeSpeed || 5; // delay per character, to simulate the machine "typing" - widerBy = options.widerBy || 2; // add a little extra width to bubbles to make sure they don't break - sidePadding = options.sidePadding || 6; // padding on both sides of chat bubbles - inputCallbackFn = options.inputCallbackFn || false; // should we display an input field? - - var standingAnswer = "ice"; // remember where to restart convo if interrupted - - var _convo = {}; // local memory for conversation JSON object - //--> NOTE that this object is only assigned once, per session and does not change for this - // constructor name during open session. - - - - // set up the stage - container.classList.add("bubble-container"); - var bubbleWrap = document.createElement("div"); - bubbleWrap.className = "bubble-wrap"; - container.appendChild(bubbleWrap); - - // install user input textfield - this.typeInput = function(callbackFn){ - var inputWrap = document.createElement("div"); - inputWrap.className = "input-wrap"; - var inputText = document.createElement("textarea"); - inputText.setAttribute("placeholder", "Ask me anything..."); - inputWrap.appendChild(inputText); - inputText.addEventListener("keypress", function(e){ // register user input - if(e.keyCode == 13){ - e.preventDefault(); - typeof bubbleQueue !== false ? clearTimeout(bubbleQueue) : false; // allow user to interrupt the bot - var lastBubble = document.querySelectorAll(".bubble.say"); lastBubble = lastBubble[lastBubble.length-1]; - lastBubble.classList.contains("reply") && !lastBubble.classList.contains("reply-freeform") ? lastBubble.classList.add("bubble-hidden") : false; - addBubble("" + this.value + "", function(){}, "reply reply-freeform"); - // callback - typeof callbackFn === "function" ? callbackFn({ - "input" : this.value, - "convo" : _convo, - "standingAnswer": standingAnswer - }) : false; - this.value = ""; - } - }); - container.appendChild(inputWrap); - bubbleWrap.style.paddingBottom = "100px"; - inputText.focus(); - } - inputCallbackFn ? this.typeInput(inputCallbackFn) : false; - - - - // init typing bubble - var bubbleTyping = document.createElement("div"); - bubbleTyping.className = "bubble-typing imagine"; - for (dots = 0; dots < 3; dots++) { - var dot = document.createElement("div"); - dot.className = "dot_" + dots + " dot"; - bubbleTyping.appendChild(dot); - } - bubbleWrap.appendChild(bubbleTyping); + var standingAnswer = "ice" // remember where to restart convo if interrupted + + var _convo = {} // local memory for conversation JSON object + //--> NOTE that this object is only assigned once, per session and does not change for this + // constructor name during open session. + + // set up the stage + container.classList.add("bubble-container") + var bubbleWrap = document.createElement("div") + bubbleWrap.className = "bubble-wrap" + container.appendChild(bubbleWrap) + + // install user input textfield + this.typeInput = function(callbackFn) { + var inputWrap = document.createElement("div") + inputWrap.className = "input-wrap" + var inputText = document.createElement("textarea") + inputText.setAttribute("placeholder", "Ask me anything...") + inputWrap.appendChild(inputText) + inputText.addEventListener("keypress", function(e) { + // register user input + if (e.keyCode == 13) { + e.preventDefault() + typeof bubbleQueue !== false ? clearTimeout(bubbleQueue) : false // allow user to interrupt the bot + var lastBubble = document.querySelectorAll(".bubble.say") + lastBubble = lastBubble[lastBubble.length - 1] + lastBubble.classList.contains("reply") && + !lastBubble.classList.contains("reply-freeform") + ? lastBubble.classList.add("bubble-hidden") + : false + addBubble( + '' + this.value + "", + function() {}, + "reply reply-freeform" + ) + // callback + typeof callbackFn === "function" + ? callbackFn({ + input: this.value, + convo: _convo, + standingAnswer: standingAnswer + }) + : false + this.value = "" + } + }) + container.appendChild(inputWrap) + bubbleWrap.style.paddingBottom = "100px" + inputText.focus() + } + inputCallbackFn ? this.typeInput(inputCallbackFn) : false + + // init typing bubble + var bubbleTyping = document.createElement("div") + bubbleTyping.className = "bubble-typing imagine" + for (dots = 0; dots < 3; dots++) { + var dot = document.createElement("div") + dot.className = "dot_" + dots + " dot" + bubbleTyping.appendChild(dot) + } + bubbleWrap.appendChild(bubbleTyping) // accept JSON & create bubbles this.talk = function(convo, here) { + // all further .talk() calls will append the conversation with additional blocks defined in convo parameter + _convo = Object.assign(_convo, convo) // POLYFILL REQUIRED FOR OLDER BROWSERS - // all further .talk() calls will append the conversation with additional blocks defined in convo parameter - _convo = Object.assign(_convo, convo); // POLYFILL REQUIRED FOR OLDER BROWSERS - - this.reply(_convo[here]); - here ? standingAnswer = here : false; + this.reply(_convo[here]) + here ? (standingAnswer = here) : false } this.reply = function(turn) { - turn = typeof turn !== "undefined" ? turn : _convo.ice; - questionsHTML = ""; - if(turn.reply !== undefined){ - turn.reply.reverse(); - for(var i=0; i" - + el.question + ""; - })(turn.reply[i], i); - } - } - orderBubbles(turn.says, function(){ - bubbleTyping.classList.remove("imagine"); - questionsHTML !== "" ? addBubble(questionsHTML, function(){}, "reply") : bubbleTyping.classList.add("imagine"); - }); + turn = typeof turn !== "undefined" ? turn : _convo.ice + questionsHTML = "" + if (turn.reply !== undefined) { + turn.reply.reverse() + for (var i = 0; i < turn.reply.length; i++) { + ;(function(el, count) { + questionsHTML += + '" + + el.question + + "" + })(turn.reply[i], i) + } + } + orderBubbles(turn.says, function() { + bubbleTyping.classList.remove("imagine") + questionsHTML !== "" + ? addBubble(questionsHTML, function() {}, "reply") + : bubbleTyping.classList.add("imagine") + }) } // navigate "answers" - this.answer = function(key){ - var func = function(key){ typeof window[key] === "function" ? window[key]() : false; } - _convo[key] !== undefined ? (this.reply(_convo[key]), standingAnswer = key) : func(key); - }; + this.answer = function(key) { + var func = function(key) { + typeof window[key] === "function" ? window[key]() : false + } + _convo[key] !== undefined + ? (this.reply(_convo[key]), (standingAnswer = key)) + : func(key) + } // api for typing bubble - this.think = function(){ - bubbleTyping.classList.remove("imagine"); - this.stop = function(){ - bubbleTyping.classList.add("imagine"); - } + this.think = function() { + bubbleTyping.classList.remove("imagine") + this.stop = function() { + bubbleTyping.classList.add("imagine") + } } // "type" each message within the group - var orderBubbles = function(q, callback){ - var start = function(){ setTimeout(function() { callback() }, animationTime); }; - var position = 0; - for (var nextCallback = position + q.length - 1; nextCallback >= position; nextCallback --){ - (function(callback, index){ - start = function(){ - addBubble(q[index], callback); - }; - })(start, nextCallback); - } - start(); + var orderBubbles = function(q, callback) { + var start = function() { + setTimeout(function() { + callback() + }, animationTime) + } + var position = 0 + for ( + var nextCallback = position + q.length - 1; + nextCallback >= position; + nextCallback-- + ) { + ;(function(callback, index) { + start = function() { + addBubble(q[index], callback) + } + })(start, nextCallback) + } + start() } // create a bubble - var bubbleQueue = false; - var addBubble = function(say, posted, reply){ - reply = typeof reply !== "undefined" ? reply : ""; - // create bubble element - var bubble = document.createElement("div"); - var bubbleContent = document.createElement("span"); - bubble.className = "bubble imagine " + reply; - bubbleContent.className = "bubble-content"; - bubbleContent.innerHTML = say; - bubble.appendChild(bubbleContent); - bubbleWrap.insertBefore(bubble, bubbleTyping); - // answer picker styles - if(reply !== ""){ - var bubbleButtons = bubbleContent.querySelectorAll(".bubble-button"); - - for(var z =0; z animationTime && reply == ""){ - wait += typeSpeed * say.length; - wait < minTypingWait ? wait = minTypingWait : false; - setTimeout(function() { bubbleTyping.classList.remove("imagine"); }, animationTime ); - } - setTimeout(function() { bubbleTyping.classList.add("imagine"); }, wait - animationTime * 2 ); - bubbleQueue = setTimeout(function() { - bubble.classList.remove("imagine"); - var bubbleWidthCalc = bubbleContent.offsetWidth + widerBy + "px"; - bubble.style.width = reply == "" ? bubbleWidthCalc : ""; - bubble.style.width = say.includes(" containerHeight ? bubbleWrap.scrollTop = bubbleWrap.scrollTop + scrollHop : false; - }, (i * 5) ); - })(); - } - } - setTimeout(scrollBubbles, animationTime / 2); - }, wait + animationTime * 2); - } - - - -} // close function + var bubbleQueue = false + var addBubble = function(say, posted, reply) { + reply = typeof reply !== "undefined" ? reply : "" + // create bubble element + var bubble = document.createElement("div") + var bubbleContent = document.createElement("span") + bubble.className = "bubble imagine " + reply + bubbleContent.className = "bubble-content" + bubbleContent.innerHTML = say + bubble.appendChild(bubbleContent) + bubbleWrap.insertBefore(bubble, bubbleTyping) + // answer picker styles + if (reply !== "") { + var bubbleButtons = bubbleContent.querySelectorAll(".bubble-button") + + for (var z = 0; z < bubbleButtons.length; z++) { + ;(function(el) { + if (!el.parentNode.parentNode.classList.contains("reply-freeform")) + el.style.width = el.offsetWidth - sidePadding * 2 + widerBy + "px" + })(bubbleButtons[z]) + } + bubble.addEventListener("click", function() { + for (var i = 0; i < bubbleButtons.length; i++) { + ;(function(el) { + el.style.width = 0 + "px" + el.classList.contains("bubble-pick") ? (el.style.width = "") : false + el.removeAttribute("onclick") + })(bubbleButtons[i]) + } + this.classList.add("bubble-picked") + }) + } + // time, size & animate + wait = animationTime * 2 + minTypingWait = animationTime * 6 + if (say.length * typeSpeed > animationTime && reply == "") { + wait += typeSpeed * say.length + wait < minTypingWait ? (wait = minTypingWait) : false + setTimeout(function() { + bubbleTyping.classList.remove("imagine") + }, animationTime) + } + setTimeout(function() { + bubbleTyping.classList.add("imagine") + }, wait - animationTime * 2) + bubbleQueue = setTimeout(function() { + bubble.classList.remove("imagine") + var bubbleWidthCalc = bubbleContent.offsetWidth + widerBy + "px" + bubble.style.width = reply == "" ? bubbleWidthCalc : "" + bubble.style.width = say.includes(" containerHeight + ? (bubbleWrap.scrollTop = bubbleWrap.scrollTop + scrollHop) + : false + }, i * 5) + })() + } + } + setTimeout(scrollBubbles, animationTime / 2) + }, wait + animationTime * 2) + } +} + +// below functions are specifically for WebPack-type project that work with import() + +// this function automatically adds all HTML and CSS necessary for chat-bubble to function +function prepHTML(options) { + // options + var options = typeof options !== "undefined" ? options : {} + var container = options.container || "chat" // id of the container HTML element + var relative_path = options.relative_path || "./node_modules/chat-bubble/" + + // make HTML container element + window[container] = document.createElement("div") + window[container].setAttribute("id", container) + document.body.appendChild(window[container]) + + // style everything + var appendCSS = function(file) { + var link = document.createElement("link") + link.href = file; + link.type = "text/css" + link.rel = "stylesheet" + link.media = "screen,print" + document.getElementsByTagName("head")[0].appendChild(link) + } + appendCSS(relative_path+ "component/styles/input.css"); + appendCSS(relative_path + "component/styles/reply.css") + appendCSS(relative_path + "component/styles/says.css") + appendCSS(relative_path + "component/styles/setup.css") + appendCSS(relative_path + "component/styles/typing.css") + +} + +// exports for es6 +exports.Bubbles = Bubbles +exports.prepHTML = prepHTML