Last active
May 14, 2018 09:22
-
-
Save alexcdot/0487371dd5160de9e77d7fa838ae39b3 to your computer and use it in GitHub Desktop.
Create a web chat bubble using just one javascript file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var jokesonyou = 1; | |
alert("loaded a script!"); | |
document.body.innerHTML += '<h1>You loaded me</h1>'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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()">×</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()">×</span> </div> <div id="chat"></div></div></div><button id="myBtn" class="open-chat-button" onclick="displayModal()"><i class="material-icons"></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;}'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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