///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
interface Window { ConversationalForm: any; }
namespace cf {
// CUI options
export interface ConversationalFormOptions{
// HTMLFormElement
formEl: HTMLFormElement;
// context (HTMLElement) of where to append the ConversationalForm (see also cf-context attribute)
context?: HTMLElement;
// pass in custom tags (when prevent the auto-instantiation of ConversationalForm)
tags?: Array;
// overwrite the default user Dictionary items
dictionaryData?: Object;
// overwrite the default robot Dictionary items
dictionaryRobot?: Object;
//base64 || image url // overwrite user image, without overwritting the user dictionary
userImage?: string;
// base64 || image url // overwrite robot image, without overwritting the robot dictionary
robotImage?: string;
// custom submit callback if button[type=submit] || form.submit() is not wanted..
submitCallback?: () => void | HTMLButtonElement;
// can be set to false to allow for loading and packaging of Conversational Form styles within a larger project.
loadExternalStyleSheet?: boolean;
// Theme
theme?: String;
// prevent auto appending of Conversational Form, append it yourself.
preventAutoAppend?: boolean;
// start the form in your own time, {cf-instance}.start(), exclude cf-form from form tag, see examples: manual-start.html
preventAutoStart?: boolean;
// prevents the initial auto focus on UserInput
preventAutoFocus?: boolean;
// optional horizontal scroll acceleration value, 0-1
scrollAcceleration?: number;
// allow for a global validation method, asyncronous, so a value can be validated through a server, call success || error
flowStepCallback?: (dto: FlowDTO, success: () => void, error: () => void) => void;
// optional event dispatcher, has to be an instance of cf.EventDispatcher
eventDispatcher?: EventDispatcher;
// optional, set microphone nput, future, add other custom inputs, ex. VR
microphoneInput?:IUserInput;
// optional, hide ÜserInputField when radio, checkbox, select input is active
hideUserInputOnNoneTextInput?:boolean;
// optional, parameters for the User Interface of Conversational Form, set here to show thinking dots or not, set delay time in-between robot responses
userInterfaceOptions?:IUserInterfaceOptions;
// optional, Whenther to suppress console.log, default true
suppressLog?:boolean;
// Show progressbar
showProgressBar?:boolean;
// Prevent submit on Enter keypress: https://github.com/space10-community/conversational-form/issues/270
preventSubmitOnEnter?:boolean;
animationsEnabled?:boolean;
}
// CUI formless options
export interface ConversationalFormlessOptions{
options: any;
tags: any;
}
export class ConversationalForm{
public version: string = "1.0.2";
public static animationsEnabled: boolean = true;
public static illustrateAppFlow: boolean = true;
public static suppressLog: boolean = true;
public static showProgressBar: boolean = false;
public static preventSubmitOnEnter: boolean = false;
private cdnPath: string = "https://cdn.jsdelivr.net/gh/space10-community/conversational-form@{version}/dist/";
/**
* createId
* Id of the instance, to isolate events
*/
private _createId: string
public get createId(): string{
if(!this._createId){
this._createId = new Date().getTime().toString();
}
return this._createId;
}
// instance specific event target
private _eventTarget: EventDispatcher;
public get eventTarget(): EventDispatcher{
if(!this._eventTarget){
this._eventTarget = new EventDispatcher(this);
}
return this._eventTarget;
}
public dictionary: Dictionary;
public el: HTMLElement;
public chatList: ChatList;
public uiOptions: IUserInterfaceOptions;
public options:ConversationalFormOptions;
public preventSubmitOnEnter: boolean;
private context: HTMLElement;
private formEl: HTMLFormElement;
private submitCallback: (cf: ConversationalForm) => void | HTMLButtonElement;
private onUserAnswerClickedCallback: () => void;
private flowStepCallback: (dto: FlowDTO, success: () => void, error: () => void) => void;
private tags: Array;
private flowManager: FlowManager;
private isDevelopment: boolean = false;
private loadExternalStyleSheet: boolean = true;
private theme: String = 'light';
private preventAutoAppend: boolean = false;
private preventAutoStart: boolean = false;
private userInput: UserTextInput;
private microphoneInputObj: IUserInput;
constructor(options: ConversationalFormOptions){
window.ConversationalForm = this;
this.cdnPath = this.cdnPath.split("{version}").join(this.version);
if(typeof options.suppressLog === 'boolean')
ConversationalForm.suppressLog = options.suppressLog;
if(typeof options.showProgressBar === 'boolean')
ConversationalForm.showProgressBar = options.showProgressBar;
if(typeof options.preventSubmitOnEnter === 'boolean')
this.preventSubmitOnEnter = options.preventSubmitOnEnter;
if(!ConversationalForm.suppressLog) console.log('Conversational Form > version:', this.version);
if(!ConversationalForm.suppressLog) console.log('Conversational Form > options:', options);
window.ConversationalForm[this.createId] = this;
// possible to create your own event dispatcher, so you can tap into the events of the app
if(options.eventDispatcher)
this._eventTarget = options.eventDispatcher;
if(!this.eventTarget.cf)
this.eventTarget.cf = this;
// set a general step validation callback
if(options.flowStepCallback)
this.flowStepCallback = options.flowStepCallback;
this.isDevelopment = ConversationalForm.illustrateAppFlow = !!document.getElementById("conversational-form-development");
if(options.loadExternalStyleSheet == false){
this.loadExternalStyleSheet = false;
}
if(typeof options.theme === 'string')
this.theme = options.theme;
if(!isNaN(options.scrollAcceleration))
ScrollController.acceleration = options.scrollAcceleration;
this.preventAutoStart = options.preventAutoStart;
this.preventAutoAppend = options.preventAutoAppend;
if(!options.formEl)
throw new Error("Conversational Form error, the formEl needs to be defined.");
this.formEl = options.formEl;
this.formEl.setAttribute("cf-create-id", this.createId);
if(options.hideUserInputOnNoneTextInput === true){
UserInputElement.hideUserInputOnNoneTextInput = true;
}
this.submitCallback = options.submitCallback;
if(this.submitCallback && typeof this.submitCallback === "string"){
// Must be a string on window, rewritten to avoid unsafe eval() calls
const fn = (window as any)[this.submitCallback];
this.submitCallback = fn;
}
if(this.formEl.getAttribute("cf-no-animation") == "")
ConversationalForm.animationsEnabled = false;
if (
typeof options.animationsEnabled === 'boolean'
&& options.animationsEnabled === false
) {
ConversationalForm.animationsEnabled = false;
this.formEl.setAttribute("cf-no-animation", "");
}
if(options.preventAutoFocus || this.formEl.getAttribute("cf-prevent-autofocus") == "")
UserInputElement.preventAutoFocus = true;
this.dictionary = new Dictionary({
data: options.dictionaryData,
robotData: options.dictionaryRobot,
userImage: options.userImage,
robotImage: options.robotImage,
version: this.version
});
this.context = options.context ? options.context : document.body;
this.tags = options.tags;
if(options.microphoneInput){
// validate the user ..... TODO....
if(!options.microphoneInput.init || !options.microphoneInput.input){
console.warn("Conversational Form: microphoneInput is not correctly setup", options.microphoneInput);
options.microphoneInput = null;
}
}
this.microphoneInputObj = options.microphoneInput;
// set the ui options
this.uiOptions = Helpers.extendObject(UserInterfaceDefaultOptions, options.userInterfaceOptions || {});
// console.log('this.uiOptions:', this.uiOptions);
this.options = options;
this.init();
}
public init(): ConversationalForm{
switch(this.theme) {
case 'dark':
this.theme = 'conversational-form-dark.min.css';
if (!this.options.robotImage) this.updateDictionaryValue('robot-image', 'robot', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%233A3A3C'/%3E%3Crect x='66' y='66' width='68' height='68' fill='%23E5E6EA'/%3E%3C/svg%3E%0A");
if (!this.options.userImage) this.updateDictionaryValue('user-image', 'user', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23E5E6EA'/%3E%3Cpath d='M100 55L138.971 122.5H61.0289L100 55Z' fill='%233A3A3C'/%3E%3C/svg%3E%0A");
break;
case 'green':
this.theme = 'conversational-form-green.min.css';
if (!this.options.robotImage) this.updateDictionaryValue('robot-image', 'robot', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23EEEFF0'/%3E%3Crect x='66' y='66' width='68' height='68' fill='%2300BF75'/%3E%3C/svg%3E%0A");
if (!this.options.userImage) this.updateDictionaryValue('user-image', 'user', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%2300BF75'/%3E%3Cpath d='M100 55L138.971 122.5H61.0289L100 55Z' fill='%23EEEFF0'/%3E%3C/svg%3E%0A");
break;
case 'blue':
this.theme = 'conversational-form-irisblue.min.css';
if (!this.options.robotImage) this.updateDictionaryValue('robot-image', 'robot', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23E8E9EB'/%3E%3Crect x='66' y='66' width='68' height='68' fill='%2300C2DF'/%3E%3C/svg%3E%0A");
if (!this.options.userImage) this.updateDictionaryValue('user-image', 'user', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%2300C2DF'/%3E%3Cpath d='M100 55L138.971 122.5H61.0289L100 55Z' fill='%23E8E9EB'/%3E%3C/svg%3E%0A");
break;
case 'purple':
this.theme = 'conversational-form-purple.min.css';
if (!this.options.robotImage) this.updateDictionaryValue('robot-image', 'robot', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23EEEFF0'/%3E%3Crect x='66' y='66' width='68' height='68' fill='%235A1DE4'/%3E%3C/svg%3E%0A");
if (!this.options.userImage) this.updateDictionaryValue('user-image', 'user', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%235A1DE4'/%3E%3Cpath d='M100 55L138.971 122.5H61.0289L100 55Z' fill='%23EEEFF0'/%3E%3C/svg%3E%0A");
break;
case 'red':
this.theme = 'conversational-form-red.min.css';
if (!this.options.robotImage) this.updateDictionaryValue('robot-image', 'robot', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23E8E9EB'/%3E%3Crect x='66' y='66' width='68' height='68' fill='%23FF3233'/%3E%3C/svg%3E%0A");
if (!this.options.userImage) this.updateDictionaryValue('user-image', 'user', "data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='100' fill='%23FF3233'/%3E%3Cpath d='M100 55L138.971 122.5H61.0289L100 55Z' fill='%23E8E9EB'/%3E%3C/svg%3E%0A");
break;
default:
this.theme = 'conversational-form.min.css';
}
if (this.isDevelopment) {
// Set path for development
this.cdnPath = '../build/';
// strip .min from filename since we do not have minified css in build
this.theme = this.theme.replace('.min', '');
}
if(this.loadExternalStyleSheet){
// not in development/examples, so inject production css
const head: HTMLHeadElement = document.head || document.getElementsByTagName("head")[0];
const style: HTMLStyleElement = document.createElement("link");
const githubMasterUrl: string = this.cdnPath + this.theme;
style.type = "text/css";
style.media = "all";
style.setAttribute("rel", "stylesheet");
style.setAttribute("href", githubMasterUrl);
head.appendChild(style);
}
// set context position to relative, else we break out of the box
const position: string = window.getComputedStyle(this.context).getPropertyValue("position").toLowerCase();
if(["fixed", "absolute", "relative"].indexOf(position) == -1){
this.context.style.position = "relative";
}
// if tags are not defined then we will try and build some tags our selves..
if(!this.tags || this.tags.length == 0){
this.tags = [];
let fields: Array = [].slice.call(this.formEl.querySelectorAll("input, select, button, textarea, cf-robot-message"), 0);
for (var i = 0; i < fields.length; i++) {
const element = fields[i];
if(Tag.isTagValid(element)){
// ignore hidden tags
this.tags.push(Tag.createTag(element));
}
}
}else{
// tags are manually setup and passed as options.tags.
}
// remove invalid tags if they've sneaked in.. this could happen if tags are setup manually as we don't encurage to use static Tag.isTagValid
const indexesToRemove: Array = [];
for(var i = 0; i < this.tags.length; i++){
const element = this.tags[i];
if(!element || !Tag.isTagValid(element.domElement)){
indexesToRemove.push(element);
}
}
for (var i = 0; i < indexesToRemove.length; i++) {
var tag: ITag = indexesToRemove[i];
this.tags.splice(this.tags.indexOf(tag), 1);
}
if(!ConversationalForm.suppressLog && (!this.tags || this.tags.length == 0)) {
console.warn("Conversational Form: No tags found or registered.");
}
//let's start the conversation
this.tags = this.setupTagGroups(this.tags);
this.setupUI();
return this;
}
/**
* @name updateDictionaryValue
* set a dictionary value at "runtime"
* id: string, id of the value to update
* type: string, "human" || "robot"
* value: string, value to be inserted
*/
public updateDictionaryValue(id:string, type: string, value: string){
Dictionary.set(id, type, value);
// if(["robot-image", "user-image"].indexOf(id) != -1){
// this.chatList.updateThumbnail(id == "robot-image", value);
// }
}
public getFormData(serialized: boolean = false): FormData | any{
if(serialized){
const serialized: any = {}
for(var i = 0; i < this.tags.length; i++){
const element = this.tags[i];
if(element.value)
serialized[element.name || "tag-" + i.toString()] = element.value
}
return serialized
}else{
var formData: FormData = new FormData(this.formEl);
return formData;
}
}
public addRobotChatResponse(response: string){
this.chatList.createResponse(true, null, response);
}
public addUserChatResponse(response: string){
// add a "fake" user response..
this.chatList.createResponse(false, null, response);
}
public stop(optionalStoppingMessage: string = ""){
this.flowManager.stop();
if(optionalStoppingMessage != "")
this.chatList.createResponse(true, null, optionalStoppingMessage);
this.userInput.onFlowStopped();
}
public start(){
this.userInput.disabled = false;
if(!ConversationalForm.suppressLog) console.log('option, disabled 3', );
this.userInput.visible = true;
this.flowManager.start();
}
public getTag(nameOrIndex: string | number): ITag{
if(typeof nameOrIndex == "number"){
return this.tags[nameOrIndex];
}else{
// TODO: fix so you can get a tag by its name attribute
return null;
}
}
private setupTagGroups(tags: Array) : Array{
// make groups, from input tag[type=radio | type=checkbox]
// groups are used to bind logic like radio-button or checkbox dependencies
var groups: any = [];
for(var i = 0; i < tags.length; i++){
const tag: ITag = tags[i];
if(tag.type == "radio" || tag.type == "checkbox"){
if(!groups[tag.name])
groups[tag.name] = [];
groups[tag.name].push(tag);
}
}
if(Object.keys(groups).length > 0){
for (let group in groups){
if(groups[group].length > 0){
// always build groupd when radio or checkbox
// find the fieldset, if any..
let isFieldsetValidForCF = (tag: HTMLElement) : boolean => {return tag && tag.tagName.toLowerCase() !== "fieldset" && !tag.hasAttribute("cf-questions")};
let fieldset: HTMLElement = groups[group][0].domElement.parentNode;
if(fieldset && fieldset.tagName.toLowerCase() !== "fieldset"){
fieldset = fieldset.parentNode;
if(isFieldsetValidForCF(fieldset)){
// not a valid fieldset, we only accept fieldsets that contain cf attr
fieldset = null;
}
}
const tagGroup: TagGroup = new TagGroup({
fieldset: fieldset, // <-- can be null
elements: groups[group]
});
// remove the tags as they are now apart of a group
for(var i = 0; i < groups[group].length; i++){
let tagToBeRemoved: InputTag = groups[group][i];
if(i == 0)// add the group at same index as the the first tag to be removed
tags.splice(tags.indexOf(tagToBeRemoved), 1, tagGroup);
else
tags.splice(tags.indexOf(tagToBeRemoved), 1);
}
}
}
}
return tags;
}
private setupUI(){
// start the flow
this.flowManager = new FlowManager({
cfReference: this,
flowStepCallback: this.flowStepCallback,
eventTarget: this.eventTarget,
tags: this.tags
});
this.el = document.createElement("div");
this.el.id = "conversational-form";
this.el.className = "conversational-form";
this.addBrowserTypes(this.el);
if(ConversationalForm.animationsEnabled)
this.el.classList.add("conversational-form--enable-animation");
// add conversational form to context
if(!this.preventAutoAppend)
this.context.appendChild(this.el);
//hide until stylesheet is rendered
this.el.style.visibility = "hidden";
var innerWrap = document.createElement("div");
innerWrap.className = "conversational-form-inner";
this.el.appendChild(innerWrap);
// Conversational Form UI
this.chatList = new ChatList({
eventTarget: this.eventTarget,
cfReference: this
});
innerWrap.appendChild(this.chatList.el);
this.userInput = new UserTextInput({
microphoneInputObj: this.microphoneInputObj,
eventTarget: this.eventTarget,
cfReference: this
});
if (ConversationalForm.showProgressBar) {
const progressBar = new ProgressBar(this);
innerWrap.appendChild(progressBar.el);
}
this.chatList.addInput(this.userInput);
innerWrap.appendChild(this.userInput.el);
this.onUserAnswerClickedCallback = this.onUserAnswerClicked.bind(this);
this.eventTarget.addEventListener(ChatResponseEvents.USER_ANSWER_CLICKED, this.onUserAnswerClickedCallback, false);
this.el.classList.add("conversational-form--show")
if(!this.preventAutoStart)
this.flowManager.start();
if(!this.tags || this.tags.length == 0){
// no tags, so just show the input
this.userInput.visible = true;
}
}
/**
* @name onUserAnswerClicked
* on user ChatReponse clicked
*/
private onUserAnswerClicked(event: CustomEvent): void {
const tag: ITag | ITagGroup = event.detail;
this.flowManager.editTag(tag);
}
private addBrowserTypes(el:Element):void {
if (navigator.userAgent.indexOf('Firefox') > -1) el.classList.add('browser-firefox');
if (/Edge/.test(navigator.userAgent)) el.classList.add('browser-edge');
}
/**
* @name addTag
* Add a tag to the conversation. This can be used to add tags at runtime
* see examples/formless.html
*/
public addTags(tagsData: Array, addAfterCurrentStep: boolean = true, atIndex: number = -1): void {
let tags: Array = [];
for (let i = 0; i < tagsData.length; i++) {
let tagData: DataTag = tagsData[i];
if(tagData.tag === "fieldset"){
// group ..
// const fieldSetChildren: Array = tagData.children;
// parse group tag
const groupTag: HTMLElement = TagsParser.parseGroupTag(tagData);
for (let j = 0; j < groupTag.children.length; j++) {
let tag: HTMLElement = groupTag.children[j];
if(Tag.isTagValid(tag)){
let tagElement : ITag = Tag.createTag( tag);
// add ref for group creation
if(!tagElement.name){
tagElement.name = "tag-ref-"+j.toString();
}
tags.push(tagElement);
}
}
}else{
let tag: HTMLElement | HTMLInputElement | HTMLSelectElement | HTMLButtonElement = tagData.tag === "select" ? TagsParser.parseGroupTag(tagData) : TagsParser.parseTag(tagData);
if(Tag.isTagValid(tag)){
let tagElement : ITag = Tag.createTag( tag);
tags.push(tagElement);
}
}
}
// map free roaming checkbox and radio tags into groups
tags = this.setupTagGroups(tags);
// add new tags to the flow
this.tags = this.flowManager.addTags(tags, addAfterCurrentStep ? this.flowManager.getStep() + 1 : atIndex);
//this.flowManager.startFrom ?
}
/**
* @name remapTagsAndStartFrom
* index: number, what index to start from
* setCurrentTagValue: boolean, usually this method is called when wanting to loop or skip over questions, therefore it might be usefull to set the value of the current tag before changing index.
* ignoreExistingTags: boolean, possible to ignore existing tags, to allow for the flow to just "happen"
*/
public remapTagsAndStartFrom(index: number = 0, setCurrentTagValue: boolean = false, ignoreExistingTags: boolean = false){
if(setCurrentTagValue){
this.chatList.setCurrentUserResponse(this.userInput.getFlowDTO());
}
// possibility to start the form flow over from {index}
for(var i = 0; i < this.tags.length; i++){
const tag: ITag | ITagGroup = this.tags[i];
tag.refresh();
}
this.flowManager.startFrom(index, ignoreExistingTags);
}
/**
* @name focus
* Sets focus on Conversational Form
*/
public focus(){
if(this.userInput)
this.userInput.setFocusOnInput();
}
public doSubmitForm(){
this.el.classList.add("done");
this.userInput.reset();
if(this.submitCallback){
// remove should be called in the submitCallback
this.submitCallback(this);
}else{
// this.formEl.submit();
// doing classic .submit wont trigger onsubmit if that is present on form element
// as described here: http://wayback.archive.org/web/20090323062817/http://blogs.vertigosoftware.com/snyholm/archive/2006/09/27/3788.aspx
// so we mimic a click.
var button: HTMLButtonElement = this.formEl.ownerDocument.createElement('button');
button.style.display = 'none';
button.type = 'submit';
this.formEl.appendChild(button);
button.click();
this.formEl.removeChild(button);
// remove conversational
this.remove();
}
}
public remove(){
if(this.microphoneInputObj){
this.microphoneInputObj = null;
}
if(this.onUserAnswerClickedCallback){
this.eventTarget.removeEventListener(ChatResponseEvents.USER_ANSWER_CLICKED, this.onUserAnswerClickedCallback, false);
this.onUserAnswerClickedCallback = null;
}
if(this.flowManager)
this.flowManager.dealloc();
if(this.userInput)
this.userInput.dealloc();
if(this.chatList)
this.chatList.dealloc();
this.dictionary = null;
this.flowManager = null;
this.userInput = null;
this.chatList = null;
this.context = null;
this.formEl = null;
this.tags = null;
this.submitCallback = null;
this.el.parentNode.removeChild(this.el);
this.el = null;
window.ConversationalForm[this.createId] = null;
}
// to illustrate the event flow of the app
public static illustrateFlow(classRef: any, type: string, eventType: string, detail: any = null){
// ConversationalForm.illustrateFlow(this, "dispatch", FlowEvents.USER_INPUT_INVALID, event.detail);
// ConversationalForm.illustrateFlow(this, "receive", event.type, event.detail);
if(ConversationalForm.illustrateAppFlow){
const highlight: string = "font-weight: 900; background: "+(type == "receive" ? "#e6f3fe" : "pink")+"; color: black; padding: 0px 5px;";
if(!ConversationalForm.suppressLog) console.log("%c** event flow: %c" + eventType + "%c flow type: %c" + type + "%c from: %c"+( classRef.constructor).name, "font-weight: 900;",highlight, "font-weight: 400;", highlight, "font-weight: 400;", highlight);
if(detail)
if(!ConversationalForm.suppressLog) console.log("** event flow detail:", detail);
}
}
private static hasAutoInstantiated: boolean = false;
public static startTheConversation(data: ConversationalFormOptions | ConversationalFormlessOptions) {
let isFormless: boolean = !!( data).formEl === false;
let formlessTags: any;
let constructorOptions: ConversationalFormOptions;
if(isFormless){
if(typeof data === "string"){
// Formless init w. string
isFormless = true;
const json: any = JSON.parse(data)
constructorOptions = ( json).options;
formlessTags = ( json).tags;
}else{
// Formless init w. JSON object
constructorOptions = ( data).options;
formlessTags = ( data).tags;
}
// formless, so generate the pseudo tags
const formEl: HTMLFormElement = cf.TagsParser.parseJSONIntoElements(formlessTags)
constructorOptions.formEl = formEl;
}else{
// keep it standard
constructorOptions = data;
}
return new cf.ConversationalForm(constructorOptions);
}
public static autoStartTheConversation() {
if(cf.ConversationalForm.hasAutoInstantiated)
return;
// auto start the conversation
let formElements: NodeListOf = document.querySelectorAll("form[cf-form]");
// no form elements found, look for the old init attribute
if(formElements.length === 0){
formElements = document.querySelectorAll("form[cf-form-element]");
}
const formContexts: NodeListOf = document.querySelectorAll("*[cf-context]");
if(formElements && formElements.length > 0){
for (let i = 0; i < formElements.length; i++) {
let form: HTMLFormElement = formElements[i];
let context: HTMLFormElement = formContexts[i];
cf.ConversationalForm.startTheConversation({
formEl: form,
context: context
});
}
cf.ConversationalForm.hasAutoInstantiated = true;
}
}
}
}
if(document.readyState == "complete"){
// if document alread instantiated, usually this happens if Conversational Form is injected through JS
setTimeout(() => cf.ConversationalForm.autoStartTheConversation(), 0);
}else{
// await for when document is ready
window.addEventListener("load", () =>{
cf.ConversationalForm.autoStartTheConversation();
}, false);
}