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
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",