export class ExpressionInput { private static EXPRESSION_INPUT_CLASS: string = "expression-input"; private static OVERLAY_CLASS: string = "overlay"; private static PACKING_CLASS: string = "packing"; private static HINTS_CLASS: string = "hints"; private static HINT_CLASS: string = "hint"; private static HINT_SELECTED_CLASS: string = "selected-hint"; private static TYPED_CLASS: string = "typed"; private static SUGGESTION_CLASS: string = "suggestion"; private initialized: boolean = false; private input: HTMLInputElement; private overlay: HTMLDivElement; private hintsContainer: HTMLDivElement; private packing: HTMLSpanElement; private hints: HTMLSpanElement; private caretPosition: number = 0; private selectedHint: number = 0; private hintValues: Hint[] = []; private columnNames = ["productId", "notional", "strike", "spot", "pv"]; private operators = ["and","or","(",")"]; private compareOperators = [">","<",">=","<=","=","!="]; private dictionary = this.columnNames.concat(this.operators).concat(this.compareOperators); public constructor(private container: (HTMLElement|any)) { if (!this.initialized) { /* Add HTML elements */ this.input = document.createElement("input"); this.input.type = "text"; this.input.className = ExpressionInput.EXPRESSION_INPUT_CLASS; this.overlay = document.createElement("div"); this.overlay.className = ExpressionInput.OVERLAY_CLASS; this.hintsContainer = document.createElement("div"); this.packing = document.createElement("span"); this.packing.className = ExpressionInput.PACKING_CLASS; this.hints = document.createElement("span"); this.hints.className = ExpressionInput.HINTS_CLASS; this.hints.style.display = "none"; this.hints.style.width = "100px"; this.hintsContainer.appendChild(this.packing); this.hintsContainer.appendChild(this.hints); this.container.appendChild(this.input); this.container.appendChild(this.overlay); this.container.appendChild(this.hintsContainer); /* Initialize listeners */ this.input.addEventListener("click", () => this.checkCaretPosition()); this.input.addEventListener("blur", () => this.onBlur()); this.input.addEventListener("focus", () => this.onFocus()); this.input.addEventListener("keyup", () => this.checkCaretPosition()); this.input.addEventListener("keydown", (event) => { switch (event.keyCode) { case 9: { this.onTabPress(); event.preventDefault(); break; } case 13: { this.onEnterPress(); event.preventDefault(); break; } case 38: { this.onUpPress(); event.preventDefault(); break; } case 40: { this.onDownPress(); event.preventDefault(); break; } } }); this.input.addEventListener("input", () => { this.onInput(); this.checkCaretPosition(); }); this.initialized = true; } else { throw "This Expression Input is already initialized. You can create other instance and initialize it " + "separately for other expression input container."; } } private onCaretPositionChange(): void { if (this.input.value.length == this.caretPosition || this.input.value.charAt(this.caretPosition) == " ") { let startIndex: number = this.caretPosition - 1; while (startIndex >= 0) { let character = this.input.value.charAt(startIndex); if (character == " ") break; startIndex--; } this.packing.innerHTML = this.input.value.substring(0, startIndex + 1); const typed: string = this.input.value.substring(startIndex + 1, this.input.value.length); this.hintValues = this.dictionary .filter(word => word.startsWith(typed) && word.length != typed.length) .map(suggestion => new Hint(typed, suggestion.substring(typed.length, suggestion.length), null)); if (this.hintValues.length > 0) { this.renderHints(); this.showHints(); } else { this.hideHints(); } } else { this.hideHints(); } }; private updateOverlay(): void { while (this.overlay.hasChildNodes()) { this.overlay.removeChild(this.overlay.lastChild); } const tokens: string[] = this.input.value.split(" "); for (let i: number = 0; i < tokens.length; i++) { const tokenElement: HTMLSpanElement = document.createElement("span"); tokenElement.appendChild(document.createTextNode(tokens[i])); if (this.columnNames.indexOf(tokens[i]) >= 0) { tokenElement.className = "column-name"; } else if (this.operators.indexOf(tokens[i]) >= 0) { tokenElement.className = "operator"; } else if (this.compareOperators.indexOf(tokens[i]) >= 0) { tokenElement.className = "compare-operator"; } else { tokenElement.className = "exception"; } this.overlay.appendChild(tokenElement); this.overlay.appendChild(document.createTextNode(" ")); } }; private onTabPress(): void { if (this.selectedHint >= 0) { this.input.value += this.hintValues[this.selectedHint].suggestion; this.updateOverlay(); } }; private onEnterPress(): void { this.onTabPress(); }; private onInput(): void { this.updateOverlay(); }; private onUpPress(): void { if (this.selectedHint >= 0) { this.hintValues[this.selectedHint].htmlElement.className = ExpressionInput.HINT_CLASS; this.selectedHint = (this.selectedHint == 0 ? this.hintValues.length - 1 : this.selectedHint - 1); this.hintValues[this.selectedHint].htmlElement.className = ExpressionInput.HINT_CLASS + " " + ExpressionInput.HINT_SELECTED_CLASS; } }; private onDownPress(): void { if (this.selectedHint >= 0) { this.hintValues[this.selectedHint].htmlElement.className = ExpressionInput.HINT_CLASS; this.selectedHint = (this.selectedHint == this.hintValues.length - 1 ? 0 : this.selectedHint + 1); this.hintValues[this.selectedHint].htmlElement.className = ExpressionInput.HINT_CLASS + " " + ExpressionInput.HINT_SELECTED_CLASS; } }; private onBlur(): void { this.hideHints(); }; private onFocus(): void { this.onCaretPositionChange(); }; private getCaretPosition(): number { let pos: number = 0; if (document["selection"]) { this.input.focus(); const sel: any = document["selection"].createRange(); sel.moveStart('character', -this.input.value.length); pos = sel.text.length; } else if (this.input.selectionStart || this.input.selectionStart == 0) { pos = this.input.selectionStart; } return pos; } private checkCaretPosition(): void { const newCaretPosition = this.getCaretPosition(); if (newCaretPosition != this.caretPosition) { this.caretPosition = newCaretPosition; this.onCaretPositionChange(); } }; private renderHints(): void { while (this.hints.hasChildNodes()) { this.hints.removeChild(this.hints.lastChild); } for (let i = 0; i < this.hintValues.length; i++) { const hint: Hint = this.hintValues[i]; const hintElement: HTMLDivElement = document.createElement("div"); hintElement.className = i == 0 ? "hint selected" : "hint"; const typed: HTMLSpanElement = document.createElement("span"); typed.className = ExpressionInput.TYPED_CLASS; const suggestion: HTMLSpanElement = document.createElement("suggestion"); suggestion.className = ExpressionInput.SUGGESTION_CLASS; typed.appendChild(document.createTextNode(hint.typed)); suggestion.appendChild(document.createTextNode(hint.suggestion)); hintElement.appendChild(typed); hintElement.appendChild(suggestion); this.hints.appendChild(hintElement); hint.htmlElement = hintElement; } this.showHints(); }; private showHints(): void { this.hints.style.display = "inline-block"; this.selectedHint = 0; } private hideHints(): void { this.hints.style.display = "none"; this.selectedHint = -1; } } class Hint { public constructor(public typed: string, public suggestion: string, public htmlElement: HTMLDivElement) {} }