Skip to content

Instantly share code, notes, and snippets.

@jgrant41475
Last active August 26, 2018 00:54
Show Gist options
  • Save jgrant41475/4a0ddab0dda99817c428ccde2c21a799 to your computer and use it in GitHub Desktop.
Save jgrant41475/4a0ddab0dda99817c428ccde2c21a799 to your computer and use it in GitHub Desktop.
<!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