Last active
August 26, 2018 00:54
-
-
Save jgrant41475/4a0ddab0dda99817c428ccde2c21a799 to your computer and use it in GitHub Desktop.
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
<!doctype html> | |
<html class="no-js" lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Foundation | Welcome</title> | |
<link rel="stylesheet" href="css/foundation.css" /> | |
<script src="js/vendor/modernizr.js"></script> | |
<script src="js/vendor/jquery.js"></script> | |
<script src="js/foundation.min.js"></script> | |
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" | |
crossorigin="anonymous"> | |
<script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js" integrity="sha384-3LK/3kTpDE/Pkp8gTNp2gR/2gOiwQ6QaO7Td0zV76UFJVhqLl4Vl3KL1We6q6wR9" | |
crossorigin="anonymous"></script> | |
<style> | |
#Assistant { | |
border: 3px solid #000; | |
background-color: #FFF; | |
position: absolute; | |
bottom: 10px; | |
right: 35px; | |
min-height: 56px; | |
max-height: 70%; | |
min-width: 250px; | |
} | |
#AssistantMinimized { | |
display: none; | |
cursor: pointer; | |
position: fixed; | |
right: 35px; | |
bottom: 10px; | |
background-color: #008cba !important; | |
height: 50px; | |
min-width: 250px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 14pt; | |
padding-top: 10px; | |
} | |
#BackButton { | |
position: fixed; | |
margin-top: -50px; | |
min-width: 244px; | |
height: 50px; | |
} | |
.AssistantWindows { | |
margin: 0; | |
padding: 0; | |
} | |
.AssistantWindow { | |
display: none; | |
overflow: auto; | |
min-height: 250px; | |
max-height: 350px; | |
max-width: 244px; | |
margin-bottom: 60px; | |
} | |
.AssistantCloseButton { | |
cursor: pointer; | |
position: absolute; | |
top: -25px; | |
right: 0px; | |
} | |
.WindowHeader { | |
text-align: center; | |
border-bottom: 2px solid #000; | |
} | |
.AssistantDefaultSubmit { | |
float: right; | |
margin-right: 10px; | |
} | |
.AssistantOptionList { | |
list-style: none; | |
margin: 0; | |
} | |
.AssistantOptionList > li { | |
cursor: pointer; | |
text-align: center; | |
margin: 10px auto; | |
border: 2px solid #00F; | |
border-radius: 20px; | |
width: 70%; | |
} | |
.requiredInput { | |
border: 2px solid #F00 !important; | |
} | |
@media screen and (max-width: 600px) { | |
/* */ | |
#Assistant, #AssistantMinimized { | |
left: 35px; | |
} | |
.AssistantWindow { | |
max-width: 100%; | |
} | |
#BackButton { | |
left: 38px; | |
right: 38px; | |
} | |
} | |
</style> | |
<script> | |
let Assistant = function(inputTree){ | |
const self = this, | |
storagePreface = "ASSISTANT-", // Preface for all storage keys | |
storageType = "localStorage", // type of storage to use | |
lastUsedCookie = "ASSISTANT-usecache", // Cookie name to look for to determine whether to use cached data or reset | |
historyDelimeter = '\0'; // History ids separated by this character | |
this.storage = null; | |
this.tree = inputTree || []; | |
this.history = []; | |
this.init = function() { | |
if($("#Assistant").length != 0) | |
throw Error("Only one assistant can be created."); | |
this.storage = this.getStorage(); | |
if(this.storage == null) | |
throw Error("Web storage is not available."); | |
else { | |
if(this.tree.length == 0) | |
throw Error("Invalid input tree"); | |
else { | |
// If revision or id doesn't match cached values or lastUsedCookie isn't set, clear all stored data | |
const rev = this.getLocal("revision_id"), | |
id = this.getLocal("id"), | |
useCache = document.cookie.split(";") | |
.map(function(x) { return x.split("="); }) | |
.map(function(k, v) { return [k[0].trim(), k[1]]; }) | |
.filter(function(x) { return x[0] == lastUsedCookie; }) | |
.length > 0; // Returns true if cookie exists, else false | |
expirationDate = (function(now, weeks){ return new Date(now.getFullYear(), now.getMonth(), now.getDate()+(weeks * 7)); })(new Date(), 2); // 2 weeks | |
if(id != this.tree.id || rev != this.tree.revision || useCache == false) { | |
console.log("Purging assistant storage data."); | |
this.purgeLocal(); | |
this.setLocal("revision_id", this.tree.revision); | |
this.setLocal("id", this.tree.id); | |
} | |
// Sets cookie with an expriation set for 2 weeks, as long as this cookie exists the assistant will use the cached data | |
document.cookie = lastUsedCookie + "=true;expires=" + expirationDate + ";path=/;"; | |
// Insert Assistant into the page | |
this.initWidget(this.tree); | |
// Fetch cached history if available | |
const hist = this.getLocal("history"); | |
if(hist != null) | |
this.history = hist.split(historyDelimeter); | |
if(this.tree.default == null) | |
this.tree.default = this.tree.windows[0].id; | |
// Selects the last active window from history, or the default window if no history is available | |
this.selectId(this.history.pop() || this.tree.default); | |
if(this.getLocal("user_closed") != null) { | |
// don't open assistant | |
} else if(hist == null) { | |
setTimeout(function(){ if(self.getLocal("user_closed") == null) self.toggleAssistant(); }, 5000); // Delay assistant popup | |
} else this.toggleAssistant(); // User has interacted with assistant already, show immediately | |
} | |
} | |
}; | |
this.selectId = function(id) { | |
const assistantWindow = $(".AssistantWindow"); | |
// Selects the HTML container for the given id, if there is one | |
const elem = assistantWindow.filter(function(i, e){ return $(e).data("window_id") == id; }); | |
if(elem.length == 0) { | |
// throw Error("Invalid ID"); | |
console.log("Invalid ID: " + id); | |
} else { | |
// Show selected window, update history | |
assistantWindow.hide(); | |
elem.show(); | |
this.history.push(id); | |
this.setLocal("history", this.history.join(historyDelimeter)); | |
const ref = this.getRef(id); | |
if(ref == null) | |
return; | |
if(ref.trigger != null) | |
$(document).trigger(ref.trigger, this); | |
} | |
}; | |
this.selectPrevious = function() { | |
// Select the previous window from history, or default window if history is empty | |
if(this.history == null) | |
return; | |
this.history.pop(); | |
if(this.history.length > 0) { | |
this.selectId(this.history.pop()); | |
} else this.selectId(this.tree.default); | |
} | |
this.getRef = function(id, inTree) { | |
// returns the JSON object with the given id | |
if(id == null) | |
return null; | |
const wins = inTree || this.tree.windows || []; | |
let returnRef = null; | |
for(const i in wins) { | |
const win = wins[i]; | |
if(returnRef != null) | |
break; | |
if(win.id == id) | |
returnRef = win; | |
else { | |
if(win.options != null) | |
returnRef = self.getRef(id, win.options); | |
if(win.fields != null) | |
returnRef = self.getRef(id, win.fields); | |
} | |
} | |
return returnRef; | |
}; | |
this.toggleAssistant = function (userClick) { | |
// Toggles assistant window and assistant minimized button | |
$("#Assistant, #AssistantMinimized").toggle(); | |
if(userClick == true) // If this is user interaction, save visibilty state | |
if(this.remLocal("user_closed") == false) // If user_closed doesn't exist, returns false | |
this.setLocal("user_closed", true); | |
}; | |
this.initWidget = function(obj) { | |
// Insert assistant into the page | |
const att = $('<div id="Assistant"></div>'); | |
$("body").append(att).append('<div id="AssistantMinimized">Minimized</div>'); | |
$("#Assistant").append($('<div id="AssistantWindows"></div>')) | |
.append($('<div class="button highlight" id="BackButton">Back</div>')) | |
.append($('<div class="AssistantCloseButton"><i class="fas fa-window-close"></i></div>')); | |
$("#AssistantMinimized, #AssistantMinimized *").click(function () { self.toggleAssistant(true); }); | |
$(".AssistantCloseButton").click(function () { self.toggleAssistant(true); }); | |
$("#BackButton").click(function(){ self.selectPrevious(); }); | |
this.toggleAssistant(); | |
// Create HTML elements from JSON | |
if(obj.windows != null) | |
this.createWindows(obj.windows); | |
// Clicking an option will load the appropriate window | |
$(".AssistantOptionList li").click(function(){ | |
const id = $(this).data("id"); | |
if(id == null) | |
return; | |
self.selectId(id); | |
const ref = self.getRef(id); | |
if(ref != null && typeof ref.operation == "function") | |
ref.operation(this, self); | |
}); | |
// Validate input when enter key is pressed, if valid selects next input element | |
$(".AssistantFieldContainer input").keypress(function(e){ | |
if(e.keyCode == "13") { | |
const id = self.getRef(this.id); | |
if(id != null) { | |
if(self.validateSingle(this)) { | |
const parent = $(this).parent(); | |
let next = parent.next().find("input"); | |
if(next.length == 0) | |
next = parent.parent().find("input").last(); // No more input fields, select last input element | |
next.focus(); | |
} | |
} | |
} | |
}); | |
}; | |
this.createWindows = function(windows){ | |
// Parse JSON tree and create AssistantWindows | |
const container = $("#AssistantWindows"); | |
if(container == null || windows == null) | |
return; | |
windows.forEach(function(win){ // Iter over everything | |
if(win.type != null) { | |
const id = win.id; | |
let windowContainer = $('<div class="AssistantWindow"></div>').data("window_id", id); | |
if(win.type == "ExternalWindow") { | |
const extern = $("#" + id); | |
if(extern == null) { | |
console.log("External ID '" + id + "' not found."); | |
} else { | |
container.append(extern.addClass("AssistantWindow CustomWindow").data("window_id", id)); | |
} | |
windowContainer = null; | |
} | |
else if(win.type != null) { // If there is a window type, send to WindowFactory to build container | |
windowContainer.addClass(win.type); | |
windowContainer.append(new self.WindowFactory(win.type).build(win, windowContainer)); | |
} | |
if(windowContainer != null) | |
container.append(windowContainer); // Append each window to the main AssistantWindows container | |
} | |
}); | |
}; | |
this.validate = function(id){ | |
// Validates user input, returns true if valid otherwise returns false | |
const win = $(".AssistantWindow").filter(function(i,x){ return $(x).data("window_id") == id; }); // Only select the windows the windows with correct id | |
let allValid = true; | |
if(win.length > 0) { | |
win.first().children(".AssistantFieldContainer").each(function(i, elem){ // Iter over every field container | |
const input = $(elem).find("input"); | |
if(input.length > 0) { | |
input.each(function(){ // Iter over every input field and validate | |
if(self.validateSingle(this) == false) | |
allValid = false; | |
}); | |
} | |
}); | |
return allValid; // If any inputs failed, this will return false | |
} | |
}; | |
this.validateSingle = function(field) { | |
const cur_id = field.id, | |
ref = self.getRef(cur_id); | |
let valid = true; | |
if(cur_id == null || ref == null) | |
throw Error("Invalid ID or reference: " + cur_id); | |
if(ref.validation != null) | |
valid = ref.validation(field.value); | |
if(valid == false) { // Input failed validation, set allValid to false and continue validating the rest of the inputs | |
$(field).addClass("requiredInput"); | |
self.remLocal(cur_id); | |
} | |
else { // Input passes validation, save user input to storage | |
$(field).removeClass("requiredInput"); | |
self.setLocal(cur_id, field.value); | |
} | |
return valid; | |
}; | |
this.setLocal = function(key, value) { | |
// Saves key to storage, doesn't allow blank input | |
if(this.storage == null || key == null || key == "" || value == null || value == "") | |
return false; | |
this.storage.setItem(storagePreface + key, value); | |
return true; | |
}; | |
this.getLocal = function(key) { | |
// Fetches key from storage | |
if(this.storage == null || key == null) | |
return null; | |
return this.storage.getItem(storagePreface + key); | |
}; | |
this.remLocal = function(key){ | |
// Removes key from storage | |
if(this.getLocal(key) == null) | |
return false; | |
this.storage.removeItem(storagePreface + key); | |
return true; | |
}; | |
this.purgeLocal = function() { | |
// Clears all assistant data | |
if(this.storage == null) | |
return false; | |
for(const item in this.getStorage()) { | |
if(typeof item == "string" && item.slice(0, storagePreface.length) == storagePreface) | |
this.storage.removeItem(item); | |
} | |
}; | |
// Returns the storage object if available, else null | |
this.getStorage = function(){ return this.storageAvailable() ? window[storageType] : null; }; | |
this.storageAvailable = function(type){ | |
// Determines whether the browser supports storage | |
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Testing_for_availability | |
if (type == null) | |
type = storageType; | |
try { | |
const storage = window[type], | |
x = '__storage_test__'; | |
storage.setItem(x, x); | |
storage.removeItem(x); | |
return true; | |
} | |
catch (e) { | |
return e instanceof DOMException && ( | |
// everything except Firefox | |
e.code === 22 || | |
// Firefox | |
e.code === 1014 || | |
// test name field too, because code might not be present | |
// everything except Firefox | |
e.name === 'QuotaExceededError' || | |
// Firefox | |
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && | |
// acknowledge QuotaExceededError only if there's something already stored | |
storage.length !== 0; | |
} | |
}; | |
this.WindowFactory = function(type) { | |
// Object that creates assistant windows, if a window has a type that is a function member of this object | |
// it will automatically pass the reference object to the appropriate function with the same name | |
this.DefaultOptionWindow = function(ref, win) { // Creates default option windows | |
win.append($('<div class="WindowHeader"></div>').text(ref.header || "Default Header")); | |
if(ref.options != null) { | |
const optList = $('<ul class="AssistantOptionList"></ul>'); | |
ref.options.forEach(function(opt){ | |
if(opt.type == "break") | |
optList.append($("<br>")) | |
else | |
optList.append($('<li></li>').text(opt.text).data("id", opt.id)); | |
}); | |
self.createWindows(ref.options); | |
win.append(optList); | |
} | |
return win; | |
}; | |
this.DefaultInputWindow = function(ref, win) { // Creates default input windows | |
win.append($('<div class="WindowHeader"></div>').text(ref.header || "Default Header")); | |
if(ref.fields != null) { | |
const factory = this; | |
ref.fields.forEach(function(field){ | |
if(field.type == null) | |
throw Error("Input field type required for: " + field.id); | |
if(factory.hasOwnProperty(field.type)) { | |
win.append(new self.WindowFactory(field.type).build(field, ref)); | |
} else console.log("Invalid Input Field Type: " + field.type + ", id = " + field.id); | |
}); | |
}; | |
return win; | |
} | |
this.DefaultInputField = function(ref) { // Creates default input fields | |
const fieldContainer = $('<div class="AssistantFieldContainer"></div>').data("input_for", ref.id), | |
label = $('<label></label>').attr("for", ref.id).text(ref.label), | |
input = $('<input type="text" />').attr("id", ref.id), | |
cachedValue = self.getLocal(ref.id); | |
if(cachedValue != null) | |
input.val(cachedValue); | |
return fieldContainer.append(label).append(input); | |
}; | |
this.DefaultSubmit = function(ref, win) { // Creates default submit button | |
const submit = $('<input type="button" class="AssistantDefaultSubmit" />').data("submit_for", win.id).val(ref.text || "Next").click(function(){ | |
if(win.options != null && self.validate(win.id)) | |
self.selectId(win.options[0].id); | |
}); | |
if(win.options != null) | |
self.createWindows(win.options); | |
return submit; | |
} | |
this.CustomHTML = function(ref) { // Creates window from HTML input as string | |
if(ref != null && ref.html != null) | |
return $(ref.html).data("id", ref.id); | |
return null; | |
}; | |
this.break = true; // Allow 'break' type | |
// Returns new window | |
this.build = function(ref, container) { | |
if(typeof this[this.type] == "function") | |
return this[this.type](ref, container); | |
}; | |
if(type == null || !this.hasOwnProperty(type)) // If type isn't a function member, throw an error | |
throw Error("Invalid window type: " + type); | |
this.type = type; | |
}; | |
this.SimpleValidators = function() { | |
// Provides validation for simple cases | |
this.isNotBlank = function(text) { // Validates that input isn't blank | |
if(text == null || text.trim() == "") | |
return false; | |
return true; | |
}; | |
this.isValidPhone = function(text) { // Validates that input is a phone number | |
if(text == null || text.match(/^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/) == null) | |
return false; | |
return true; | |
}; | |
this.isValidEmail = function(text) { // Validates that input is an email address | |
return (text == null || text.match(/^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/) == null) ? false : true; | |
}; | |
return this; | |
} | |
if(inputTree != null) // Initialize assistant | |
this.init(); | |
}; | |
</script> | |
</head> | |
<body> | |
<div class="AssistantWindow text-center" id="customWindow1"> | |
<div class="button" style="margin-top: 30px;"> | |
<a href="#" style="color: #FFF;">Call Now!</a> | |
</div> | |
<script> | |
$(document).on("customWindow1Loaded", function(e, assistantRef){ | |
console.log("Loaded!"); | |
$(document).off("customWindow1Loaded"); | |
}); | |
</script> | |
</div> | |
<div class="AssistantWindow" id="BusinessHoursWindow"> | |
<div class="WindowHeader">Office Hours</div> | |
<ul style="list-style: none; margin: 20px 0 0 0;" class="text-center" id="BusinessHoursList"></ul> | |
<script> | |
$(document).on("BusinessHoursLoaded", function(e, assistantRef){ | |
const week = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], | |
hoursList = $("#BusinessHoursList"); | |
"|8am - 5pm".repeat(7).split("|").slice(1).forEach(function(hours, i){ | |
hoursList.append($("<li></li>").text(week[i] + ": ").append($('<span id="' + week[i] + 'Hours"></span>').text(hours))); | |
}); | |
$("#" + week[new Date().getDay()] + "Hours").parent().css({"border": "2px solid black", "font-weight": "bold"}); | |
$(document).off("BusinessHoursLoaded"); | |
}); | |
</script> | |
</div> | |
<script> | |
const validators = new Assistant().SimpleValidators(); | |
const tree = { | |
id: "ExampleTree", | |
revision: 1, | |
default: 1, | |
windows: [ | |
{ | |
type: "DefaultOptionWindow", | |
id: "1", | |
header: "Is this an Emergency?", | |
options: [ | |
{ | |
type: "ExternalWindow", | |
id: "customWindow1", | |
trigger: "customWindow1Loaded", | |
text: "Yes" | |
}, | |
{ | |
type: "DefaultInputWindow", | |
id: "NotEmergency", | |
text: "No", | |
header: "Information", | |
fields: [ | |
{ | |
type: "DefaultInputField", | |
id: "visitor_name", | |
label: "What is your name?", | |
validation: validators.isNotBlank | |
}, | |
{ | |
type: "DefaultInputField", | |
id: "visitor_phone", | |
label: "What is your phone number?", | |
validation: validators.isValidPhone | |
}, | |
{ | |
type: "DefaultInputField", | |
id: "visitor_email", | |
label: "What is your email address?", | |
validation: validators.isValidEmail | |
}, {type:"DefaultSubmit"} | |
], | |
options: [{id: "MenuOptions"}] | |
} | |
] | |
}, | |
{ | |
type: "DefaultOptionWindow", | |
id: "MenuOptions", | |
header: "Select an Option", | |
text: null, | |
options: [ | |
{ | |
type: "ExternalWindow", | |
id: "BusinessHoursWindow", | |
text: "Hours", | |
trigger: "BusinessHoursLoaded", | |
operation: function(){ console.log(this.id + " clicked"); }, | |
options: null | |
}, | |
{ | |
type: "DefaultOptionWindow", | |
id: "ServiceMenuWindow", | |
header: "Services Menu", | |
text: "Services", | |
options: [ | |
{ | |
type: "DefaultOptionWindow", | |
id: "ACService", | |
text:"A/C", | |
header: "Air conditioning Services", | |
options: [ | |
{text:"AC Installation"}, | |
{text:"AC Repair"}, | |
{text:"AC Maintenance"}, | |
{type: "break"}, | |
{text:"Get a Quote!", id: "customWindow1"} | |
] | |
}, | |
{text:"Heating"}, | |
{text:"Plumbing"}, | |
{text:"Electrical"} | |
] | |
}, | |
{ | |
type: "CustomHTML", | |
id: "test", | |
text: "Click Me!", | |
html: '<div>This is a custom element created from a string.</div>' | |
} | |
] | |
} | |
] | |
}; | |
let assistant; | |
$(document).ready(function(){ assistant = new Assistant(tree); }); | |
</script> | |
<script>$(document).foundation();</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment