Skip to content

Instantly share code, notes, and snippets.

@alexcdot
Last active May 14, 2018 09:22
Show Gist options
  • Save alexcdot/0487371dd5160de9e77d7fa838ae39b3 to your computer and use it in GitHub Desktop.
Save alexcdot/0487371dd5160de9e77d7fa838ae39b3 to your computer and use it in GitHub Desktop.
Create a web chat bubble using just one javascript file
var jokesonyou = 1;
alert("loaded a script!");
document.body.innerHTML += '<h1>You loaded me</h1>';
// When the user clicks the button, open the modal
function displayModal() {
document.getElementById("myModal").style.display = "block";
var lastChildIndex = document.getElementById("chat").children[0].children.length;
document.getElementById("chat").children[0].children[lastChildIndex -1].scrollIntoView();
}
function hideModal() {
document.getElementById("myModal").style.display = "none";
}
// core function
function Bubbles(container, self, options) {
// options
options = typeof options !== "undefined" ? options : {}
animationTime = options.animationTime || 50 // how long it takes to animate chat bubble, also set in CSS
typeSpeed = options.typeSpeed || 0 // 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
recallInteractions = options.recallInteractions || 5 // number of interactions to be remembered and brought back upon restart
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.
// local storage for recalling conversations upon restart
var localStorageCheck = function() {
var test = "chat-bubble-storage-test"
try {
localStorage.setItem(test, test)
localStorage.removeItem(test)
return true
} catch (error) {
console.error(
"Your server does not allow storing data locally. Most likely it's because you've opened this page from your hard-drive. For testing you can disable your browser's security or start a localhost environment."
)
return false
}
}
var localStorageAvailable = localStorageCheck() && recallInteractions > 0
var interactionsLS = "chat-bubble-interactions"
var interactionsHistory =
(localStorageAvailable &&
JSON.parse(localStorage.getItem(interactionsLS))) ||
[]
// prepare next save point
interactionsSave = function(say, reply) {
if (!localStorageAvailable) return
// limit number of saves
if (interactionsHistory.length > recallInteractions)
interactionsHistory.shift() // removes the oldest (first) save to make space
// do not memorize buttons; only user input gets memorized:
if (
// `bubble-button` class name signals that it's a button
say.includes("bubble-button") &&
// if it is not of a type of textual reply
reply !== "reply reply-freeform" &&
// if it is not of a type of textual reply or memorized user choice
reply !== "reply reply-pick"
)
// ...it shan't be memorized
return
// save to memory
interactionsHistory.push({ say: say, reply: reply })
}
// commit save to localStorage
interactionsSaveCommit = function() {
if (!localStorageAvailable) return
localStorage.setItem(interactionsLS, JSON.stringify(interactionsHistory))
}
// 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(
'<span class="bubble-button bubble-pick">' + this.value + "</span>",
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 = "80px"
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
this.reply(_convo[here])
here ? (standingAnswer = here) : false
}
var iceBreaker = false // this variable holds answer to whether this is the initative bot interaction or not
this.reply = function(turn) {
if (typeof turn === "undefined") {
// initial message
if (localStorage.getItem("chat-bubble-interactions") !== null) {
turn = _convo.returnMessage;
} else {
turn = _convo.ice;
}
}
questionsHTML = ""
if (!turn) return
if (turn.reply !== undefined) {
turn.reply.reverse()
for (var i = 0; i < turn.reply.length; i++) {
;(function(el, count) {
if (el.question === undefined) {
return;
}
questionsHTML +=
'<span class="bubble-button" style="animation-delay: ' +
animationTime / 2 * count +
'ms" onClick="' +
self +
".answer('" +
el.answer +
"', '" +
el.question +
"');this.classList.add('bubble-pick')\">" +
el.question +
"</span>"
})(turn.reply[i], i)
}
}
var saidResponse = turn.says;
if (typeof turn.says === "function") {
saidResponse = [turn.says()];
}
orderBubbles(saidResponse, function() {
bubbleTyping.classList.remove("imagine")
questionsHTML !== ""
? addBubble(questionsHTML, function() {}, "reply")
: bubbleTyping.classList.add("imagine")
})
}
// navigate "answers"
this.answer = function(key, content) {
var func = function(key) {
typeof window[key] === "function" ? window[key]() : false
}
// speical jank method to intercept normal flow to add whimmly response
if (key === "variable-response") {
window.question = content;
callWhimmlyAPI();
return;
}
_convo[key] !== undefined
? (this.reply(_convo[key]), (standingAnswer = key))
: func(key)
// add re-generated user picks to the history stack
if (_convo[key] !== undefined && content !== undefined) {
interactionsSave(
'<span class="bubble-button reply-pick">' + content + "</span>",
"reply reply-pick"
)
}
}
// api for typing bubble
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()
}
// create a bubble
var bubbleQueue = false
var addBubble = function(say, posted, reply, live) {
reply = typeof reply !== "undefined" ? reply : ""
live = typeof live !== "undefined" ? live : true // bubbles that are not "live" are not animated and displayed differently
var animationTime = live ? this.animationTime : 0
var typeSpeed = live ? this.typeSpeed : 0
// create bubble element
var bubble = document.createElement("div")
var bubbleContent = document.createElement("span")
bubble.className = "bubble imagine " + (!live ? " history " : "") + 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 = live ? animationTime * 2 : 0
minTypingWait = live ? animationTime * 6 : 0
if (say.length * typeSpeed > animationTime && reply == "") {
wait += typeSpeed * say.length
wait < minTypingWait ? (wait = minTypingWait) : false
setTimeout(function() {
bubbleTyping.classList.remove("imagine")
}, animationTime)
}
live && setTimeout(function() {
bubbleTyping.classList.add("imagine")
}, wait - animationTime * 2)
bubbleQueue = setTimeout(function() {
bubble.classList.remove("imagine")
var baseWidth = bubbleContent.offsetWidth == 0 ? 400 : bubbleContent.offsetWidth;
var bubbleWidthCalc = baseWidth + widerBy + "px"
bubble.style.width = reply == "" ? bubbleWidthCalc : ""
bubble.style.width = say.includes("<img src=")
? "50%"
: bubble.style.width
bubble.classList.add("say")
posted()
// save the interaction
interactionsSave(say, reply)
!iceBreaker && interactionsSaveCommit() // save point
// animate scrolling
containerHeight = container.offsetHeight
scrollDifference = bubbleWrap.scrollHeight - bubbleWrap.scrollTop
scrollHop = scrollDifference / 200
var scrollBubbles = function() {
for (var i = 1; i <= scrollDifference / scrollHop; i++) {
;(function() {
setTimeout(function() {
bubbleWrap.scrollHeight - bubbleWrap.scrollTop > containerHeight
? (bubbleWrap.scrollTop = bubbleWrap.scrollTop + scrollHop)
: false
}, i * 5)
})()
}
}
setTimeout(scrollBubbles, animationTime / 2)
}, wait + animationTime * 2)
}
// recall previous interactions
for (var i = 0; i < interactionsHistory.length; i++) {
addBubble(
interactionsHistory[i].say,
function() {},
interactionsHistory[i].reply,
false
)
}
}
// 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])
}
document.body.innerHTML += '<div class="modal-content" style="position: fixed; bottom: 10px; right: 10px; width: 400px; box-shadow: 0px 0px 15px 5px rgba(0, 0, 0, .2); z-index: 10;"><div style="background: #CCC;display: flex;justify-content: center;align-items: center;"><img src="https://whimmly.com/wp-content/uploads/2018/02/full_logo.png" style="max-width: 150px;margin: auto;padding: 8px;display: inline-block;text-align: center;"/> <span class="close" onclick="hideModal()">&times;</span> </div> <div id="chat"></div></div>';
//document.body.innerHTML += '<div id="myModal" class="modal"><div class="modal-content" style="position: fixed; bottom: 10px; right: 10px; width: 400px; box-shadow: 0px 0px 15px 5px rgba(0, 0, 0, .2);"><div style="background: #CCC;display: flex;justify-content: center;align-items: center;"><img src="https://whimmly.com/wp-content/uploads/2018/02/full_logo.png" style="max-width: 150px;margin: auto;padding: 8px;display: inline-block;text-align: center;"/> <span class="close" onclick="hideModal()">&times;</span> </div> <div id="chat"></div></div></div><button id="myBtn" class="open-chat-button" onclick="displayModal()"><i class="material-icons">&#xE0CA;</i></button>';
// initialize by constructing a named function...
var chatWindow = new Bubbles(
document.getElementById("chat"), // ...passing HTML container element...
"chatWindow", { // ...and name of the function as a parameter
// the one that we care about is inputCallbackFn()
// this function returns an object with some data that we can process from user input
// and understand the context of it
// this is an example function that matches the text user typed to one of the answer bubbles
// this function does no natural language processing
// this is where you may want to connect this script to NLC backend.
inputCallbackFn: function(o) {
o.convo[o.standingAnswer].reply.forEach(function(e, i) {
window['question'] = o.input;
});
callWhimmlyAPI();
}
})
// conversation object defined separately, but just the same as in the
// "Basic chat-bubble Example" (1-basics.html) -- with an exception that...
// ...allows running your scripts on-demand
function getResponse() {
return window.response;
}
var convo = {
"variable-response": {
says: getResponse,
reply: [
{
}
]
},
returnMessage: {
says: ["Welcome back to Dobbin!"],
reply: [
{
question: "Find me an amber ale",
answer: "variable-response"
},
{
question: "What are you?",
answer: "variable-response"
},
{
question: "Can you recommend any strong stouts?",
answer: "variable-response"
}
]
},
ice: {
says: ["Hi, welcome to Dobbin!",
"You can ask me for " +
"beer suggestions that match your palate, and definition" +
"s for beer terms, like porters or stouts. You can also " +
"tell me more about your tastes to personalize your " +
"experience."],
reply: [
{
question: "What is a porter?",
answer: "variable-response"
},
{
question: "Can you suggest any stouts?",
answer: "variable-response"
},
{
question: "Help me out!",
answer: "variable-response"
}
]
}
}
// this function is called when user gives an input
callWhimmlyAPI = function() {
var question = window['question'];
if (question === undefined || question === null || question === "") {
console.log('no question asked');
return;
}
console.log("fetching")
fetch("https://api.whimmly.com/chat?query="
+ encodeURIComponent(question), {
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
mode: 'cors',
credentials: 'include'
})
.then(response => response.json())
.then(parsedResponse => {
window.response = (parsedResponse || {}).response;
chatWindow.talk(convo, "variable-response");
});
}
// pass JSON to your function and you're done!
chatWindow.talk(convo)
function addStyleSheet(fileName) {
var head = document.head;
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = fileName;
head.appendChild(link);
}
function addCss(css) {
var head = document.head;
var style = document.createElement("style");
style.type = "text/css";
style.appendChild(document.createTextNode(css));
head.appendChild(style);
}
addStyleSheet('https://fonts.googleapis.com/icon?family=Material+Icons');
addCss('/* style user input field */.bubble-container .input-wrap { position: absolute; bottom: 0; left: 0; right: 0; padding-right: 30px; font-family: "Helvetica Neue", Helvetica, sans-serif; color: #2c2c2c;}.bubble-container .input-wrap textarea { width: calc(100% - 20px); font-family: "Helvetica Neue", Helvetica, sans-serif; color: #2c2c2c; background: rgba(250, 250, 250, 0.95); font-size: 1em; letter-spacing: .5px; font-weight: 400; margin: 10px; border-radius: 15px; border: none; padding: 10px 15px; outline: none; box-shadow: 0 0 0 1px #d0d0d0 inset; line-height: 1.25em;}.bubble.reply-freeform { margin: 0;}.bubble.reply.reply-freeform.say .bubble-content .bubble-button { margin-top: 1px; text-align: left;}.bubble.reply.say.bubble-hidden { margin: 0; transform: scale(0); height: 0;}/* style user response reply */.bubble.reply { background: transparent; box-shadow: none; float: right; position: relative; transform-origin: right top; margin: 8px 0 10px; padding: 0; max-width: 65%;}.bubble.reply.history { margin: 0 0 2px 0; /* remembered bubbles do not need to stand out via margin */}.bubble.reply.say { /* min-width: 350px; */}.bubble.reply .bubble-content { transition: all 200ms;}.bubble.reply .bubble-content .bubble-button { background: rgba(44, 44, 44, 0.67); color: #fff; padding: 8px 16px; border-radius: 15px 15px 5px 5px; margin-left: 2px; text-align: center; display: inline-block; float: right; cursor: pointer; transition: all 200ms; text-decoration: none; word-break: normal; box-sizing: content-box; /* animation-duration: 1s; */ animation-name: animate-reply; animation-play-state: paused; animation-fill-mode: forwards; /* opacity: 0; */ transform: translate3d(0px, 0px, 0px); animation-delay: -3s; -ms-animation-delay: -3; -webkit-animation-delay: -3s;}@keyframes animate-reply { from { opacity: 0; } to { opacity: 1; }}.bubble.reply.say .bubble-content .bubble-button { animation-play-state: running; margin-top: 3px; min-height: 24px; overflow: hidden;}.bubble.reply .bubble-content .bubble-button:first-child { border-radius: 15px 15px 15px 5px; margin-left: 2px;}.bubble.reply .bubble-content .bubble-button:last-child,.bubble.reply .bubble-content .bubble-button.bubble-pick { border-radius: 15px 15px 5px 15px;}.bubble.reply.bubble-picked .bubble-content .bubble-button { transform: scale(0) translate3d(0px, 0px, 0px); padding: 0;}.bubble.reply:not(.bubble-picked) .bubble-content .bubble-button:hover,.bubble.reply .bubble-content .bubble-button.bubble-pick { background: rgba(44, 44, 44, 1); transform: scale(1) translate3d(0px, 0px, 0px); padding: 8px 16px; height: auto;}/* interaction recall styles */.bubble.history .bubble-content .bubble-button,.bubble.history.reply:not(.bubble-picked) .bubble-content .bubble-button:hover,.bubble.history.reply .bubble-content .bubble-button.bubble-pick { background: rgba(44, 44, 44, 0.67); cursor: default;}/* input fields for bubbles */.bubble .bubble-content input { background: linear-gradient(193deg, #1faced, #5592dc 100%) !important; box-shadow: 0 0px 1px 0px #000, 0 -1px 0 0px rgba(255, 255, 255, 0.38) inset; text-shadow: 0 1px rgba(0, 0, 0, 0.35); border: 0; outline: 0;}.bubble .bubble-content input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ color: rgba(255, 255, 255, .5); text-shadow: none;}.bubble .bubble-content input::-moz-placeholder { /* Firefox 19+ */ color: rgba(255, 255, 255, .5); text-shadow: none;}.bubble .bubble-content input:read-only { background: linear-gradient(166deg, #48121d, #0d4058 100%) !important;}/* style bubbles */.bubble,.bubble-typing { color: #212121; background: rgba(255, 255, 255, 0.84); padding: 8px 16px; border-radius: 5px 15px 15px 15px; font-weight: 400; text-transform: none; text-align: left; font-size: 16px; letter-spacing: .5px; margin: 0 0 2px 0; max-width: 65%; float: none; clear: both; line-height: 1.5em; word-break: break-word; transform-origin: left top; transition: all 200ms; box-sizing: content-box;}.bubble .bubble-content { transition: opacity 150ms;}.bubble:not(.say) .bubble-content { opacity: 0;}.bubble-typing.imagine,.bubble.imagine { transform: scale(0); transition: all 200ms, height 200ms 1s, padding 200ms 1s;}.bubble.imagine { margin: 0; height: 0; padding: 0;}.bubble .bubble-content img { width: calc(100% + 32px); margin: -8px -16px; overflow: hidden; display: block; border-radius: 5px 15px 15px 15px;}/* interaction recall styles */.bubble.history,.bubble.history .bubble-content,.bubble.history .bubble-content .bubble-button,.bubble.history .bubble-content .bubble-button:hover { transition: all 0ms !important;}.bubble.history { opacity: .25;}/* setup container styles */.bubble-container { background: #dcdde0; height: 520px; max-width: 750px; width: 100%; margin: 0 auto; overflow: hidden; position: relative;}.bubble-wrap { position: absolute; top: 0; bottom: 0; left: 0; right: -17px; padding: 10px calc(17px + 10px) 30px 10px; overflow-y: scroll; -webkit-overflow-scrolling: touch; -webkit-transform: translate3d(0, 0, 0);}/* style "loading" or "typing" stae */.bubble-typing { width: 38px; padding: 12px 16px; height: 8px;}.dot { background-color: rgb(255, 255, 255); float: left; height: 7px; margin-left: 4px; width: 7px; animation-name: bounce_dot; animation-duration: 2.24s; animation-iteration-count: infinite; animation-direction: normal; border-radius: 5px;}.dot_1 { animation-delay: 0.45s;}.dot_2 { animation-delay: 1.05s;}.dot_3 { animation-delay: 1.35s;}@keyframes bounce_dot { 0% {} 50% { background-color: rgb(0, 0, 0); } 100% {}}/* ADDED IN BY WHIMMLY */.bubble-container { font-family: "Helvetica Neue", Helvetica, sans-serif; margin: 0;}/* The Modal (background) */.modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1; /* Sit on top */ left: 0; top: 0; width: 0%; /* Full width */ height: 0%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0, 0, 256); /* Fallback color */ background-color: rgba(0, 0, 0, 0.1); /* Black w/ opacity */ -webkit-animation-name: fadeIn; /* Fade in the background */ -webkit-animation-duration: 0.4s; animation-name: fadeIn; animation-duration: 0.4s}/* Modal Content */.modal-content { /*position: fixed; bottom: 0; background-color: #fefefe; width: 100%; */ -webkit-animation-name: slideIn; -webkit-animation-duration: 0.4s; animation-name: slideIn; animation-duration: 0.4s; animation-direction: alternate;}/* The Close Button */.close { color: white; float: right; font-size: 28px; font-weight: bold; position: absolute; right: 20px;}.close:hover,.close:focus { color: #000; text-decoration: none; cursor: pointer;}.modal-header { padding: 2px 16px; background-color: #5cb85c; color: white;}.modal-body { padding: 2px 16px;}.modal-footer { padding: 2px 16px; background-color: #5cb85c; color: white;}.open-chat-button { background-color: #4AF; border: none; color: white; padding: 20px; text-align: center; text-decoration: none; font-size: 16px; margin: 4px 2px; border-radius: 30%; position: fixed; bottom: 20px; right: 20px; box-shadow: 0px 0px 10px 3px rgba(0, 0, 0, .2);}/* Add Animation */@-webkit-keyframes slideIn { from { bottom: -300px; opacity: 0 } to { bottom: 0; opacity: 1 }}@keyframes slideIn { from { bottom: -300px; opacity: 0 } to { bottom: 0; opacity: 1 }}@-webkit-keyframes slideOut { from { bottom: 0; opacity: 1 } to { bottom: -300px; opacity: 0 }}@keyframes slideOut { from { bottom: 0; opacity: 1 } to { bottom: -300px; opacity: 0 }}@-webkit-keyframes fadeOut { from { opacity: 1 } to { opacity: 0 }}@keyframes fadeOut { from { opacity: 1 } to { opacity: 0 }} .bubble-container { height: 60vh;}');
document.write('<style> /* style user input field */ .bubble-container .input-wrap { position: absolute; bottom: 0; left: 0; right: 0; font-family: "Helvetica Neue", Helvetica, sans-serif; color: #2c2c2c; } .bubble-container .input-wrap textarea { width: calc(100% - 20px); font-family: "Helvetica Neue", Helvetica, sans-serif; color: #2c2c2c; background: rgba(250, 250, 250, 0.95); font-size: 1em; letter-spacing: .5px; font-weight: 400; margin: 10px; border-radius: 15px; border: none; padding: 10px 15px; outline: none; box-shadow: 0 0 0 1px #d0d0d0 inset; line-height: 1.25em; } .bubble.reply-freeform { margin: 0; } .bubble.reply.reply-freeform.say .bubble-content .bubble-button { margin-top: 1px; text-align: left; } .bubble.reply.say.bubble-hidden { margin: 0; transform: scale(0); height: 0; } /* style user response reply */ .bubble.reply { background: transparent; box-shadow: none; float: right; position: relative; transform-origin: right top; margin: 8px 0 10px; padding: 0; max-width: 65%; } .bubble.reply.history { margin: 0 0 2px 0; /* remembered bubbles do not need to stand out via margin */ } .bubble.reply.say { /* min-width: 350px; */ } .bubble.reply .bubble-content { transition: all 200ms; } .bubble.reply .bubble-content .bubble-button { background: rgba(44, 44, 44, 0.67); color: #fff; padding: 8px 16px; border-radius: 15px 15px 5px 5px; margin-left: 2px; text-align: center; display: inline-block; float: right; cursor: pointer; transition: all 200ms; text-decoration: none; word-break: normal; box-sizing: content-box; /* animation-duration: 1s; */ animation-name: animate-reply; animation-play-state: paused; animation-fill-mode: forwards; /* opacity: 0; */ transform: translate3d(0px, 0px, 0px); animation-delay: -3s; -ms-animation-delay: -3; -webkit-animation-delay: -3s; } @keyframes animate-reply { from { opacity: 0; } to { opacity: 1; } } .bubble.reply.say .bubble-content .bubble-button { animation-play-state: running; margin-top: 3px; min-height: 24px; overflow: hidden; } .bubble.reply .bubble-content .bubble-button:first-child { border-radius: 15px 15px 15px 5px; margin-left: 2px; } .bubble.reply .bubble-content .bubble-button:last-child, .bubble.reply .bubble-content .bubble-button.bubble-pick { border-radius: 15px 15px 5px 15px; } .bubble.reply.bubble-picked .bubble-content .bubble-button { transform: scale(0) translate3d(0px, 0px, 0px); padding: 0; } .bubble.reply:not(.bubble-picked) .bubble-content .bubble-button:hover, .bubble.reply .bubble-content .bubble-button.bubble-pick { background: rgba(44, 44, 44, 1); transform: scale(1) translate3d(0px, 0px, 0px); padding: 8px 16px; height: auto; } /* interaction recall styles */ .bubble.history .bubble-content .bubble-button, .bubble.history.reply:not(.bubble-picked) .bubble-content .bubble-button:hover, .bubble.history.reply .bubble-content .bubble-button.bubble-pick { background: rgba(44, 44, 44, 0.67); cursor: default; } /* input fields for bubbles */ .bubble .bubble-content input { background: linear-gradient(193deg, #1faced, #5592dc 100%) !important; box-shadow: 0 0px 1px 0px #000, 0 -1px 0 0px rgba(255, 255, 255, 0.38) inset; text-shadow: 0 1px rgba(0, 0, 0, 0.35); border: 0; outline: 0; } .bubble .bubble-content input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ color: rgba(255, 255, 255, .5); text-shadow: none; } .bubble .bubble-content input::-moz-placeholder { /* Firefox 19+ */ color: rgba(255, 255, 255, .5); text-shadow: none; } .bubble .bubble-content input:read-only { background: linear-gradient(166deg, #48121d, #0d4058 100%) !important; } /* style bubbles */ .bubble, .bubble-typing { color: #212121; background: rgba(255, 255, 255, 0.84); padding: 8px 16px; border-radius: 5px 15px 15px 15px; font-weight: 400; text-transform: none; text-align: left; font-size: 16px; letter-spacing: .5px; margin: 0 0 2px 0; max-width: 65%; float: none; clear: both; line-height: 1.5em; word-break: break-word; transform-origin: left top; transition: all 200ms; box-sizing: content-box; } .bubble .bubble-content { transition: opacity 150ms; } .bubble:not(.say) .bubble-content { opacity: 0; } .bubble-typing.imagine, .bubble.imagine { transform: scale(0); transition: all 200ms, height 200ms 1s, padding 200ms 1s; } .bubble.imagine { margin: 0; height: 0; padding: 0; } .bubble .bubble-content img { width: calc(100% + 32px); margin: -8px -16px; overflow: hidden; display: block; border-radius: 5px 15px 15px 15px; } /* interaction recall styles */ .bubble.history, .bubble.history .bubble-content, .bubble.history .bubble-content .bubble-button, .bubble.history .bubble-content .bubble-button:hover { transition: all 0ms !important; } .bubble.history { opacity: .25; } /* setup container styles */ .bubble-container { background: #dcdde0; height: 520px; max-width: 750px; width: 100%; margin: 0 auto; overflow: hidden; position: relative; } .bubble-wrap { position: absolute; top: 0; bottom: 0; left: 0; right: -17px; padding: 10px calc(17px + 10px) 30px 10px; overflow-y: scroll; -webkit-overflow-scrolling: touch; -webkit-transform: translate3d(0, 0, 0); } /* optional page styles */ h1 { text-align: center; font-weight: 300; font-size: 4em; margin: .5em auto 0.15em; } body { font-family: "Helvetica Neue", Helvetica, sans-serif; margin: 0; } /* style "loading" or "typing" stae */ .bubble-typing { width: 38px; padding: 12px 16px; height: 8px; } .dot { background-color: rgb(255,255,255); float: left; height: 7px; margin-left: 4px; width: 7px; animation-name: bounce_dot; animation-duration: 2.24s; animation-iteration-count: infinite; animation-direction: normal; border-radius: 5px; } .dot_1 { animation-delay: 0.45s; } .dot_2 { animation-delay: 1.05s; } .dot_3 { animation-delay: 1.35s; } @keyframes bounce_dot { 0% {} 50% { background-color:rgb(0,0,0); } 100% {} } body { background: #dcdde0; } .bubble-container { height: 100vh; } </style>');function Bubbles(container,self,options){options=typeof options!=="undefined"?options:{}
animationTime=options.animationTime||200
typeSpeed=options.typeSpeed||5
widerBy=options.widerBy||2
sidePadding=options.sidePadding||6
recallInteractions=options.recallInteractions||0
inputCallbackFn=options.inputCallbackFn||!1
var standingAnswer="ice"
var _convo={}
var localStorageCheck=function(){var test="chat-bubble-storage-test"
try{localStorage.setItem(test,test)
localStorage.removeItem(test)
return!0}catch(error){console.error("Your server does not allow storing data locally. Most likely it's because you've opened this page from your hard-drive. For testing you can disable your browser's security or start a localhost environment.")
return!1}}
var localStorageAvailable=localStorageCheck()&&recallInteractions>0
var interactionsLS="chat-bubble-interactions"
var interactionsHistory=(localStorageAvailable&&JSON.parse(localStorage.getItem(interactionsLS)))||[]
interactionsSave=function(say,reply){if(!localStorageAvailable)return
if(interactionsHistory.length>recallInteractions)
interactionsHistory.shift()
if(say.includes("bubble-button")&&reply!=="reply reply-freeform"&&reply!=="reply reply-pick")
return
interactionsHistory.push({say:say,reply:reply})}
interactionsSaveCommit=function(){if(!localStorageAvailable)return
localStorage.setItem(interactionsLS,JSON.stringify(interactionsHistory))}
container.classList.add("bubble-container")
var bubbleWrap=document.createElement("div")
bubbleWrap.className="bubble-wrap"
container.appendChild(bubbleWrap)
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){if(e.keyCode==13){e.preventDefault()
typeof bubbleQueue!==!1?clearTimeout(bubbleQueue):!1
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"):!1
addBubble('<span class="bubble-button bubble-pick">'+this.value+"</span>",function(){},"reply reply-freeform")
typeof callbackFn==="function"?callbackFn({input:this.value,convo:_convo,standingAnswer:standingAnswer}):!1
this.value=""}})
container.appendChild(inputWrap)
bubbleWrap.style.paddingBottom="100px"
inputText.focus()}
inputCallbackFn?this.typeInput(inputCallbackFn):!1
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)
this.talk=function(convo,here){_convo=Object.assign(_convo,convo)
this.reply(_convo[here])
here?(standingAnswer=here):!1}
var iceBreaker=!1
this.reply=function(turn){iceBreaker=typeof turn==="undefined"
turn=!iceBreaker?turn:_convo.ice
questionsHTML=""
if(!turn)return
if(turn.reply!==undefined){turn.reply.reverse()
for(var i=0;i<turn.reply.length;i++){;(function(el,count){questionsHTML+='<span class="bubble-button" style="animation-delay: '+animationTime/2*count+'ms" onClick="'+self+".answer('"+el.answer+"', '"+el.question+"');this.classList.add('bubble-pick')\">"+el.question+"</span>"})(turn.reply[i],i)}}
orderBubbles(turn.says,function(){bubbleTyping.classList.remove("imagine")
questionsHTML!==""?addBubble(questionsHTML,function(){},"reply"):bubbleTyping.classList.add("imagine")})}
this.answer=function(key,content){var func=function(key){typeof window[key]==="function"?window[key]():!1}
_convo[key]!==undefined?(this.reply(_convo[key]),(standingAnswer=key)):func(key)
if(_convo[key]!==undefined&&content!==undefined){interactionsSave('<span class="bubble-button reply-pick">'+content+"</span>","reply reply-pick")}}
this.think=function(){bubbleTyping.classList.remove("imagine")
this.stop=function(){bubbleTyping.classList.add("imagine")}}
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 bubbleQueue=!1
var addBubble=function(say,posted,reply,live){reply=typeof reply!=="undefined"?reply:""
live=typeof live!=="undefined"?live:!0
var animationTime=live?this.animationTime:0
var typeSpeed=live?this.typeSpeed:0
var bubble=document.createElement("div")
var bubbleContent=document.createElement("span")
bubble.className="bubble imagine "+(!live?" history ":"")+reply
bubbleContent.className="bubble-content"
bubbleContent.innerHTML=say
bubble.appendChild(bubbleContent)
bubbleWrap.insertBefore(bubble,bubbleTyping)
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=""):!1
el.removeAttribute("onclick")})(bubbleButtons[i])}
this.classList.add("bubble-picked")})}
wait=live?animationTime*2:0
minTypingWait=live?animationTime*6:0
if(say.length*typeSpeed>animationTime&&reply==""){wait+=typeSpeed*say.length
wait<minTypingWait?(wait=minTypingWait):!1
setTimeout(function(){bubbleTyping.classList.remove("imagine")},animationTime)}
live&&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("<img src=")?"50%":bubble.style.width
bubble.classList.add("say")
posted()
interactionsSave(say,reply)
!iceBreaker&&interactionsSaveCommit()
containerHeight=container.offsetHeight
scrollDifference=bubbleWrap.scrollHeight-bubbleWrap.scrollTop
scrollHop=scrollDifference/200
var scrollBubbles=function(){for(var i=1;i<=scrollDifference/scrollHop;i++){;(function(){setTimeout(function(){bubbleWrap.scrollHeight-bubbleWrap.scrollTop>containerHeight?(bubbleWrap.scrollTop=bubbleWrap.scrollTop+scrollHop):!1},i*5)})()}}
setTimeout(scrollBubbles,animationTime/2)},wait+animationTime*2)}
for(var i=0;i<interactionsHistory.length;i++){addBubble(interactionsHistory[i].say,function(){},interactionsHistory[i].reply,!1)}}
function prepHTML(options){var options=typeof options!=="undefined"?options:{}
var container=options.container||"chat"
var relative_path=options.relative_path||"./node_modules/chat-bubble/"
window[container]=document.createElement("div")
window[container].setAttribute("id",container)
document.body.appendChild(window[container])}
document.body.innerHTML+='<div style="position: fixed; bottom: 10px; right: 10px; width: 300px; height: 400px; border: 3px solid #000"> <div id="chat"></div></div>';var chatWindow=new Bubbles(document.getElementById("chat"),"chatWindow")
chatWindow.talk({ice:{says:["Hey!","Can I have a banana?"],reply:[{question:"?",answer:"banana"}]},banana:{says:["Thank you!","Can I have another banana?"],reply:[{question:"??",answer:"banana"}]}})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment