///
///
///
// namespace
namespace cf {
// interface
export const ChatListEvents = {
CHATLIST_UPDATED: "cf-chatlist-updated"
}
// class
export class ChatList extends BasicElement {
private flowUpdateCallback: () => void;
private userInputUpdateCallback: () => void;
private onInputKeyChangeCallback: () => void;
private onInputHeightChangeCallback: () => void;
private onControlElementsResizedCallback: () => void;
private onControlElementsChangedCallback: () => void;
private currentResponse: ChatResponse;
private currentUserResponse: ChatResponse;
private flowDTOFromUserInputUpdate: FlowDTO;
private responses: Array;
private input: UserInputElement;
constructor(options: IBasicElementOptions){
super(options);
ChatResponse.list = this;
this.responses = [];
// flow update
this.flowUpdateCallback = this.onFlowUpdate.bind(this);
this.eventTarget.addEventListener(FlowEvents.FLOW_UPDATE, this.flowUpdateCallback, false);
// user input update
this.userInputUpdateCallback = this.onUserInputUpdate.bind(this);
this.eventTarget.addEventListener(FlowEvents.USER_INPUT_UPDATE, this.userInputUpdateCallback, false);
// user input key change
this.onInputKeyChangeCallback = this.onInputKeyChange.bind(this);
this.eventTarget.addEventListener(UserInputEvents.KEY_CHANGE, this.onInputKeyChangeCallback, false);
// user input height change
this.onInputHeightChangeCallback = this.onInputHeightChange.bind(this);
this.eventTarget.addEventListener(UserInputEvents.HEIGHT_CHANGE, this.onInputHeightChangeCallback, false);
// on control elements changed
this.onControlElementsResizedCallback = this.onControlElementsResized.bind(this);
this.eventTarget.addEventListener(ControlElementsEvents.ON_RESIZE, this.onControlElementsResizedCallback, false);
this.onControlElementsChangedCallback = this.onControlElementsChanged.bind(this);
this.eventTarget.addEventListener(ControlElementsEvents.CHANGED, this.onControlElementsChangedCallback, false);
}
private onInputHeightChange(event: CustomEvent){
const dto: FlowDTO = ( event.detail).dto;
ConversationalForm.illustrateFlow(this, "receive", event.type, dto);
// this.input.controlElements.el.style.transition = "height 2s ease-out";
// this.input.controlElements.el.style.height = this.input.controlElements.el.scrollHeight + 'px';
this.onInputElementChanged();
}
private onInputKeyChange(event: CustomEvent){
const dto: FlowDTO = ( event.detail).dto;
ConversationalForm.illustrateFlow(this, "receive", event.type, dto);
}
private onUserInputUpdate(event: CustomEvent){
ConversationalForm.illustrateFlow(this, "receive", event.type, event.detail);
if(this.currentUserResponse){
const response: FlowDTO = event.detail;
this.setCurrentUserResponse(response);
}
}
public addInput(input: UserInputElement){
this.input = input;
}
/**
* @name onControlElementsChanged
* on control elements change
*/
private onControlElementsChanged(event: Event): void {
this.onInputElementChanged();
}
/**
* @name onControlElementsResized
* on control elements resize
*/
private onControlElementsResized(event: Event): void {
ConversationalForm.illustrateFlow(this, "receive", ControlElementsEvents.ON_RESIZE);
let responseToScrollTo: ChatResponse = this.currentResponse;
if(responseToScrollTo){
if(!responseToScrollTo.added){
// element not added yet, so find closest
for (let i = this.responses.indexOf(responseToScrollTo); i >= 0; i--) {
let element: ChatResponse = this.responses[i];
if(element.added){
responseToScrollTo = element;
break;
}
}
}
responseToScrollTo.scrollTo();
}
this.onInputElementChanged();
}
private onInputElementChanged(){
if (!this.cfReference || !this.cfReference.el) return;
const cfHeight: number = this.cfReference.el.offsetHeight;
const inputHeight: number = this.input.height;
const listHeight: number = cfHeight - inputHeight;
//this.el.style.height = listHeight + "px";
}
private onFlowUpdate(event: CustomEvent){
ConversationalForm.illustrateFlow(this, "receive", event.type, event.detail);
const currentTag: ITag | ITagGroup = event.detail.tag;
if(this.currentResponse)
this.currentResponse.disabled = false;
if(this.containsTagResponse(currentTag) && !event.detail.ignoreExistingTag){
// because user maybe have scrolled up and wants to edit
// tag is already in list, so re-activate it
this.onUserWantsToEditTag(currentTag);
}else{
// robot response
setTimeout(() => {
const robot: ChatResponse = this.createResponse(true, currentTag, currentTag.question);
robot.whenReady(() =>{
// create user response
this.currentUserResponse = this.createResponse(false, currentTag);
robot.scrollTo();
});
if(this.currentUserResponse){
// linked, but only if we should not ignore existing tag
this.currentUserResponse.setLinkToOtherReponse(robot);
robot.setLinkToOtherReponse(this.currentUserResponse);
}
}, this.responses.length === 0 ? 500 : 0);
}
}
/**
* @name containsTagResponse
* @return boolean
* check if tag has already been responded to
*/
private containsTagResponse(tagToChange: ITag): boolean {
for (let i = 0; i < this.responses.length; i++) {
let element: ChatResponse = this.responses[i];
if(!element.isRobotResponse && element.tag == tagToChange && !tagToChange.hasConditions()){
return true;
}
}
return false;
}
/**
* @name onUserAnswerClicked
* on user ChatReponse clicked
*/
private onUserWantsToEditTag(tagToChange: ITag): void {
let responseUserWantsToEdit: ChatResponse;
for (let i = 0; i < this.responses.length; i++) {
let element: ChatResponse = this.responses[i];
if(!element.isRobotResponse && element.tag == tagToChange){
// update element thhat user wants to edit
responseUserWantsToEdit = element;
break;
}
}
// reset the current user response
this.currentUserResponse.processResponseAndSetText();
if(responseUserWantsToEdit){
// remove latest user response, if it is there any, also make sure we don't remove the first one
if(this.responses.length > 2){
if(!this.responses[this.responses.length - 1].isRobotResponse){
this.responses.pop().dealloc();
}
// remove latest robot response, it should always be a robot response
this.responses.pop().dealloc();
}
this.currentUserResponse = responseUserWantsToEdit;
// TODO: Set user field to thinking?
// this.currentUserResponse.setToThinking??
this.currentResponse = this.responses[this.responses.length - 1];
this.onListUpdate(this.currentUserResponse);
}
}
private updateTimer: number = 0;
private onListUpdate(chatResponse: ChatResponse){
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(() => {
this.eventTarget.dispatchEvent(new CustomEvent(ChatListEvents.CHATLIST_UPDATED, {
detail: this
}));
chatResponse.show();
}, 0);
}
/**
* @name clearFrom
* remove responses, this usually happens if a user jumps back to a conditional element
*/
public clearFrom(index: number): void {
index = index * 2; // double up because of robot responses
index += index % 2; // round up so we dont remove the user response element
while(this.responses.length > index){
this.responses.pop().dealloc();
}
}
/**
* @name setCurrentUserResponse
* Update current reponse, is being called automatically from onFlowUpdate, but can also, in rare cases, be called when flow is controlled manually.
* reponse: FlowDTO
*/
public setCurrentUserResponse(dto: FlowDTO){
this.flowDTOFromUserInputUpdate = dto;
if(!this.flowDTOFromUserInputUpdate.text && dto.tag){
if(dto.tag.type == "group"){
this.flowDTOFromUserInputUpdate.text = Dictionary.get("user-reponse-missing-group");
}else if(dto.tag.type != "password"){
this.flowDTOFromUserInputUpdate.text = Dictionary.get("user-reponse-missing");
}
}
this.currentUserResponse.setValue(this.flowDTOFromUserInputUpdate);
}
/**
* @name getResponses
* returns the submitted responses.
*/
public getResponses(): Array {
return this.responses;
}
public updateThumbnail(robot: boolean, img: string){
Dictionary.set(robot ? "robot-image" : "user-image", robot ? "robot" : "human", img);
const newImage: string = robot ? Dictionary.getRobotResponse("robot-image") : Dictionary.get("user-image");
for (let i = 0; i < this.responses.length; i++) {
let element: ChatResponse = this.responses[i];
if(robot && element.isRobotResponse){
element.updateThumbnail(newImage);
}else if(!robot && !element.isRobotResponse){
element.updateThumbnail(newImage);
}
}
}
public createResponse(isRobotResponse: boolean, currentTag: ITag, value: string = null) : ChatResponse{
const scrollable: HTMLElement = this.el.querySelector(".scrollableInner");
const response: ChatResponse = new ChatResponse({
// image: null,
cfReference: this.cfReference,
list: this,
tag: currentTag,
eventTarget: this.eventTarget,
isRobotResponse: isRobotResponse,
response: value,
image: isRobotResponse ? Dictionary.getRobotResponse("robot-image") : Dictionary.get("user-image"),
container: scrollable
});
this.responses.push(response);
this.currentResponse = response;
this.onListUpdate(response);
return response;
}
public getTemplate () : string {
return `
`;
}
public dealloc(){
this.eventTarget.removeEventListener(FlowEvents.FLOW_UPDATE, this.flowUpdateCallback, false);
this.flowUpdateCallback = null;
this.eventTarget.removeEventListener(FlowEvents.USER_INPUT_UPDATE, this.userInputUpdateCallback, false);
this.userInputUpdateCallback = null;
this.eventTarget.removeEventListener(UserInputEvents.KEY_CHANGE, this.onInputKeyChangeCallback, false);
this.onInputKeyChangeCallback = null
super.dealloc();
}
}
}