Skip to content

Instantly share code, notes, and snippets.

@jgrant41475
Created August 8, 2018 00:08
Show Gist options
  • Save jgrant41475/9e522a809377acfee393a133fa1d4751 to your computer and use it in GitHub Desktop.
Save jgrant41475/9e522a809377acfee393a133fa1d4751 to your computer and use it in GitHub Desktop.
Attendant
<!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>
@media screen and (max-width: 360px) {
#Attendant {
display: none;
}
}
#QuestionQuestionBox>* {
display: none;
}
#Attendant {
border: 3px solid #000;
background-color: #FFF;
position: fixed;
bottom: 10px;
right: 35px;
min-height: 300px;
min-width: 250px;
}
#questionHeader {
font-weight: bold;
border-bottom: 2px solid black;
margin-bottom: 10px;
}
.AttendantWindow {
text-align: center;
}
#QuestionBox {
text-align: left;
margin: 5px 5px;
min-height: 250px;
}
#BackButton {
margin: 5px auto;
}
.question {
border: 3px solid #00F;
border-radius: 25px;
padding: 10px 5px;
margin: 5px auto;
text-align: center;
font-weight: bold;
width: 80%;
}
.requiredInput {
border: 2px solid #F00 !important;
}
.default_form_submit {
float: right;
}
.fieldsContainer {
display: none;
}
.attendantCloseButton {
cursor: pointer;
position: absolute;
top: -25px;
right: 0px;
}
#attendantMinimized {
display: none;
position: fixed;
right: 35px;
bottom: 10px;
background-color: #008cba !important;
}
#Question-1-1 {
width: 70%;
}
</style>
</head>
<body>
<!-- Attendant element -->
<div id="Attendant"></div>
<div id="attendantMinimized">Click for attendant!</div>
<div id="customFormElement1" style="display:none;">
<div id="serviceAreaCheck">
<div>Input:</div>
<input type="text" id="SAInput" />
</div>
<script>
$(document).on("widgetLoad", function () {
if (document.getElementById("welcomeMessage") == null)
$("#serviceAreaCheck").append("<div id='welcomeMessage'></div>");
let welcomeMessageElem = $("#welcomeMessage");
welcomeMessageElem.text("Welcome, " + (self.attendant.getFieldFromLocal("visitor_name") || "friend") + "!");
$("#SAInput").val(self.attendant.getFieldFromLocal("visitor_address"));
// $(document).off("widgetLoad");
})
</script>
</div>
<!-- Custom attendant selection -->
<div id="servicesProvided" style="display:none;">
<ul id="servicesProvidedList"></ul>
<script>
$(document).on("servicesProvidedLoaded", function () {
console.log("fish")
let servicesList = ["A/C", "Heating", "Duct Cleaning"],
servicesListElem = $("#servicesProvidedList");
servicesList.forEach(function (service) {
servicesListElem.append($("<li></li>").text(service));
})
$(document).off("servicesProvidedLoaded");
})
</script>
</div>
<script>
// The JSON object that the attendant maps to
var assistantTree = {
id: "1",
revision: 1,
header: "Is this an emergency?",
options: [
{
id: "1-1",
question: "Yes!",
header: "Call Now!",
hasFields: true,
options: [
{
customContainer: '<div class="row text-center"><a href="#" style="color: #FFF;"><div class="small-6 columns small-centered button highlight">Click to Call!</div></a></div><br><div class="text-center">This is a custom element!</div>'
}
]
},
{
id: "1-2",
question: "Nope!",
header: "Visitor Information",
hasFields: true,
options: [
{
id: "1-2-1",
header: "Select an Option.",
onSubmit: true,
options: [
{
id: "1-2-1-1",
question: "Schedule",
options: null
},
{
id: "1-2-1-2",
question: "Cost of...",
redirectTo: "1-3",
options: []
},
{
id: "1-2-1-3",
question: "See Services",
redirectTo: "1-3",
options: []
}
]
},
{
question: "What is your name?",
inputElem: '<input type="text" id="visitor_name" value="" />',
validation: new SimpleValidators().isNotBlank
},
{
question: "What is your phone number?",
inputElem: '<input type="text" id="visitor_phone" value="" />',
validation: new SimpleValidators().isValidPhone
},
{
question: "What is your email?",
inputElem: '<input type="text" id="visitor_email" value="" />',
validation: new SimpleValidators().isValidEmail
}, { defaultSubmit: true }
]
},
{
id: "1-3",
header: "Services Provided",
hasFields: true,
hidden: true,
options: [
{
useExternalId: "servicesProvided",
useTrigger: "servicesProvidedLoaded"
}
]
}
]
}
var actualTree = {
id: "1",
revision: 1,
header: "Is this an emergency?",
options: [
{
id: "1-1",
question: "Yes!",
header: "Call Now!",
hasFields: true,
options: [
{
customContainer: '<div class="row text-center"><a href="#" style="color: #FFF;"><div class="small-6 columns small-centered button highlight">Click to Call!</div></a></div><br><div class="text-center">This is a custom element!</div>'
}
]
},
{
id: "1-2",
question: "Nope!",
header: "Visitor Information",
hasFields: true,
options: [
{
id: "1-2-1",
header: "Select an Option.",
onSubmit: true,
options: [
{
id: "1-2-1-1",
question: "Schedule",
header: "Schedule a Service",
options: []
},
{
id: "1-2-1-2",
question: "Cost of...",
options: null
},
{
id: "1-2-1-3",
question: "See Services:",
redirectTo: "1-3",
options: []
}
]
},
{
question: "What is your name?",
inputElem: '<input type="text" id="visitor_name" value="" />',
validation: new SimpleValidators().isNotBlank
},
{
question: "What is your phone number?",
inputElem: '<input type="text" id="visitor_phone" value="" />',
validation: new SimpleValidators().isValidPhone
},
{
question: "What is your email?",
inputElem: '<input type="text" id="visitor_email" value="" />',
validation: new SimpleValidators().isValidEmail
}, { defaultSubmit: true }
]
},
{
id: "1-3",
header: "Services Provided",
hasFields: true,
hidden: true,
options: [
{
useExternalId: "servicesProvided",
useTrigger: "servicesProvidedLoaded"
}
]
},
{
id: "1-4",
question: "Service Area Confirmation",
header: "Service Area Confirmation",
hasFields: true,
options: [
{
id: "1-4-1",
header: "Checking...",
onSubmit: true,
hasFields: true,
options: [
{
useExternalId: "customFormElement1",
useTrigger: "widgetLoad"
}
]
},
{
question: "What is your address?",
inputElem: '<input type="text" id="visitor_address" value="" />',
validation: new SimpleValidators().isNotBlank
}, { defaultSubmit: true }
]
}
]
};
var realTree = {
id: "1",
header: "Is this an emergency?",
options: [
{
id: "1-1",
question: "Yes!",
header: "Call Now!",
hasFields: true,
options: [
{
customContainer: '<div class="row text-center"><a href="#" style="color: #FFF;"><div class="small-6 columns small-centered button highlight">Click to Call!</div></a></div>'
}
]
},
{
id: "1-2",
header: "Visitor Information",
question: "Nope!",
hasFields: true,
options: [
{
id: "1-2-1",
header: "Hello!",
onSubmit: true,
options: [
{
id: "1-2-1-1",
question: "Email",
header: "Wabbajack",
options: [
{
id: "1-2-1-1-1",
question: "Salami",
option: []
}
]
}
]
},
{
question: "What is your name?",
inputElem: '<input type="text" id="visitor_name" value="" />',
validation: function (elem) {
let inputField = $(elem).find("input");
if (inputField.val() == "") {
inputField.addClass("requiredInput");
return false;
}
inputField.removeClass("requiredInput");
return { qid: inputField.attr("id"), data: inputField.val() };
},
},
{
question: "What is your email address?",
inputElem: '<input type="text" id="visitor_email" value="" />',
validation: function (elem) {
let inputField = $("#visitor_email");
if (inputField.val() == "") {
inputField.addClass("requiredInput");
return false;
}
inputField.removeClass("requiredInput");
return { qid: "visitor_email", data: inputField.val() };
}
}, { defaultSubmit: true }
]
}
]
}
var tree = {
id: "1",
revision: 2,
header: "Question 1!",
question: "Question 1",
options: [
{
id: "1-1",
header: "Selected 1-1!",
question: "Question 1-1",
options: [
{
id: "1-1-1",
header: "Selected 1-1-1!",
question: "Question 1-1-1",
options: []
}
]
},
{
id: "1-2",
header: "Selected 1-2!",
question: "Question 1-2",
options: [
{
id: "1-2-1",
header: "Selected 1-2-1!",
question: "Question 1-2-1",
options: [
{
id: "1-2-1-1",
header: "Selected 1-2-1-1!",
question: "Question 1-2-1-1",
hasFields: true,
options: [
{
id: "1-2-1-1-1",
header: "Pepparoni Cows",
question: "Hot Spice",
options: [],
onSubmit: true
},
{
question: "What is your first name?",
inputElem: '<input type="text" id="first_name" value="" />',
validation: new SimpleValidators().isNotBlank
},
{
question: "What is your last name?",
inputElem: '<input type="text" id="last_name" value="" />'
}, { defaultSubmit: true }
]
},
{
id: "1-2-1-2",
header: "Selected 1-2-1-2!",
question: "Question 1-2-1-2",
options: null
}
]
},
{
id: "1-2-2",
header: "Selected 1-2-2!",
question: "Question 1-2-2",
operation: function () {
alert("Hello");
},
options: null
}
]
}
]
};
// The actual Attendant function declaration
let Attendant = function (inputTree, hist) {
this.init = function () {
if (self.storageAvailable()) {
let storage = self.getStorage(), revision = storage.getItem("revision_id");
if (revision != self.tree.revision) {
storage.clear();
self.history = [self.tree.id];
storage.setItem("revision_id", self.tree.revision)
}
}
self.parseTree(this.tree.options);
this.selectQuestion((this.history.length == 0) ? this.tree.id : this.history.pop());
$("#attendantMinimized, #attendantMinimized *").click(function () { self.toggleAttendant(); });
$(".attendantCloseButton").click(function () { self.toggleAttendant(); });
$(".question").click(function () { self.selectQuestion(this.id); });
$("#BackButton").click(function () { self.selectPrevious(); });
$(".form_submit").click(function () {
let $this = $(this), id = $this.data("submitfor");
if (id != null && self.validateForm($this.data("parentid")) != false)
self.selectQuestion(id);
});
};
this.selectQuestion = function (id) {
if (id.slice(0, 9) == "Question-")
id = id.slice(9);
let treeRef = this.getID(id);
if (treeRef == null)
return;
if (treeRef.redirectTo != null)
return self.selectQuestion(treeRef.redirectTo);
if (treeRef.operation != null)
treeRef.operation();
if (treeRef.options != null) {
this.history.push(id);
$(".question").hide();
$(".fieldContainer").hide();
$("#questionHeader").text((treeRef.header != null) ? treeRef.header : "");
$(".question[id*=Question-" + id + "]").each(function (index, elem) {
let i = elem.id.slice(9);
if (i != id && i.slice(id.length + 1).indexOf("-") == -1)
$(elem).show();
});
treeRef.options.filter(function (x) { return x.hidden == true; }).forEach(function (x) { $("#Question-" + x.id).hide(); });
treeRef.options.filter(function (x) { return x.useTrigger != null; }).forEach(function (x) { $(document).trigger(x.useTrigger, self); });
}
if (treeRef.hasFields == true) {
$(".fieldContainer[data-fieldfor=" + treeRef.id + "]").show();
treeRef.options.filter(function (x) { return x.id != null; }).forEach(function (x) { $("#Question-" + x.id).hide(); });
}
self.updateStorage();
};
this.getID = function (idString) {
if (idString.slice(0, 9) == "Question-")
idString = idString.slice(9);
let idStringList = idString.split("-"),
temp = this.tree,
getTreeFromID = function getTreeFromID(id, inputTree) {
if (inputTree.options != null && inputTree.options.length >= id && id > 0)
return inputTree.options[id - 1];
else return null;
};
for (let i = 1, max = idStringList.length; i < max; i++) {
if (temp == null) {
temp = this.tree;
break;
}
temp = getTreeFromID(idStringList[i], temp);
}
return temp;
};
this.createQuestionFromRef = function (ref) {
if (ref.id == null)
return;
let elem = $(document.createElement("div"));
elem.addClass("question").attr({ "id": "Question-" + ref.id });
elem.text(ref.question);
if (ref.hasFields == true)
$("#QuestionBox").append(self.createFieldsFromRef(ref));
return elem;
};
this.createFieldsFromRef = function (ref) {
if (ref.id == null)
throw new Error("Invalid ID");
let fieldsContainer = $('<div class="fieldContainer" data-fieldfor="' + ref.id + '"></div>'), submitId = null;
ref.options.forEach(function (field) {
let tempContainer = $('<div class="Field"></div>');
if (field.onSubmit == true) {
submitId = field.id;
self.parseTree(ref.options);
}
else if (field.useExternalId != null) {
tempContainer = $("#" + field.useExternalId);
tempContainer.show();
}
else if (field.customContainer != null)
tempContainer = field.customContainer;
else if (field.defaultSubmit == true)
tempContainer.append($('<input type="button" class="form_submit default_form_submit" data-parentID="' + ref.id + '" data-submitfor="' + submitId + '" value="Next" />'));
else {
let textElem, inputElem;
if (field.customQuestion != null)
textElem = $(field.customQuestion);
else {
textElem = $('<div class="fieldText"></div>');
textElem.text(field.question);
}
if (field.customInput)
inputElem = $(field.customInput);
else
inputElem = $(field.inputElem);
let answer = self.getFieldFromLocal(inputElem.attr("id"));
if (answer != null)
inputElem.val(answer);
tempContainer.append(textElem);
tempContainer.append(inputElem);
}
fieldsContainer.append(tempContainer);
});
return fieldsContainer;
};
this.selectPrevious = function () {
if (this.history != null && this.history.length > 1) {
this.history.pop();
this.selectQuestion(this.history.pop());
}
};
this.getStorage = function () { return (self.storageAvailable(self.storageType) == true) ? window[self.storageType] : null; };
this.updateStorage = function () {
if (!self.storageAvailable(self.storageType))
return;
let storage = self.getStorage();
if (self.history.length == null)
storage.removeItem(self.storageKey);
else
storage.setItem(self.storageKey, self.history.join("\t"));
};
this.getHistory = function () {
if (!self.storageAvailable(self.storageType))
return;
let storage = self.getStorage();
let local = storage.getItem(self.storageKey);
return (local != null) ? local.split("\t") : [];
};
this.parseTree = function parseTree(arr) {
let container = $("#QuestionBox"), cur = null;
for (let i in arr) {
cur = arr[i];
if (cur.id != undefined)
container.append(self.createQuestionFromRef(cur));
if (cur.options != null && cur.hasFields != true)
parseTree(cur.options);
}
}
this.validateForm = function (id) {
let ref = self.getID(id);
let container = $('.fieldContainer[data-fieldfor="' + ref.id + '"]').children();
if (ref == null || ref.hasFields != true)
return false;
if (ref.options.length != container.length)
return false;
let rValue = true;
ref.options.forEach(function (refElem, index) {
if (refElem.validation != null) {
let validated = refElem.validation(container[index]);
if (validated == false)
rValue = false;
else if (validated.qid != null && validated.data != null)
self.saveFieldToLocal(validated.qid, validated.data);
}
});
return rValue;
};
this.saveFieldToLocal = function (id, data) {
if (!self.storageAvailable(self.storageType))
return;
let storage = self.getStorage();
storage.setItem("Attendant-" + id, data);
};
this.getFieldFromLocal = function (id) {
if (!self.storageAvailable(self.storageType))
return;
let storage = self.getStorage();
return storage.getItem("Attendant-" + id);
}
this.storageAvailable = function (type) {
if (type == null)
type = self.storageType;
try {
let 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.toggleAttendant = function () {
$("#Attendant").toggle();
$("#attendantMinimized").toggle();
};
let self = this;
$("div#Attendant").append($('<div class="AttendantWindow"></div>'))
.find(".AttendantWindow")
.append($('<div id="questionHeader"></div>'))
.append($('<div id="QuestionBox"></div>'))
.append($('<div class="button highlight" id="BackButton">Back</div>'))
.append($('<div class="attendantCloseButton"><i class="fas fa-window-close"></i></div>'));
this.tree = inputTree;
this.storageType = "localStorage"
this.storageKey = "Attendant_History";
this.history = self.getHistory();
}
// Provides default methods of validation for simple cases
function SimpleValidators() {
this.isNotBlank = function (elem) {
let inputField = $(elem).find("input");
if (inputField.val().trim() == "") {
inputField.addClass("requiredInput");
return false;
}
inputField.removeClass("requiredInput");
return { qid: inputField.attr("id"), data: inputField.val() };
};
this.isValidPhone = function (elem) {
let inputField = $(elem).find("input");
let inputVal = inputField.val();
if (typeof inputVal != "string" || inputVal.match(/^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/) == null) {
inputField.addClass("requiredInput");
return false;
}
inputField.removeClass("requiredInput");
return { qid: inputField.attr("id"), data: inputVal };
};
this.isValidEmail = function (elem) {
let inputField = $(elem).find("input");
let inputVal = inputField.val();
if (typeof inputVal != "string" || inputVal.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) {
inputField.addClass("requiredInput");
return false;
}
inputField.removeClass("requiredInput");
return { qid: inputField.attr("id"), data: inputVal };
}
}
// Initilialize the attendant
var attendant = new Attendant(actualTree);
attendant.init();
</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