class HTML { static escape(text) { let div = document.createElement('DIV'); div.innerText = text; return div.innerHTML;} static parseElement(html) { let div = document.createElement('DIV'); div.innerHTML = html; return div.firstElementChild;} static parseList(html) { let div = document.createElement('DIV'); div.innerHTML = html; return [...div.children];} static parseRows(html) { let table = document.createElement('TABLE'); table.innerHTML = html; return [...table.rows];} static parseRow(html) { let table = document.createElement('TABLE'); table.innerHTML = html; return table.rows[0];} static highlightSearchString(element, searchString, highlightClass = 'highlight') { if (Utils.isNullOrEmpty(searchString)) return; const searchRegex = new RegExp(Utils.escapeRegex(searchString), 'gi'); element.normalize(); let textNodes = HTML.getTextNodes(element); let textValues = [...textNodes.map(n => n.textContent)]; let searchText = textValues.join(''); while (true) { const match = searchRegex.exec(searchText); if (match == null) break; let startInfo = this.findNodeAtIndex(textNodes, match.index); let endInfo = this.findNodeAtIndex(textNodes, match.index + match[0].length - 1); let hilighting = false; for (const node of textNodes) { let isStart = node == startInfo.node; let isEnd = node == endInfo.node; if (isStart && isEnd) { HTML.highlightRange(node, startInfo.offset, endInfo.offset, highlightClass);} else if (isStart) { hilighting = true; HTML.highlightRange(node, startInfo.offset, node.textContent.length - 1, highlightClass);} else if (isEnd) { hilighting = false; if (!Utils.isNullOrEmpty(node.textContent)) HTML.highlightRange(node, 0, endInfo.offset, highlightClass)} else if (hilighting) { HTML.highlightRange(node, 0, node.textContent.length, highlightClass);}}}} static highlightRange(node, startOffset, endOffset, highlightClass) { let range = document.createRange(); range.setStart(node, startOffset); range.setEnd(node, endOffset); const span = document.createElement('span'); span.className = highlightClass; range.surroundContents(span);} static getTextNodes(element) { let ret = []; const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); let node = null; while ((node = walker.nextNode())) ret.push(node); return ret;} static findNodeAtIndex(nodes, index) { let currentIndex = 0; for (const node of nodes) { const nextIndex = currentIndex + node.textContent.length; if (index < nextIndex) { return { node, offset: index - currentIndex };} currentIndex = nextIndex;} return null;}} ; class HTTP { static callsInProgress = 0; static postJSON(URL, postData = null, options = null) { let xhr = HTTP.createRequest("POST", URL, options); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify(postData));} static postFiles(URL, postData, options = null) { let xhr = HTTP.createRequest("POST", URL, options); if (options.progress) xhr.upload.onprogress = options.progress; xhr.send(postData);} static getJSON(URL, options = null) { let delim = URL.includes('?') ? '&' : '?'; let fullURL = URL + delim + `z=${HTTP.randomNumber}`; let xhr = HTTP.createRequest("GET", fullURL, options); xhr.send();} static request(URL, postData = null, oncomplete = null) { if (postData) HTTP.post(URL, postData, oncomplete); else HTTP.get(URL, oncomplete);} static post(URL, postData, oncomplete = null) { HTTP.postJSON(URL, postData, { success: oncomplete });} static get(URL, oncomplete = null) { HTTP.getJSON(URL, { success: oncomplete });} static onLoad(xhr, options) { if (options != null && !options.isBackground) HTTP.callsInProgress--; switch (xhr.status) { case 200: return HTTP.onHttpSuccess(xhr, options); case 440: return HTTP.onHttpSessionExpired(xhr); default: return HTTP.onHttpError(xhr, options);}} static onHttpSuccess(xhr, options) { if (HTTP.getMimeType(xhr) == 'application/json') return HTTP.onJsonSuccess(xhr, options); if (options?.success) options.success(xhr.responseText, xhr);} static sessionExpiredShown; static onHttpSessionExpired(xhr) { if (HTTP.sessionExpiredShown) return; else HTTP.sessionExpiredShown = true; let errorBoxOptions = { endpoint: xhr.requestURL, onOk: () => window.location.reload() }; Utils.layout.errorBox('Your server session has expired - page will be reloaded.', errorBoxOptions);} static onXhrError(xhr, options) { if (!options.isBackground) HTTP.callsInProgress--; return HTTP.onHttpError(xhr, options);} static onXhrTimeout(xhr, options) { if (!options.isBackground) HTTP.callsInProgress--; return HTTP.onHttpError(xhr, options);} static onHttpError(xhr, options) { let message = xhr.status == 0 ? 'Cannot connect to server.' : xhr.statusText; let errorInfo = { error: `[${xhr.status}] ${message}` }; if (options?.error) options.error(xhr, errorInfo); else HTTP.handleError(xhr, errorInfo);} static onJsonSuccess(xhr, options) { let data = JSON.parse(xhr.responseText); if (data.error != undefined && data.stack != undefined) return HTTP.onJsonError(xhr, options, data); if (options?.success) options.success(data, xhr);} static onJsonError(xhr, options, error) { if (options?.error) options.error(xhr, error); else HTTP.handleError(xhr, error);} static handleError(xhr, errorInfo) { Utils.layout.errorBox(errorInfo.error, { endpoint: xhr.requestURL, stack: errorInfo.stack });} static get randomNumber() { return Math.floor(Math.random() * 10000); } static getMimeType(xhr) { let contentTypeHeader = xhr.getResponseHeader("Content-Type"); return contentTypeHeader?.split(';')[0].toLowerCase() ?? '';} static createRequest(verb, URL, options) { if (options != null && !options.isBackground) HTTP.callsInProgress++; let xhr = new XMLHttpRequest(); xhr.open(verb, Utils.mapPath(URL), true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.requestURL = Utils.mapPath(URL); xhr.onload = e => HTTP.onLoad(e.target, options); xhr.onerror = e => HTTP.onXhrError(e.target, options); xhr.ontimeout = e => HTTP.onXhrTimeout(e.target, options); if (options?.timeout) xhr.timeout = options.timeout; return xhr;}}; class Snippets { static loaderHTML = '
LOADING
'; static createButton(id, iconName, title, enabled) { let ret = document.createElement("BUTTON"); ret.id = id; ret.title = title; ret.className = 'material-icons'; ret.disabled = !enabled; ret.setAttribute('type', 'button'); ret.setAttribute('aria-label', id); Utils.setButtonState(ret, enabled); ret.innerText = iconName; return ret;} static createDivider() { let ret = document.createElement('SPAN'); ret.className = 'divider'; ret.innerText = "│" return ret;}}; class Span { index; length; constructor(index, length) { this.index = index; this.length = length;} get endIndex() { return this.index + this.length - 1; } toString() { if (this.length == 1) { return `Index: ${this.index}, Length: ${this.length}`;} else { return `Range: ${this.index}-${this.endIndex}, Length: ${this.length}`;}} intersects(span) { return this.index <= span.endIndex && this.endIndex >= span.index;} contains(span) { return span.index >= this.index && span.endIndex <= this.endIndex;} includes(value) { return (value >= this.index) && (value <= this.endIndex);} getIntersection(span) { if (!this.intersects(span)) return null; return Span.fromIndices(Math.max(span.index, this.index), Math.min(span.endIndex, this.endIndex));} isEqual(span) { return (span.index == this.index) && (span.length == this.length);} static fromIndices(startIndex, endIndex) { return new Span(startIndex, endIndex - startIndex + 1);}} ; class Speech { static getTranscript(e) { var transcript = ''; for (var i = e.resultIndex; i < e.results.length; ++i) { if (e.results[i].isFinal) { transcript += e.results[i][0].transcript;}} return Speech.replaceWords(transcript);} static replaceWords(transcript) { let ret = transcript; ret = this.replaceWord(ret, 'comma', ', '); ret = this.replaceWord(ret, 'colon', ': '); ret = this.replaceWord(ret, 'semicolon', '; '); ret = this.replaceWord(ret, 'hyphen', '-'); return ret;} static replaceWord(text, word, replacement) { var regex = new RegExp(' ' + word + ' ', 'ig'); return text.replaceAll(regex, replacement);} static createRecognition(continuous) { let ret = new webkitSpeechRecognition() ?? new speechRecognition(); ret.continuous = continuous; ret.interimResults = false; ret.maxAlternatives = 1; return ret;} static get isSupported() { return 'webkitSpeechRecognition' in window; }}; class Utils { static trim(str, chars) { return Utils.trimEnd(Utils.trimStart(str, chars), chars);} static trimStart(str, chars) { const regex = new RegExp(`^[${chars}]+`, 'g'); return str.replace(regex, '');} static trimEnd(str, chars) { const regex = new RegExp(`[${chars}]+$`, 'g'); return str.replace(regex, '');} static escapeGuidId(id) { let firstChar = id.substring(0, 1); if (!Utils.isNumber(firstChar)) return id; let code = id.charCodeAt(0); return `\\${code.toString(16)} ${id.substring(1)}`;} static isNumber(value) { return !isNaN(parseFloat(value)) && isFinite(value);} static isNullOrEmpty(value) { return value == null || value === '';} static isLetter(char) { return char.match(/[A-Za-z]/) != null;} static isLetterOrDigit(char) { if (char == null) return false; return char.match(/\w/) != null;} static isDigit(char) { return char.match(/[0-9]/) != null;} static isNullOrWhiteSpace(value) { return value == null || value.trim() === "";} static escapeControlCharacters(text) { return text.replaceAll('\n', '\\n').replaceAll('\r', '\\r').replaceAll('\t', '\\t').replaceAll('\u200b', '#');} static unEscapeControlCharacters(text) { return text.replaceAll('\\n', '\n').replaceAll('\\r', '\r').replaceAll('\\t', '\t');} static escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');} static radToDeg(angle) { return angle * 180 / Math.PI; } static degToRad(angle) { return Math.PI * angle * 180; } static levenshteinDistance(s, t) { if (!s.length) return t.length; if (!t.length) return s.length; const arr = []; for (let i = 0; i <= t.length; i++) { arr[i] = [i]; for (let j = 1; j <= s.length; j++) { if (i == 0) { arr[i][j] = j; continue; } let swapCost = s[j - 1] === t[i - 1] ? 0 : 1; arr[i][j] = Math.min(arr[i - 1][j] + 1, arr[i][j - 1] + 1, arr[i - 1][j - 1] + swapCost);}} return arr[t.length][s.length];}; static launchUrl(URL, e, target = null, features = null) { switch (true) { case e.ctrlKey && !e.shiftKey && !e.altKey: window.location.href = Utils.mapPath(URL); break; case !e.ctrlKey && !e.shiftKey && !e.altKey: window.open(Utils.mapPath(URL), target, features); break;}} static cycleClasses(element, classNames, direction) { let curIndex = Utils.getClassIndex(element, classNames); let newIndex = Utils.applyLimit(curIndex + direction, 0, classNames.length - 1); if (newIndex == curIndex) return; if (curIndex != -1) element.classList.remove(classNames[curIndex]); element.classList.add(classNames[newIndex]);} static getClassIndex(element, classNames) { let index = 0; for (const className of classNames) { if (element.classList.contains(className)) return index; index++;} return -1;} static toggleClass(element, className) { Utils.setClass(element, className, !element.classList.contains(className));} static setClass(element, className, enabled) { if (enabled) element.classList.add(className); else element.classList.remove(className);} static setButtonState(element, enabled) { if (element) element.disabled = !enabled;} static setButtonVisible(element, visible) { if (element) element.hidden = !visible; Utils.setButtonState(element, visible);} static setBooleanAttribute(element, name, enabled) { if (enabled) element.setAttribute(name, 'true'); else element.removeAttribute(name);} static isButtonEnabled(button) { return button.matches('button:not(:disabled)');} static getKeyID(e) { if (!e.key) return null; let keyName = e.key.length == 1 ? e.key.toUpperCase() : e.key; let modKeys = Utils.getModifierKeys(e); return modKeys ? modKeys + ' + ' + keyName : keyName;} static getModifierKeys(e) { var values = []; if (e.altKey && !(e.key == 'Alt')) values.push('Alt'); if (e.ctrlKey && !(e.key == 'Control')) values.push('Control'); if (e.shiftKey && !(e.key == 'Shift')) values.push('Shift'); return values.join(' + ');} static isCharacterKey(e) { return e.key != null && ((e.key.length == 1) && !(e.ctrlKey || e.altKey));} static isMatchingCharacterKey(e, criteria) { return this.isCharacterKey(e) && (e.key.match(criteria) != null);} static isModifierKey(e) { return ['Alt', 'Control', 'Shift'].includes(e.key);} static uploadSessionObject(name, object, oncomplete = null) { if (object == null) return oncomplete ? oncomplete() : null; HTTP.post(`/Shared/SetSessionObject/${encodeURIComponent(name)}`, object, oncomplete);} static downloadSessionObject(name, oncomplete) { HTTP.get(`/Shared/GetSessionObject/${encodeURIComponent(name)}`, oncomplete);} static fetchObjectJSON(typeName) { return window.localStorage.getItem(typeName);} static fetchObjectData(typeName) { let json = window.localStorage.getItem(typeName); let ret = JSON.parse(json); if (ret == null) ret = {}; return ret;} static saveObjectData(typeName, data) { window.localStorage.setItem(typeName, JSON.stringify(data));} static levenshteinDistance(str1, str2) { const len1 = str1.length; const len2 = str2.length; const dp = Utils.computeMatrix(str1, str2); return dp[len1][len2];} static computeMatrix(str1, str2) { const len1 = str1.length; const len2 = str2.length; const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0)); for (let i = 0; i <= len1; i++) { dp[i][0] = i;} for (let j = 0; j <= len2; j++) { dp[0][j] = j;} for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1];} else { dp[i][j] = Math.min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1 );}}} return dp;} static cloneObject(item) { return JSON.parse(JSON.stringify(item));} static get layout() { return HtmlControl.get(document.body); } static get timeZone() { let date = new Date(); let text = date.toString(); let result = text.match(/(?<=\().*(?=\))/); return result[0]} static appRoot = "/"; static mapPath(path) { if (Utils.appRoot == "/") return path; if (path.startsWith('http://') || path.startsWith('https://')) return path; if (path.startsWith(Utils.appRoot)) return path; return Utils.appRoot + path.substring(1);} static selectContents(element) { let sel = window.getSelection(); let range = document.createRange(); range.selectNodeContents(element); sel.removeAllRanges(); sel.addRange(range);} static applyLimit(value, min, max) { return Math.max(min, Math.min(max, value));} static copyAttributes(srcElement, dstElement) { [...dstElement.attributes].forEach(attr => dstElement.removeAttribute(attr.name)); [...srcElement.attributes].forEach(attr => dstElement.setAttribute(attr.name, attr.value));} static copyDataAttributes(srcElement, dstElement) { [...dstElement.attributes].forEach(attr => { if (attr.name.startsWith('data-')) dstElement.removeAttribute(attr.name);}); [...srcElement.attributes].forEach(attr => { if (attr.name.startsWith('data-')) dstElement.setAttribute(attr.name, attr.value);});} static compareValue(valueA, valueB) { switch (true) { case valueA > valueB: return 1 case valueA < valueB: return -1 default: return 0;}} static findChild(parent, selector) { for (const child of parent.children) { if (child.matches(selector)) return child;} return null;} static clearTextSelection() { let windowSel = window.getSelection ? window.getSelection() : null; if (windowSel && windowSel.rangeCount) return windowSel.removeAllRanges(); if (document.selection && document.selection.type != "Control") document.selection.removeAllRanges();}} class Constants { static emptyGuid = '00000000-0000-0000-0000-000000000000';}; class Line { X1; Y1; X2; Y2; constructor(X1, Y1, X2, Y2) { this.X1 = X1; this.Y1 = Y1; this.X2 = X2; this.Y2 = Y2;} get width() { return this.X2 - this.X1; } get height() { return this.Y2 - this.Y1; } get slope() { return (this.Y2 - this.Y1) / (this.X2 - this.X1); } get yIntercept() { return this.Y1 - this.slope * this.X1; } get angle() { return Math.atan(this.slope); } get ptA() { return new Point(this.X1, this.Y1); } get ptB() { return new Point(this.X2, this.Y2); } get isStraight() { return this.X1 == this.X2 || this.Y1 == this.Y2; } get length() { return this.ptA.distanceFrom(this.ptB); } get center() { return new Point((this.X1 + this.X2) / 2, (this.Y1 + this.Y2) / 2); } get bounds() { let ret = Rectangle.fromLTRB(this.X1, this.Y1, this.X2, this.Y2); ret.normalize(); return ret;} generateTooltip(options, units) { return `${this.ptA.generateTooltip(options, units)} : ${this.ptB.generateTooltip(options, units)}`;} scaleXY(factorX, factorY) { this.X1 *= factorX; this.Y1 *= factorY; this.X2 *= factorX; this.Y2 *= factorY;} cloneScaledXY(factorX, factorY) { let ret = this.clone(); ret.scaleXY(factorX, factorY); return ret;} scale(factor) { this.scaleXY(factor, factor); } clone() { return new Line(this.X1, this.Y1, this.X2, this.Y2); } cloneScaled(factor) { return this.cloneScaledXY(factor, factor); } distanceTo(pt) { let numerator = Math.abs(((this.Y2 - this.Y1) * pt.x) - ((this.X2 - this.X1) * pt.y) + (this.X2 * this.Y1) - (this.Y2 * this.X1)); let diffX = this.X2 - this.X1, diffY = this.Y2 - this.Y1; let denominator = Math.sqrt((diffX * diffX) + (diffY * diffY)); return numerator / denominator;} segmentDistanceTo(pt) { let edgeA = Line.fromPoints(pt, this.ptA), edgeB = Line.fromPoints(pt, this.ptB), edgeC = Line.fromPoints(this.ptA, this.ptB); let a = edgeA.length, b = edgeB.length, c = edgeC.length; let valueBC = ((b * b) + (c * c) - (a * a)) / (2.0 * b * c); let valueAC = ((a * a) + (c * c) - (b * b)) / (2.0 * a * c); let angleBC = Utils.radToDeg(Math.acos(valueBC)); let angleAC = Utils.radToDeg(Math.acos(valueAC)); if ((angleBC > 90.0) || (angleAC > 90.0)) return Math.min(pt.distanceFrom(this.ptA), pt.distanceFrom(this.ptB)); return this.distanceTo(pt);} intersectsLine(line) { return (this.getIntersection(line) != null); } getIntersection(line) { let X3 = line.X1, X4 = line.X2, Y3 = line.Y1, Y4 = line.Y2; if (this.X1 == this.X2 && this.Y1 == this.Y2 || X3 == X4 && Y3 == Y4) return null; let denominator = ((Y4 - Y3) * (this.X2 - this.X1) - (X4 - X3) * (this.Y2 - this.Y1)) if (denominator == 0) return null; let unknownA = ((X4 - X3) * (this.Y1 - Y3) - (Y4 - Y3) * (this.X1 - X3)) / denominator; let unknownB = ((this.X2 - this.X1) * (this.Y1 - Y3) - (this.Y2 - this.Y1) * (this.X1 - X3)) / denominator; if (unknownA < 0 || unknownA > 1 || unknownB < 0 || unknownB > 1) return null; let X = this.X1 + unknownA * (this.X2 - this.X1); let Y = this.Y1 + unknownA * (this.Y2 - this.Y1); return new Point(X, Y);} static fromPoints(ptA, ptB) { return new Line(ptA.x, ptA.y, ptB.x, ptB.y); } 1} ; class Point { x; y; constructor(x, y) { this.x = x; this.y = y;} generateTooltip(options, units) { return `(${this.x.toLocaleString(undefined, options)}${units}, ${this.y.toLocaleString(undefined, options)}${units})`;} scale(factor) { this.scaleXY(factor, factor);} scaleXY(factorX, factorY) { this.x *= factorX; this.y *= factorY;} cloneScaled(factor) { return this.cloneScaledXY(factor, factor);} cloneScaledXY(factorX, factorY) { let ret = new Point(this.x, this.y); ret.scaleXY(factorX, factorY); return ret;} translate(amountX, amountY) { this.x += amountX; this.y += amountY;} distanceFrom(pt) { return Math.sqrt(Math.pow(Math.abs(pt.x - this.x), 2) + Math.pow(Math.abs(pt.y - this.y), 2));} angleTo(pt) { let dx = (pt.x - this.x), dy = (this.y - pt.y); return Math.atan2(dy, dx)} cloneTranslated(amountX, amountY) { let ret = new Point(this.x, this.y); ret.translate(amountX, amountY); return ret;} formatNum(value) { let val = Math.round(value * 100) / 100; return val.toLocaleString(undefined, { minimumFractionDigits: 2 }) + '"';} restrictTo(rect) { if (this.x < rect.left) this.x = rect.left; else if (this.x > rect.right) this.x = rect.right; if (this.y < rect.top) this.y = rect.top; else if (this.y > rect.bottom) this.y = rect.bottom;}} ; class Quadrilateral { tl; tr; br; bl; constructor(tl, tr, br, bl) { this.tl = tl; this.tr = tr; this.br = br; this.bl = bl;} get points() { return [this.tl, this.tr, this.br, this.bl]; } get bounds() { let left = Number.MAX_SAFE_INTEGER, top = Number.MAX_SAFE_INTEGER, right = 0, bottom = 0; for (const point of this.points) { left = Math.min(left, point.x); top = Math.min(top, point.y); right = Math.max(right, point.x); bottom = Math.max(bottom, point.y);} return Rectangle.fromLTRB(left, top, right, bottom);} get leftEdge() { return Line.fromPoints(this.tl, this.bl); } get topEdge() { return Line.fromPoints(this.tl, this.tr); } get rightEdge() { return Line.fromPoints(this.tr, this.br); } get bottomEdge() { return Line.fromPoints(this.bl, this.br); } get edges() { return [this.leftEdge, this.topEdge, this.rightEdge, this.bottomEdge]; } get isRectangle() { if (!this.leftEdge.isStraight) return false; if (!this.topEdge.isStraight) return false; if (!this.rightEdge.isStraight) return false; if (!this.bottomEdge.isStraight) return false; return true;} generateTooltip(options, units) { return `Top Left:\t\t${this.tl.generateTooltip(options, units)}\nTop Right:\t\t${this.tr.generateTooltip(options, units)}\n` + `Bottom Right:\t${this.br.generateTooltip(options, units)}\nBottom Left:\t\t${this.bl.generateTooltip(options, units)}`;} scale(factor) { this.scaleXY(factor, factor);} scaleXY(factorX, factorY) { this.tl.scaleXY(factorX, factorY); this.tr.scaleXY(factorX, factorY); this.br.scaleXY(factorX, factorY); this.bl.scaleXY(factorX, factorY);} round(precision) { let factor = Math.pow(10, precision); this.tl.x = Math.round(this.tl.x * factor) / factor; this.tl.y = Math.round(this.tl.y * factor) / factor; this.tr.x = Math.round(this.tr.x * factor) / factor; this.tr.y = Math.round(this.tr.y * factor) / factor; this.br.x = Math.round(this.br.x * factor) / factor; this.br.y = Math.round(this.br.y * factor) / factor; this.bl.x = Math.round(this.bl.x * factor) / factor; this.bl.y = Math.round(this.bl.y * factor) / factor;} clone() { let tl = new Point(this.tl.x, this.tl.y); let tr = new Point(this.tr.x, this.tr.y); let br = new Point(this.br.x, this.br.y); let bl = new Point(this.bl.x, this.bl.y); return new Quadrilateral(tl, tr, br, bl);} cloneRounded(precision) { let ret = this.clone(); ret.round(precision); return ret;} cloneScaled(factor) { return this.cloneScaledXY(factor, factor);} cloneScaledXY(factorX, factorY) { let ret = this.clone(); ret.scale(factorX, factorY); return ret;} containsPoint(point) { let x = Math.max(this.tr.x, this.br.x) + 5; let testLine = new Line(point.x, point.y, x, 0); let intersectionCount = 0; if (testLine.intersectsLine(this.topEdge)) intersectionCount += 1; if (testLine.intersectsLine(this.rightEdge)) intersectionCount += 1; if (testLine.intersectsLine(this.bottomEdge)) intersectionCount += 1; if (testLine.intersectsLine(this.leftEdge)) intersectionCount += 1; return (intersectionCount == 1);} restrictTo(rect) { for (const point of this.points) point.restrictTo(rect);} adjustLeft(x, minSize, size) { let min = (Math.abs(this.tl.x - this.bl.x)) / 2; let max = this.isRectangle ? size.width - 1 : Math.min(this.tr.x - min - minSize, this.br.x - min - minSize); let adjusted = Utils.applyLimit(x, min, max); let center = (this.tl.x + this.bl.x) / 2; let amount = adjusted - center; this.tl.x += amount; this.bl.x += amount;} adjustTop(y, minSize, size) { let min = (Math.abs(this.tl.y - this.tr.y)) / 2; let max = this.isRectangle ? size.height - 1 : Math.min(this.bl.y - min - minSize, this.br.y - min - minSize); let adjusted = Utils.applyLimit(y, min, max); let center = (this.tl.y + this.tr.y) / 2; let amount = adjusted - center; this.tl.y += amount; this.tr.y += amount;} adjustRight(x, minSize, size) { let offset = (Math.abs(this.tr.x - this.br.x)) / 2; let min = this.isRectangle ? 0 : Math.max(this.tl.x + offset + minSize, this.bl.x + offset + minSize); let max = size.width - 1 - offset; let adjusted = Utils.applyLimit(x, min, max); let center = (this.tr.x + this.br.x) / 2; let amount = adjusted - center; this.tr.x += amount; this.br.x += amount;} adjustBottom(y, minSize, size) { let offset = (Math.abs(this.bl.y - this.br.y)) / 2; let min = this.isRectangle ? 0 : Math.max(this.tl.y + offset + minSize, this.tr.y + offset + minSize); let max = size.height - 1 - offset; let adjusted = Utils.applyLimit(y, min, max); let center = (this.bl.y + this.br.y) / 2; let amount = adjusted - center; this.bl.y += amount; this.br.y += amount;} adjustTL(point, minSize, square, size) { if (square) { this.tl.x = Utils.applyLimit(point.x, 0, size.width - 1); this.tl.y = Utils.applyLimit(point.y, 0, size.height - 1); this.bl.x = this.tl.x; this.tr.y = this.tl.y;} else { this.tl.x = Utils.applyLimit(point.x, 0, this.tr.x - minSize); this.tl.y = Utils.applyLimit(point.y, 0, this.bl.y - minSize);}} adjustTR(point, minSize, square, size) { if (square) { this.tr.x = Utils.applyLimit(point.x, 0, size.width - 1); this.tr.y = Utils.applyLimit(point.y, 0, size.height - 1); this.br.x = this.tr.x; this.tl.y = this.tr.y;} else { this.tr.x = Utils.applyLimit(point.x, this.tl.x + minSize, size.width - 1); this.tr.y = Utils.applyLimit(point.y, 0, this.br.y - minSize);}} adjustBR(point, minSize, square, size) { if (square) { this.br.x = Utils.applyLimit(point.x, 0, size.width - 1); this.br.y = Utils.applyLimit(point.y, 0, size.height - 1); this.tr.x = this.br.x; this.bl.y = this.br.y;} else { this.br.x = Utils.applyLimit(point.x, this.bl.x + minSize, size.width - 1); this.br.y = Utils.applyLimit(point.y, this.tr.y + minSize, size.height - 1);}} adjustBL(point, minSize, square, size) { if (square) { this.bl.x = Utils.applyLimit(point.x, 0, size.width -1); this.bl.y = Utils.applyLimit(point.y, 0, size.height - 1); this.tl.x = this.bl.x; this.br.y = this.bl.y;} else { this.bl.x = Utils.applyLimit(point.x, 0, this.br.x - minSize); this.bl.y = Utils.applyLimit(point.y, this.tl.y + minSize, size.height - 1);}} static fromRectangle(rect) { return new Quadrilateral(rect.topLeft, rect.topRight, rect.bottomRight, rect.bottomLeft);}}; class Rectangle { left; top; width; height; constructor(left, top, width, height) { this.left = left; this.top = top; this.width = width; this.height = height;} get right() { return this.left + this.width; } get bottom() { return this.top + this.height; } get centerX() { return this.left + (this.width / 2); } get centerY() { return this.top + (this.height / 2); } get center() { return new Point(this.centerX, this.centerY); } get hasSize() { return (this.width != 0) && (this.height != 0); } get isEmpty() { return (this.left == 0) && (this.right == 0) && (this.width == 0) && (this.height == 0); } get topLeft() { return new Point(this.left, this.top); } get topCenter() { return new Point(this.centerX, this.top); } get topRight() { return new Point(this.right, this.top); } get bottomLeft() { return new Point(this.left, this.bottom); } get bottomCenter() { return new Point(this.centerX, this.bottom); } get bottomRight() { return new Point(this.right, this.bottom); } get leftLine() { return Line.fromPoints(this.topLeft, this.bottomLeft); } get rightLine() { return Line.fromPoints(this.topRight, this.bottomRight); } get topLine() { return Line.fromPoints(this.topLeft, this.topRight); } get bottomLine() { return Line.fromPoints(this.bottomLeft, this.bottomRight); } get edgeLines() { return [this.leftLine, this.topLine, this.rightLine, this.bottomLine]; } get toLogicalRect() { return { Left: `${this.left}in`, Top: `${this.top}in`, Width: `${this.width}in`, Height: `${this.height}in` }; } get toRectangleExF() { return { X1: `${this.left}`, Y1: `${this.top}`, X2: `${this.right}`, Y2: `${this.bottom}` }; } get asRectangleExF() { return { Left: this.left, Top: this.top, Right: this.right, Bottom: this.bottom }; } toString() { return `left: ${this.left}, top: ${this.top}, width: ${this.width}, height: ${this.height}`;} intersectsRect(refRect) { if (this.isEmpty || refRect.isEmpty) return false; return !((this.left >= refRect.right) || (this.top >= refRect.bottom) || (this.right <= refRect.left) || (this.bottom <= refRect.top));} containsRect(refRect) { if (this.isEmpty || refRect.isEmpty) return false; return (refRect.left >= this.left) && (refRect.top >= this.top) && (refRect.right <= this.right) && (refRect.bottom <= this.bottom);} containsPoint(point) { if (this.isEmpty) return false; return (point.x >= this.left) && (point.y >= this.top) && (point.x <= this.right) && (point.y <= this.bottom);} scale(factor) { this.scaleXY(factor, factor);} scaleXY(factorX, factorY) { this.left *= factorX; this.top *= factorY; this.width *= factorX; this.height *= factorY;} cloneScaled(factor) { return this.cloneScaledXY(factor, factor);} cloneScaledXY(factorX, factorY) { var ret = new Rectangle(this.left, this.top, this.width, this.height); ret.scale(factorX, factorY); return ret;} translate(amountX, amountY) { this.left += amountX; this.top += amountY;} expand(amount) { this.expandXY(amount, amount);} expandXY(amountX, amountY) { this.left -= amountX; this.top -= amountY; this.width += amountX * 2; this.height += amountX * 2;} moveLeftTo(position) { this.width += (this.left - position); this.left = position;} moveTopTo(position) { this.height += (this.top - position); this.top = position;} moveRightTo(position) { this.width = position - this.left;} moveBottomTo(position) { this.height = position - this.top;} generateTooltip(options, units) { let left = this.left.toLocaleString(undefined, options) + units; let top = this.top.toLocaleString(undefined, options) + units; let width = this.width.toLocaleString(undefined, options) + units; let height = this.height.toLocaleString(undefined, options) + units; return `left: ${left}, top: ${top}\nsize: ${width} x ${height}`;} normalize() { if (this.width < 0) { this.left += this.width; this.width = - this.width;} if (this.height < 0) { this.top += this.height; this.height = - this.height;}} formatNum(value) { let val = Math.round(value * 100) / 100; return val.toLocaleString(undefined, { minimumFractionDigits: 2 }) + '"';} round(precision) { let factor = Math.pow(10, precision); this.left = Math.round(this.left * factor) / factor; this.top = Math.round(this.top * factor) / factor; this.width = Math.round(this.width * factor) / factor; this.height = Math.round(this.height * factor) / factor;} cloneRounded(precision) { let rect = new Rectangle(this.left, this.top, this.width, this.height); rect.round(precision); return rect;} static fromParamList(value) { let coords = value.split(','); return Rectangle.fromLTRB(parseFloat(coords[0]), parseFloat(coords[1]), parseFloat(coords[2]), parseFloat(coords[3]));} static fromLTRB(left, top, right, bottom) { return new Rectangle(left, top, right - left, bottom - top);} static fromDOMRect(rect) { return new Rectangle(rect.x, rect.y, rect.width, rect.height);}} ; class Size { width; height; constructor(width, height) { this.width = width; this.height = height;} scale(factor) { this.scaleXY(factor, factor);} scaleXY(factorX, factorY) { this.width *= factorX; this.height *= factorY;} cloneScaled(factor) { return this.cloneScaledXY(factor, factor);} cloneScaledXY(factorX, factorY) { let ret = new Point(this.width, this.height); ret.scaleXY(factorX, factorY); return ret;}} ; const ChangeKinds = { None: 0, Added: 1, Moved: 2, Deleted: 3, Reordered: 4, Edited: 5, Saved: 6} ; class HtmlControl { element; _settings; static formEditedEvent = 'HtmlControl.formEdited'; constructor(element) { this.element = element; this.element.controlInstance = this; this.addClass(this.controlType);} get controlType() { return this.dataset.control; } get focusableDescendants() { return this.element.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); } get firstFocusableDescendant() { return this.focusableDescendants[0]; } get lastFocusableDescendant() { return this.focusableDescendants[this.focusableDescendants.length - 1]; } get parentControl() { return (this.element.parentElement == null) ? null : HtmlControl.get(this.element.parentElement); } get parentDialog() { return this.parentDialogElement ? HtmlControl.get(this.parentDialogElement) : null; } get parentDialogElement() { return this.element.closest('.ModalBox'); } get formBounds() { return this.parentDialog ? this.parentDialog.dialogBounds : new Rectangle(0, 0, window.innerWidth, window.innerHeight); } get clientBounds() { return this.element.getBoundingClientRect(); } get layout() { return HtmlControl.get(this.element, 'Layout'); } get dataset() { return this.element.dataset; } get parentPage() { return this.layout.page; } get repositoryId() { return this.parentPage.repositoryId; } get isInDOM() { return this.layout != null; } get controlTypes() { var control = this, ret = []; while (control) { ret.push(control.controlType); control = control.parentControl;} return ret;} triggerButtonShortcut(e) { if (Utils.isModifierKey(e)) return; let button = this.findIconButton(e); if (button != null && Utils.isButtonEnabled(button)) { button.click(); e.preventDefault(); e.stopPropagation(); return true;} else { return false;}} findIconButton(e) { let fullKey = ContextMenu.toWindowsKey(e); for (const button of this.element.querySelectorAll('button.material-icons[data-shortcut], button.labeled[data-shortcut]')) { if (button.dataset.shortcut == fullKey) return button;} return null;} find(selector) { return this.element.querySelector(selector);} findChild(selector) { for (const child of this.element.children) { if (child.matches(selector)) return child;} return null;} *findChildren(selector) { for (const child of this.element.children) { if (child.matches(selector)) yield child;}} addListener(eventName, handler) { this.element.addEventListener(eventName, handler);} addListenerTo(selector, eventName, handler) { let child = this.find(selector); if (child) child.addEventListener(eventName, handler);} removeListener(eventName, handler) { this.element.removeEventListener(eventName, handler);} raiseEvent(name, detail = null) { let options = detail ? { detail: detail } : null; this.element.dispatchEvent(new CustomEvent(name, options));} bubbleEvent(name, detail = null) { let options = { "bubbles": true, detail: detail }; this.element.dispatchEvent(new CustomEvent(name, options));} addClass(className) { this.element.classList.add(className);} removeClass(className) { this.element.classList.remove(className);} hasClass(className) { return this.element.classList.contains(className);} toggleClass(className) { if (this.hasClass(className)) this.removeClass(className); else this.addClass(className);} get repositorySettings() { return this.findRepositorySettings() ?? this.createRepositorySettings();;} findRepositorySettings() { if (this.settings.repositorySettings == null) return null; for (const settings of this.settings.repositorySettings) { if (settings.repositoryId == this.repositoryId) return settings;} return null;} createRepositorySettings() { if (this.settings.repositorySettings == null) this.settings.repositorySettings = []; let ret = { repositoryId: this.repositoryId }; this.settings.repositorySettings.push(ret); return ret;} get settings() { if (this._settings == null) this._settings = HtmlControl.getSettings(this.controlType); return this._settings;} saveSettings() { window.localStorage.setItem(this.controlType, JSON.stringify(this._settings));} clearSettings() { window.localStorage.removeItem(this.controlType);} static getSettings(controlType) { let ret = window.localStorage.getItem(controlType); return ret ? JSON.parse(ret) : {};} static saveSettings(controlType, settings) { window.localStorage.setItem(controlType, JSON.stringify(settings));} static types = new Map(); static get(elementOrDescendant, typeName = null) { if (elementOrDescendant == null) return null; var selector = (typeName == null) ? '[data-control]' : `[data-control="${typeName}"]`; var element = elementOrDescendant.closest(selector); if (element == null) return null; return (element.controlInstance != null) ? element.controlInstance : HtmlControl.createControl(element);} static load(element, URL, oncomplete = null) { HTTP.get(URL, data => { element.innerHTML = data; let control = HtmlControl.initControl(element.firstElementChild); if (oncomplete) oncomplete(control);});} static loadPost(element, URL, postData, oncomplete = null) { HTTP.post(URL, postData, (data, xhr) => { element.innerHTML = data; let control = Utils.isNullOrEmpty(data) ? null : HtmlControl.initControl(element.firstElementChild); if (oncomplete) oncomplete(control, xhr);});} static loadContents(element, URL, postData, oncomplete = null) { element.innerHTML = Snippets.loaderHTML; HTTP.postJSON(URL, postData, { success: (data, xhr) => { element.innerHTML = data; let control = Utils.isNullOrEmpty(data) ? null : HtmlControl.initControl(element.firstElementChild); if (oncomplete) oncomplete(control, xhr);}, error: (xhr, errInfo) => { element.innerHTML = null; HTTP.handleError(xhr, errInfo);}});} static reload(element, URL, oncomplete = null) { HTTP.get(URL, (data, xhr) => { let control = HtmlControl.replaceContent(element, data); if (oncomplete) oncomplete(control, xhr);});} static reloadPost(element, URL, postData, oncomplete = null) { HTTP.post(URL, postData, (data, xhr) => { let control = HtmlControl.replaceContent(element, data); if (oncomplete) oncomplete(control, xhr);});} static initControl(element) { if ((element.dataset.control != null) && (element.controlInstance == null)) element.controlInstance = HtmlControl.createControl(element); return element.controlInstance;} static initControls() { var controlElements = document.querySelectorAll('[data-control]'); controlElements.forEach(function (element) { if (element.controlInstance == null) element.controlInstance = HtmlControl.createControl(element);})} static initBranch(root) { let controlElements = root.querySelectorAll('[data-control]'); controlElements.forEach(function (element) { if (element.controlInstance == null) element.controlInstance = HtmlControl.createControl(element);})} static createControl(element) { let className = element.dataset.control if (!HtmlControl.types.has(className)) HtmlControl.types.set(className, eval(`${className}.prototype.constructor`)); let controlType = HtmlControl.types.get(className); return new controlType(element);} static replaceContent(element, data) { let newElement = HTML.parseElement(data); element.parentNode.replaceChild(newElement, element); HtmlControl.initControl(newElement); return newElement.controlInstance;}}; class HtmlPage extends HtmlControl { constructor(element) { super(element);} get commands() { return this.layout.pageButtonRoot; } get repositoryId() { return this.dataset.repository ?? null; } get pageNamespace() { return this.controlType.replace('Page', ''); } get helpContext() { return `GrooperReview.Pages.${this.pageNamespace}.${this.controlType}`; }}; class Layout extends HtmlControl { sessionSecondsRemaining; constructor(element) { super(element); this.setColorPolarity(this.settings.liteMode); this.toastContainer.onclick = e => this.OnToastClick(e); this.topNav.onclick = e => this.OnNavClick(e); this.element.onkeydown = e => this.OnKeyDown(e); window.addEventListener('resize', () => Splitter.resizeControls(this.pageElement)); Utils.appRoot = this.dataset.appRoot; if (this.clientTZ) this.clientTZ.innerText = Utils.timeZone;} get topNav() { return this.element.firstElementChild; } get topNavLeft() { return this.topNav.firstElementChild; } get topNavCenter() { return this.topNavLeft.nextElementSibling; } get topNavRight() { return this.topNav.lastElementChild; } get pageButtonRoot() { return this.topNavRight.firstElementChild; } get navButtonRoot() { return this.topNavRight.lastElementChild; } get pageElement() { return this.topNav.nextElementSibling.firstElementChild; } get page() { return HtmlControl.get(this.pageElement); } get helpBaseURL() { return `/Help?repositoryId=${this.repositoryId}`; } get helpURL() { return this.page.helpContext ? `${this.helpBaseURL}&typeName=${encodeURIComponent(this.page.helpContext)}` : this.helpBaseURL } get sessionId() { return this.dataset.sessionId; } get userDropDown() { return HtmlControl.get(this.topNavRight.querySelector('#user_info')); } get darkModeButton() { return this.topNavRight.querySelector('#dark_mode'); } get darkModeLabel() { return this.topNavRight.querySelector('#dark-mode-label'); } get clientTZ() { return this.userDropDown?.element.querySelector('.client-tz'); } get root() { return this.element.closest('html'); } showHelp() { window.open(Utils.mapPath(this.helpURL), 'Grooper Help');} addInfoLabel(name, value) { let HTML = `${value}` this.topNavCenter.innerHTML = this.topNavCenter.innerHTML + HTML;} getElementState(elementId) { let ele = this.topNav.querySelector('#' + elementId); return ele.getAttribute('disabled') == null && !ele.classList.contains('disabled');} OnToastClick(e) { if (e.target.matches('#close')) this.hideToast();} OnNavClick(e) { switch (e.target.id) { case 'help': return this.showHelp(); case 'reset': return this.OnReset(); case 'dark_mode': return this.OnDarkMode(); case 'repository': return this.OnRepositoryButton(e);}} OnDarkMode() { this.settings.liteMode = !this.settings.liteMode; this.setColorPolarity(this.settings.liteMode); this.saveSettings();} setColorPolarity(liteMode) { this.element.className = liteMode ? 'ev00 lite-mode' : 'ev00 dark-mode'; if (this.darkModeLabel) { this.darkModeLabel.innerText = liteMode ? 'Light Mode' : "Dark Mode"; this.darkModeButton.innerText = liteMode ? 'light_mode' : 'dark_mode';}} OnRepositoryButton(e) { let modal = this.layout.createModal('RepositoryPicker'); let page = this.dataset.page == 'Error' ? 'Home' : this.dataset.page; modal.showRepositoryPicker(page);} OnReset() { this.userDropDown.close(); let modal = this.confirmBox("Reset all settings back to default values?", () => { window.localStorage.clear();});} OnKeyDown(e) { switch (Utils.getKeyID(e)) { case 'F1': this.showHelp(); return false; case 'Control + A': return false;}} get popups() { return this.findChild('#popups'); } getPopup(id) { return this.popups.querySelector('#' + id); } hidePopup(id) { this.getPopup(id)?.remove(); } showPopup(id, point, content, className) { return this.createPopup(id, point, content, className);} createPopup(id, point, content, className) { let popup = document.createElement('DIV'); popup.id = id; popup.className = `popup ${className}`; popup.role = 'tooltip'; popup.style.left = `${point.x}px`; popup.style.top = `${point.y}px`; popup.innerHTML = content; this.popups.appendChild(popup); let bounds = this.element.getBoundingClientRect(); let rect = popup.getBoundingClientRect(); if (rect.right > bounds.right) popup.style.left = `${bounds.right - rect.width}px`; if (rect.bottom > bounds.bottom) popup.style.top = `${bounds.bottom - rect.height}px`; return popup;} hidePopups() { this.popups.innerHTML = '';} showTooltip(point, content, id) { this.hidePopup(id); if (Utils.isNullOrEmpty(content)) return; return this.createPopup(id, point, content, 'tooltip ev06');} showControlTooltip(id, point, element) { this.hidePopup(id); let tooltip = this.createPopup(id, point, '', 'tooltip ev06'); tooltip.appendChild(element); HtmlControl.initControl(element); return tooltip;} toastTimer; get toastContainer() { return this.findChild('.toast'); } get toastElement() { return this.toastContainer; } get toastCloseButton() { return this.toastElement.firstElementChild.lastElementChild; } get toastTitle() { return this.toastElement.firstElementChild.firstElementChild; } get toastMessage() { return this.toastElement.lastElementChild; } get toastIsVisible() { return !this.toastContainer.matches('.d-none'); } hideToast() { this.clearToastTimer(); Utils.setClass(this.toastContainer, 'd-none', true);} showToast(title, msg, delay = 5, ontimeout = null) { this.clearToastTimer(); this.toastTitle.innerText = title; this.toastMessage.innerText = msg; Utils.setClass(this.toastContainer, 'd-none', false); if (delay != 0) { this.toastTimer = setTimeout(e => this.OnToastTimeout(ontimeout), delay * 1000);}} showHtmlToast(title, html, delay = 5, ontimeout = null) { this.clearToastTimer(); this.toastTitle.innerText = title; this.toastMessage.innerHTML = html; Utils.setClass(this.toastContainer, 'd-none', false); if (delay != 0) { this.toastTimer = setTimeout(e => this.OnToastTimeout(ontimeout), delay * 1000);}} OnToastTimeout(ontimeout) { this.toastTimer = null; if (!this.toastIsVisible) return; this.hideToast() if (ontimeout) ontimeout();} clearToastTimer() { if (this.toastTimer == null) return; clearTimeout(this.toastTimer); this.toastTimer = null;} get menuContainer() { return this.findChild('#menu-container'); } get menu() { return HtmlControl.get(this.menuContainer.firstElementChild); } showMenu(menuElement, x, y, menuOwner) { this.menuContainer.innerHTML = menuElement.outerHTML; this.menu.menuOwner = menuOwner; this.menu.showAt(x, y);} get activeModals() { return this.findChild('#modals'); } get modalTemplate() { return this.find('#modal-template'); } createModal(controlType = null) { let modalElement = this.cloneModal(); if (controlType) modalElement.dataset.control = controlType; this.activeModals.appendChild(modalElement); return HtmlControl.get(modalElement);} showModal(options) { let modal = this.createModal(); modal.showDialog(options); return modal;} showBrowser(options) { let modal = this.createModal('BrowseBox'); modal.showDialog(options); return modal;} showEditDialog(caption, URL, postData, onOk) { return this.showModal({ okText: 'Save', requireEdit: true, URL: URL, postData: postData, onOk: onOk, caption: caption });} confirmBox(prompt, onOk) { let modal = this.createModal(); let content = `
${prompt}
`; modal.showDialog({ content: content, caption: 'Confirmation', onOk: onOk }); return modal;} errorBox(message, options = null) { let modal = this.createModal('ErrorBox'); modal.showErrorBox(message, options);} showProgress(caption, message, oncancel = null) { let modal = this.createModal('ProgressBox'); modal.showProgress(caption, message, oncancel); return modal;} showUpload(caption, prompt, accept, onOk) { let modal = this.createModal(); let content = `
${prompt}
`; modal.showDialog({ content: content, caption: caption, onOk: onOk, okText: 'Upload' }); modal.okButton.disabled = true; return modal;} cloneModal() { let ret = this.modalTemplate.cloneNode(true); ret.controlInstance = null; ret.removeAttribute('id'); ret.querySelector('#dialog-body').innerHTML = null; return ret;} closeAllModals() { this.activeModals.innerHTML = '';} static get instance() { return HtmlControl.get(document.body); }}; class ComboBox extends HTMLElement { static get observedAttributes() { return ['value'];} constructor() { super(); this.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` .dropdown-list { display: none; position: absolute; z-index: 10; background: white; border: 1px solid #ccc; min-width: 100%;} .dropdown-list.show { display: block;} .combo-wrapper { position: relative; display: inline-block;} button { margin-left: 2px;} `; const wrapper = document.createElement('div'); wrapper.className = 'combo-wrapper'; const input = document.createElement('input'); input.setAttribute('part', 'input'); const button = document.createElement('button'); button.setAttribute('part', 'button'); button.type = 'button'; button.tabIndex = -1; button.innerHTML = '▼'; const select = document.createElement('select'); select.setAttribute('part', 'select'); select.size = 5; select.className = 'dropdown-list'; wrapper.appendChild(input); wrapper.appendChild(button); wrapper.appendChild(select); this.shadowRoot.append(style, wrapper); this.input = input; this.button = button; this.select = select; this.wrapper = wrapper; this._suppressEvent = false; this._onDocumentClick = this._onDocumentClick.bind(this);} connectedCallback() { this.updateOptions(); this._syncValueFromAttribute(); this.input.addEventListener('input', () => { this._setValue(this.input.value, true); const filter = this.input.value.toLowerCase(); Array.from(this.select.options).forEach(option => { option.style.display = option.text.toLowerCase().includes(filter) ? '' : 'none';}); this._showDropdown();}); this.button.addEventListener('click', (e) => { e.preventDefault(); if (this.select.classList.contains('show')) { this._hideDropdown();} else { this._showDropdown(); this.input.focus();}}); this.select.addEventListener('change', () => { this._setValue(this.select.value, true); this._hideDropdown(); this.input.focus();}); document.addEventListener('mousedown', this._onDocumentClick);} disconnectedCallback() { document.removeEventListener('mousedown', this._onDocumentClick);} _onDocumentClick(e) { if (!this.contains(e.target) && !this.shadowRoot.contains(e.target)) { this._hideDropdown();}} _showDropdown() { this.select.classList.add('show');} _hideDropdown() { this.select.classList.remove('show');} attributeChangedCallback(name, oldValue, newValue) { if (name === 'value' && oldValue !== newValue) { this._syncValueFromAttribute();}} get value() { return this.getAttribute('value') || '';} set value(val) { this.setAttribute('value', val); this._syncValueFromAttribute();} _syncValueFromAttribute() { const val = this.value; this.input.value = this._getOptionTextByValue(val) || val; this.select.value = val;} _setValue(val, fireEvents = false) { const option = Array.from(this.select.options).find(opt => opt.value === val || opt.text === val); let newValue = val; if (option) { newValue = option.value; this.input.value = option.text; this.select.value = option.value;} else { this.input.value = val; this.select.value = '';} if (this.value !== newValue) { this._suppressEvent = !fireEvents; this.value = newValue; this._suppressEvent = false;} if (fireEvents) { this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true }));}} _getOptionTextByValue(val) { const option = Array.from(this.select.options).find(opt => opt.value === val); return option ? option.text : '';} updateOptions() { const options = this.querySelectorAll('option'); this.select.innerHTML = ''; options.forEach(opt => { const clone = opt.cloneNode(true); this.select.appendChild(clone);}); this._syncValueFromAttribute();}} customElements.define('combo-box', ComboBox); ; class CandidateList extends HtmlControl { static selectionChangedEvent = 'CandidateList:SelectionChanged'; static typeAssignEvent = 'CandidateList:TypeAssign'; rowCache; constructor(element) { super(element); this.addListener('keydown', e => this.OnKeyDown(e)); this.addListener('keyup', e => this.OnKeyUp(e)); this.addListener('input', e => this.OnInput(e)); this.addListener(ObjectList.selectionChangedEvent, e => this.bubbleEvent(CandidateList.selectionChangedEvent)); this.list.addListener('dblclick', e => this.assignType(e)); this.rowCache = [...this.list.rows]; this.rowCache.sort(CandidateList.compareRows); this.sizeColumns();} OnResize() { this.sizeColumns();} get focusedTypeName() { return this.list.focusedRow?.cells[0].innerText; } get focusedTypeId() { return this.list.focusedRow?.dataset.id; } get assignedTypeId() { return this.list.dataset.selectedTypeId; } get list() { return HtmlControl.get(this.element.lastElementChild); } get searchInput() { return this.element.firstElementChild.children[1].lastElementChild; } get searchText() { return this.searchInput.value; } get firstVisibleRow() { return (this.list.rows.length == 0) ? null : this.list.rows[0]; } get isDisabled() { return this.hasClass('disabled'); } disable() { this.addClass("disabled"); this.searchInput.disabled = true; this.list.clear();} enable() { this.removeClass("disabled"); this.searchInput.disabled = false;} sizeColumns() { let bounds = this.element.getBoundingClientRect(); let width = bounds.width - this.list.getColumnWidth(1) - 32; this.list.setColumnWidth(0, bounds.width - this.list.getColumnWidth(1) - 32);} assignType(e) { if (this.focusedTypeId == null) return; let pageIncrement; switch (true) { case e.ctrlKey: pageIncrement = -1; break; case e.altKey: pageIncrement = 0; break; default: pageIncrement = 1; break;} let arg = { typeId: this.focusedTypeId, pageIncrement: pageIncrement } this.bubbleEvent(CandidateList.typeAssignEvent, arg);} updateCandidates(data) { let newList = HTML.parseElement(data); let map = new Map(); for (const row of newList.querySelectorAll('tbody tr')) { if (map.has(row.dataset.id)) { if (parseInt(map.get(row.dataset.id)) > parseInt(row.cells[1].innerText)) { continue; }} map.set(row.dataset.id, row.cells[1].innerText);} this.list.dataset.selectedTypeId = newList.dataset.selectedTypeId; for (const row of this.rowCache) { if (map.has(row.dataset.id)) { row.cells[1].innerText = map.get(row.dataset.id);} else { row.cells[1].innerText = "0%";}} this.rowCache.sort(CandidateList.compareRows); this.filterRows(); let rowToFocus = this.findRow(this.assignedTypeId); if (rowToFocus) this.list.focusRow(rowToFocus, true, false);} findRow(docTypeId) { if (docTypeId == null || docTypeId == Constants.emptyGuid) return null; for (const row of this.list.rows) { if (row.dataset.id == docTypeId) return row;} return null;} filterRows(focusedTypeId = null) { this.list.tableBody.replaceChildren(...this.matchingRows); if (!Utils.isNullOrEmpty(this.searchInput.value)) { if (this.list.rows.length) this.list.focusRow(this.list.rows[0]);} let rowToFocus = this.findRow(this.assignedTypeId) ?? this.findRow(focusedTypeId); if (rowToFocus == null && this.list.rows.length == 1) rowToFocus = this.list.rows[0]; if (rowToFocus) this.list.focusRow(rowToFocus, true, false); else this.list.clearSelection();} *getMatchingRows() { let searchText = this.searchText.toLowerCase(); if (Utils.isNullOrEmpty(searchText)) { for (const row of this.rowCache) yield row;} else { for (const row of this.rowCache) { if (row.cells[0].innerText.toLowerCase().includes(searchText)) yield row;}}} get matchingRows() { let searchText = this.searchText.toLowerCase(); if (Utils.isNullOrEmpty(searchText)) return this.rowCache; let distances = new Map(); let hits = [...this.getMatchingRows()]; for (const hit of hits) { let dist = Utils.levenshteinDistance(searchText, hit.cells[0].innerText.toLowerCase()); distances.set(hit.dataset.id, dist);} return hits.sort((a, b) => distances.get(a.dataset.id) - distances.get(b.dataset.id));} OnKeyDown(e) { switch(true) { case (e.key == 'Enter'): { this.assignType(e); break; } case (e.key == 'Escape'): { this.searchInput.value = ''; this.filterRows(); break; } case (e.target.tagName == 'INPUT'): break; case this.isFilterKey(e): { this.searchInput.focus(); break; }}} OnKeyUp(e) { if (e.target.matches('input') && Utils.getKeyID(e) == 'ArrowDown') { this.list.element.focus(); if (this.list.focusedRow == null && this.list.rows.length > 0) this.list.focusRow(this.list.rows[0]);}} OnInput(e) { this.filterRows(this.focusedTypeId);} isFilterKey(e) { switch (true) { case (e.ctrlKey): return false; case (e.altKey): return false; case (e.key == 'Backspace'): return true; case (e.key == 'Delete'): return true; case (e.key.length != 1): return false; default: return /[\w_ -]/.test(e.key);}} static compareRows(row1, row2) { let conf1 = parseFloat(row1.cells[1].innerText), conf2 = parseFloat(row2.cells[1].innerText); if (conf1 != conf2) return conf2 - conf1; let name1 = row1.cells[0].innerText, name2 = row2.cells[0].innerText; if (name1 > name2) return 1; if (name1 < name2) return -1; return 0;}}; class CardList extends HtmlControl { static selectionChangedEvent = 'CardList.SelectionChanged'; constructor(element) { super(element); if (this.allowSelect) element.onkeydown = e => this.OnKeyDown(e); if (this.allowSelect) element.onclick = e => this.OnClick(e); if (this.autoSelect && this.firstCard) this.selectCard(this.firstCard);} get cards() { return this.element.children; } get cardCount() { return this.cards.length; } get firstCard() { return this.element.firstElementChild; } get lastCard() { return this.element.lastElementChild; } get focusedCard() { return this.element.querySelector('div.focused'); } get selectedCards() { return [...this.getSelectedCards()]; } get selectedIndices() { return [...this.getSelectedIndices()]; } get options() { return parseInt(this.dataset.options); } get allowSelect() { return (this.options & CardList.Options.AllowSelect) != 0; } get autoSelect() { return this.allowSelect && (this.options & CardList.Options.AutoSelect) != 0; } get totalCount() { return this.dataset.count == null ? null : parseInt(this.dataset.count); } *getCards() { for (const child of this.element.children) { yield child; }} *getSelectedCards() { for (const child of this.cards) { if (child.matches('.selected')) yield child; }} *getSelectedIndices() { for (let i = 0; i < this.cardCount; i++) { if (this.cards[i].matches('.selected')) yield i;}} OnClick(e) { let ele = e.target.closest('.card'); if (ele) this.selectCard(ele);} OnKeyDown(e) { if (this.isDisabled) return; let card = this.focusedCard; if (card == null) { if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key) && this.firstCard) { this.selectCard(this.firstCard); return false;} return;} switch (Utils.getKeyID(e)) { case 'ArrowUp': this.selectCard(card.previousElementSibling); break; case 'ArrowDown': this.selectCard(card.nextElementSibling); break; case 'Home': this.selectCard(this.firstCard); break; case 'End': this.selectCard(this.lastCard); break; default: return;} return false;} deleteSelectedCards() { let cardToFocus = this.getCardToFocus(); for (const card of this.selectedCards) card.remove(); if (cardToFocus) this.selectCard(cardToFocus);} getCardToFocus() { let indices = this.selectedIndices; let prevIdx = indices[0] - 1, nextIdx = indices[indices.length - 1] + 1; if (nextIdx < this.cardCount) return this.cards[nextIdx]; else if (prevIdx >= 0) return this.cards[prevIdx]; else return null;} clear() { this.element.innerHTML = '';} selectCard(card) { if (card && card != this.focusedCard) { this.clearSelection(); card.classList.add('selected', 'focused'); this.bubbleEvent(CardList.selectionChangedEvent, card);}} clearSelection() { for (let card of this.selectedCards) { card.classList.remove('selected', 'focused'); }} updateFocusedCard(data) { this.focusedCard.innerHTML = data;} static Options = { None: 0, AllowSelect: 1, AutoSelect: 2};}; class CodeEditor extends HtmlControl { static ELEMENT_NODE = 1; static TEXT_NODE = 3; static LineDelimiter = '\n'; static selectionChangedEvent = 'CodeEditor:selectionChanged'; lastChangeTime; completeListElement; constructor(element) { super(element); Utils.setClass(this.content, 'd-none', false); this.element.onscroll = e => this.menu.hide(); this.element.oncontextmenu = e => this.OnContextMenu(e); this.content.oninput = e => this.OnInput(e); this.element.onwheel = e => this.OnWheel(e); this.content.oncut = e => !this.isReadOnly; this.content.onpaste = e => !this.isReadOnly; this.element.onkeydown = e => this.OnKeyDown(e); this.element.onkeyup = e => this.OnKeyUp(e); this.content.onmouseup = e => this.OnMouseUp(e); this.content.onclick = e => this.OnClick(e); Utils.setClass(this.element, 'word-wrap', this.wordWrapDefault); Utils.setClass(this.element, 'control-chars', this.showControlCharsDefault); if (this.wordWrapOverride == null || this.showControlCharsOverride == null) this.menu.addSeparator(); if (this.wordWrapOverride == null) this.menu.addOption('Text Wrap', 'Enables or disables text wrapping.', 'Alt + W', this.wordWrap); if (this.showControlCharsOverride == null) this.menu.addOption('Show Control Characters', 'Toggles the visibility of control characters.', 'Alt + S', this.showControlChars); this.menu.addListener(ContextMenu.executeEvent, e => this.OnMenuItemClick(e.detail)); this.menu.addListener(ContextMenu.closedEvent, e => this.OnMenuClosed(e)); this.previousState = this.currentState;} get isReadOnly() { return this.hasClass('readonly'); } get content() { return this.element.querySelector('.ce-content'); } get menuElement() { return this.element.lastElementChild; } get menu() { return HtmlControl.get(this.menuElement); } get highlight() { return this.element.hasAttribute('data-highlight'); } get complete() { return this.element.hasAttribute('data-complete'); } get commentStart() { return this.dataset.commentStart; } get commentEnd() { return this.dataset.commentEnd; } get completeList() { return this.completeListElement ? HtmlControl.get(this.completeListElement.firstElementChild) : null; } get completeListVisible() { return this.completeListElement != null; } get triggerCharacters() { return this.dataset.trigger; } get acceptsTab() { return this.element.hasAttribute('data-accepts-tab'); } get wordWrap() { return this.element.matches('.word-wrap'); } set wordWrap(value) { Utils.setClass(this.element, 'word-wrap', value); this.syncSettings(); } get wordWrapOverride() { return this.element.hasAttribute('data-word-wrap') ? (this.dataset.wordWrap == 'true') : null; } get wordWrapDefault() { return this.wordWrapOverride ?? (this.settings.wordWrap == true); } get showControlChars() { return this.element.matches('.control-chars'); } set showControlChars(value) { Utils.setClass(this.element, 'control-chars', value); this.syncSettings(); } get showControlCharsOverride() { return this.element.hasAttribute('data-show-control-chars') ? (this.dataset.showControlChars == 'true') : null; } get showControlCharsDefault() { return this.showControlCharsOverride ?? (this.settings.showControlChars == true); } setText(text) { this.content.innerText = text; this.registerChange(); if (this.highlight) this.startHighlightTimer();} syncSettings() { if ((this.settings.wordWrap == this.wordWrap) && (this.settings.showControlChars == this.showControlChars)) return; if (this.wordWrapOverride == null) this.settings.wordWrap = this.wordWrap; if (this.showControlCharsOverride == null) this.settings.showControlChars = this.showControlChars; this.saveSettings();} isEditKey(e) { if (Utils.isCharacterKey(e)) return true; if (e.ctrlKey || e.altKey) return false; return ['Enter', 'Backspace', 'Delete'].includes(e.key);} OnWheel(e) { if (!e.ctrlKey) return; e.preventDefault(); e.stopPropagation(); if (e.deltaY == 0) return; let direction = e.deltaY < 0 ? 1 : -1; Utils.cycleClasses(this.element, ['xxxs', 'xxs', 'xs', 'small', 'normal', 'large', 'xl', 'xxl', 'xxxl', 'xxxxl', 'xxxxxl'], direction);} OnContextMenu(e) { e.preventDefault(); this.layout.showMenu(this.menuElement, e.pageX, e.pageY, this.menu);} OnMenuClosed(e) { this.content.focus();} OnInput(e) { this.logContent("OnInput"); this.normalizeLines(); this.logContent("After Normalize"); this.lastChangeTime = performance.now(); switch (e.inputType) { case 'insertParagraph': let prefix = this.prevLine ? this.prevLine.match(/^\s+/) : null; if (prefix) { document.execCommand('insertHTML', false, prefix); return;} break;} this.registerChange(); if (this.highlight) this.startHighlightTimer(); if (this.complete) this.doCodeCompletion(e);} OnKeyUp(e) { if (Utils.isModifierKey(e)) return; this.registerSelection(); switch (Utils.getKeyID(e)) { case 'F12': e.stopPropagation(); e.preventDefault(); this.stopSpeechRecognition(false); break;}} OnMouseUp(e) { this.registerSelection();} OnKeyDown(e) { `` let menuItem = this.menu.getCommand(e); if (menuItem) { e.preventDefault(); this.OnMenuItemClick(menuItem); return false;} switch (Utils.getKeyID(e)) { case 'F12': if (this.isReadOnly) return; e.stopPropagation(); e.preventDefault(); if (!e.repeat) this.startSpeechRecognition(); break; case 'Enter': e.stopPropagation(); break; case 'Escape': this.hideCompleteList(); e.stopPropagation(); break; case 'Control + A': this.selectAll(); break; case 'Control + Shift + ': if (this.isReadOnly) return; this.requestCompletionData(null); e.preventDefault(); break; case 'Alt + W': this.wordWrap = !this.wordWrap; e.preventDefault(); break; case 'Alt + S': this.showControlChars = !this.showControlChars; e.preventDefault(); break; case 'Alt + Backspace': if (!this.isReadOnly && this.undoStack.length > 0) this.OnUndo(); break; case 'Tab': if (!this.isReadOnly) this.OnTabKey(e); break; case 'Shift + Tab': this.OnShiftTabKey(e); break; case 'Alt + K': if (!this.isReadOnly) this.OnAddComment(); break; case 'Alt + Shift + K': if (!this.isReadOnly) this.OnRemoveComment(); break;} if (this.completeList) this.completeList.OnKeyDown(e);} OnTabKey(e) { if (this.acceptsTab) { e.stopPropagation(); if (!this.completeListVisible && !this.isReadOnly) { e.preventDefault(); let selectedText = this.selectedText; if (this.isFullLineSelection) { this.tabLines(false);} else if (!selectedText.includes('\n')) { document.execCommand('insertHTML', false, ' ');}}}} OnShiftTabKey(e) { if (this.acceptsTab && !this.isReadOnly) { e.stopPropagation(); e.preventDefault(); if (this.isFullLineSelection) this.tabLines(true);}} OnClick(e) { if (this.completeList) this.completeList.close();} setCommandStates() { let selection = this.selectedSpan; this.menu.setCommandState('Cut', selection.length > 0 && !this.isReadOnly); this.menu.setCommandState('Copy', selection.length > 0); this.menu.setCommandState('Undo', this.undoStack.length > 0); this.menu.setCommandState('Redo', this.redoStack.length > 0);} OnMenuItemClick(menuItem) { switch (menuItem.command) { case 'Cut': return document.execCommand('cut'); case 'Copy': return document.execCommand('copy'); case 'Paste': return this.OnPaste(); case 'Undo': return this.OnUndo(); case 'Redo': return this.OnRedo(); case 'CreateGroup': return this.OnCreateGroup(); case 'StartRequiredMode': return this.OnStartRequiredMode(); case 'EndRequiredMode': return this.OnEndRequiredMode(); case 'Text Wrap': this.wordWrap = !this.wordWrap; this.menu.setCheckState(menuItem.command, this.wordWrap); break; case 'Show Control Characters': this.showControlChars = !this.showControlChars; this.menu.setCheckState(menuItem.command, this.showControlChars); break;}} OnStartRequiredMode() { this.replaceSelection('(?r)', this.selectedSpan.index + 4);} OnEndRequiredMode() { this.replaceSelection('(?-r)', this.selectedSpan.index + 5);} OnCreateGroup() { let replacement = `(?<>${this.selectedText})`; this.replaceSelection(replacement, this.selectedSpan.index + 3);} OnPaste() { navigator.clipboard.readText().then(text => { if (Utils.isNullOrEmpty(text)) return; let unixText = text.replaceAll('\r\n', '\r'); this.replaceSelection(unixText, this.selectedSpan.index + unixText.length);});} OnAddComment() { if (this.commentStart == null || this.isReadOnly) return; let replacement = this.commentStart + this.selectedText + this.commentEnd; let span = this.selectedSpan; span.length += (this.commentStart.length + this.commentEnd.length) + 1; this.replaceSelection(replacement, span.index, span.length);} OnRemoveComment() { if (this.commentStart == null || this.isReadOnly) return; let selectedText = this.selectedText, trimmed = selectedText.trim(); if (trimmed.startsWith(this.commentStart) && trimmed.endsWith(this.commentEnd)) { let leading = selectedText.match(/^\s*/)[0], trailing = selectedText.match(/\s*$/)[0]; let newLength = trimmed.length - (this.commentStart.length + this.commentEnd.length); let cleaned = trimmed.substring(this.commentStart.length, this.commentStart.length + newLength); let span = this.selectedSpan; span.length -= (this.commentStart.length + this.commentEnd.length) - 1; let replacement = leading + cleaned + trailing; this.replaceSelection(replacement, span.index, span.length);}} get completeWindowLocation() { if (this.caretBounds) return this.caretBounds.bottomLeft.cloneTranslated(-16, 4); return Rectangle.fromDOMRect(this.clientBounds).topLeft.cloneTranslated(4, 20);} get caretBounds() { let range = this.selectionRange; let rects = range.getClientRects(); if (rects.length == 0) { const tempSpan = document.createElement("span"); range.insertNode(tempSpan); rects = range.getClientRects(); tempSpan.remove();} if (rects.length > 0) return Rectangle.fromDOMRect(rects[0]); return this.computeEndBounds(range.startContainer);} computeEndBounds(node) { let range = document.createRange(); range.selectNodeContents(node); let rects = range.getClientRects(); if (rects.length == 0) return null; let rect = rects[rects.length - 1]; return new Rectangle(rect.right, rect.top, 4, rect.height);} get caretPosition() { let range = this.selectionRange; if (range == null) return 0; return this.getPosition(range.endContainer, range.endOffset);} getPosition(node, offset) { if ((node == this.content) && (node.childNodes.length == 0)) return 0; switch (node.nodeType) { case CodeEditor.TEXT_NODE: return CodeEditor.getNodeOffset(this.content, node) + offset; case CodeEditor.ELEMENT_NODE: if (node.childNodes.length == 0) return CodeEditor.getNodeOffset(this.content, node); if (offset == node.childNodes.length) { let lastNode = node.childNodes[offset - 1]; return CodeEditor.getNodeOffset(this.content, lastNode) + CodeEditor.getContent(lastNode).length;} else { return CodeEditor.getNodeOffset(this.content, node.childNodes[offset]);} default: return 0;}} set caretPosition(value) { let nodeRef = CodeEditor.getNodeRef(this.content, value); let sel = window.getSelection(); let range = document.createRange(); if (nodeRef == null) { range.selectNodeContents(this.content); range.collapse();} else { range.selectNode(nodeRef.node); if (nodeRef.offset != -1) range.setStart(nodeRef.node, nodeRef.offset); range.collapse(nodeRef.before);} sel.removeAllRanges(); sel.addRange(range);} static getNodeRef(baseNode, offset) { let nodeOffset = 0; for (const node of baseNode.childNodes) { let isNewLine = CodeEditor.isLineStart(node); let adjustedOffset = offset - nodeOffset - (isNewLine ? 1 : 0); let childRef = CodeEditor.getNodeRef(node, adjustedOffset); if (childRef != null) return childRef; let content = CodeEditor.getContent(node); let span = new Span(nodeOffset, content.length); switch (node.nodeType) { case CodeEditor.TEXT_NODE: if (span.includes(offset)) return { node: node, offset: offset - nodeOffset, before: true }; if (((span.endIndex + 1) == offset) && CodeEditor.isLastItem(node)) { return { node: node, offset: -1, before: false };} break; case CodeEditor.ELEMENT_NODE: if (((span.endIndex + 1) == offset) && CodeEditor.isLastItem(node)) { return { node: node, offset: -1, before: (node.nextSibling != null) };} break;} nodeOffset += span.length;} return null;} static isLastItem(node) { return (node.nextSibling == null) || (CodeEditor.isLineStart(node.nextSibling));} static getNodeOffset(baseNode, targetNode) { if (targetNode == baseNode) return 0; let offset = 0; for (const node of baseNode.childNodes) { let isLineStart = CodeEditor.isLineStart(node); if (isLineStart) offset++; if (node == targetNode) return offset; let childOffset = CodeEditor.getNodeOffset(node, targetNode); if (childOffset != -1) return offset + childOffset let content = CodeEditor.getContent(node); offset += isLineStart ? content.length - 1 : content.length;} return -1;} lastSelection; registerSelection() { let selection = this.selectedSpan; if (this.lastSelection && this.lastSelection.isEqual(selection)) return; this.lastSelection = selection; this.bubbleEvent(CodeEditor.selectionChangedEvent); this.setCommandStates();} selectAll() { let range = document.createRange(); range.selectNodeContents(this.content); let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);} get selectedText() { let span = this.selectedSpan; if (span.length == 0) return ''; let content = this.textContent; return content.substring(span.index, span.endIndex + 1);} set selectedText(value) { let range = this.selectionRange; range.deleteContents(); range.insertNode(document.createTextNode(value));} get selectedSpan() { let range = this.selectionRange; if ((range == null) || range.collapsed) return new Span(this.caretPosition, 0); let startIndex = this.getPosition(range.startContainer, range.startOffset); let endIndex = this.getPosition(range.endContainer, range.endOffset); return Span.fromIndices(startIndex, endIndex - 1);} get selectedSpanWindows() { let span = this.selectedSpan; let content = this.textContent.slice(0, span.index); span.index += this.getMatchCount(content, '\n'); span.length += this.getMatchCount(this.selectedText, '\n'); return span;} set selectedSpan(value) { if (value.length == 0) { this.caretPosition = value.index; return;} let startRef = CodeEditor.getNodeRef(this.content, value.index); let endRef = CodeEditor.getNodeRef(this.content, value.endIndex + 1); if ((startRef == null) || (endRef == null)) return; let range = this.getRange(value); let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);} get selectionRange() { let windowSel = window.getSelection ? window.getSelection() : null; if (windowSel && windowSel.rangeCount) return windowSel.getRangeAt(0); if (document.selection && document.selection.type != "Control") return document.selection.createRange(); return null;} get isFullLineSelection() { let span = this.selectedSpan; if (span.length == 0) return false; let content = this.textContent; let isLineStart = span.index == 0 || content[span.index - 1] == '\n'; let isLineEnd = (content[span.endIndex] == '\n') || (span.endIndex == content.length - 1) || (content[span.endIndex + 1] == '\n'); return isLineStart && isLineEnd;} previousState; undoStack = []; redoStack = []; get currentState() { return { selection: this.selectedSpan, content: this.content.innerHTML }; } get lastUndo() { return this.undoStack.length == 0 ? null : this.undoStack[this.undoStack.length - 1]; } registerChange() { this.redoStack = []; this.previousState.selection = this.lastSelection; this.undoStack.push(this.previousState); this.previousState = this.currentState; if (this.undoStack.length > 64) this.undoStack.shift();} OnUndo() { this.redoStack.push(this.currentState); let entry = this.undoStack.pop(); this.content.innerHTML = entry.content; this.selectedSpan = entry.selection; this.previousState = entry; this.setCommandStates(); this.bubbleEvent('input');} OnRedo() { this.undoStack.push(this.currentState); let entry = this.redoStack.pop(); this.content.innerHTML = entry.content; this.selectedSpan = entry.selection; this.previousState = entry; this.setCommandStates(); this.bubbleEvent('input');} completeRequestTime; completeChars; get completeRequestPending() { return this.completeChars != null; } doCodeCompletion(e) { if (this.completeListVisible) return this.completeList.OnInput(e); let triggerChar = this.getTriggerChar(e); if (this.shouldTrigger(triggerChar)) { return this.requestCompletionData(triggerChar);} if (this.completeRequestPending) this.completeChars += triggerChar;} requestCompletionData(triggerChar) { this.completeRequestTime = performance.now(); let postData = { controlId: this.element.id, content: this.textContent, caretIndex: this.caretPosition, trigger: triggerChar }; if (Utils.isLetterOrDigit(triggerChar)) { this.completeChars = triggerChar; postData.caretIndex -= 1;} else { this.completeChars = '';} let requestTime = this.completeRequestTime; HTTP.post('/CodeEditor/Complete', postData, data => this.displayCompleteList(data, requestTime));} displayCompleteList(data, requestTime) { if (requestTime != this.completeRequestTime) return; let completeChars = this.completeChars; this.completeChars = null; if (Utils.isNullOrEmpty(data)) return; for (let idx = 0; idx < completeChars.length; idx++) { if (!Utils.isLetterOrDigit(completeChars.charAt(idx))) return;} let control = HTML.parseElement(data); let insertText = control.dataset.insert, appendText = control.dataset.append; if (!Utils.isNullOrEmpty(insertText) || !Utils.isNullOrEmpty(appendText)) { this.replaceSelection(insertText + appendText, this.selectedSpan.index + insertText.length); return;} this.completeListElement = this.layout.showControlTooltip("complete", this.completeWindowLocation, control); this.completeListElement.classList.remove('ev06'); this.completeList.initialize(this, completeChars);} hideCompleteList() { this.layout.hidePopups(); this.completeListElement = null;} getReplaceRange(span, selection) { if (span.length == 0) return selection.getRangeAt(0).cloneRange(); return this.getRange(new Span(span.index, span.length + 1));} insertText(text, span, caretOffset, keyEvent = null) { let windowSel = window.getSelection(); if (windowSel) { let range = this.getReplaceRange(span, windowSel); range.deleteContents(); let isFirst = true; let lines = text.split(CodeEditor.LineDelimiter); lines.reverse(); for (const line of lines) { if (!isFirst) range.insertNode(document.createElement('BR')); let newNode = document.createTextNode(line); range.insertNode(newNode); if (isFirst) range.selectNode(newNode); isFirst = false;} range.collapse(); windowSel.removeAllRanges(); windowSel.addRange(range); if (caretOffset) this.caretPosition += caretOffset; this.hideCompleteList(); this.lastChangeTime = performance.now(); this.bubbleEvent('input'); if (this.highlight) this.startHighlightTimer(); if (this.complete && keyEvent) this.doCodeCompletion(keyEvent);}} shouldTrigger(triggerChar) { if (triggerChar == null) return false; if (!this.isTriggerChar(triggerChar)) return this.shouldTriggerSpecial(triggerChar); switch (triggerChar) { case '\b': return !this.completeListVisible && !Utils.isLetterOrDigit(this.nextChar) && !Utils.isNullOrWhiteSpace(this.currentLineLeftPart); case ' ': return !Utils.isNullOrWhiteSpace(this.currentLine) && !Utils.isLetterOrDigit(this.nextChar); default: return true;}} isTriggerChar(char) { return Utils.isNullOrEmpty(this.triggerCharacters) ? true : this.triggerCharacters.includes(char);} shouldTriggerSpecial(triggerChar) { if (!Utils.isLetter(triggerChar)) return false; if (Utils.isLetterOrDigit(this.nextChar)) return false; let currentLineLeft = this.currentLineLeftPart.trimStart(); return (currentLineLeft.length == 1) && Utils.isLetter(currentLineLeft);} getTriggerChar(e) { switch (e.inputType) { case 'insertText': return e.data && e.data.length == 1 ? e.data : null; case 'deleteContentBackward': return String.fromCharCode(8); default: return null;}} highlightTimer; startHighlightTimer() { if (Utils.isNullOrWhiteSpace(this.textContent)) return; this.stopHighlightTimer(); this.highlightTimer = setTimeout(() => this.doHighlight(this.textContent, this.selectedSpan), 100);} stopHighlightTimer() { if (this.highlightTimer) clearTimeout(this.highlightTimer); this.highlightTimer = null;} doHighlight(content, selection, oncomplete = null) { let requestTime = this.lastChangeTime; this.highlightTimer = null; let postData = { controlId: this.element.id, content: content }; this.logContent("BeforeHighlight"); HTTP.post('/CodeEditor/Highlight', postData, data => { if (this.lastChangeTime != requestTime) return; this.content.innerHTML = data; this.selectedSpan = selection; if (oncomplete) oncomplete(); this.logContent("AfterHighlight");});} get textContent() { let ret = ''; for (const node of this.content.childNodes) ret += CodeEditor.getContent(node); return ret;} static getContent(node) { switch (node.nodeType) { case CodeEditor.TEXT_NODE: return (CodeEditor.isLineStart(node) ? '\n' : '') + node.textContent; case CodeEditor.ELEMENT_NODE: return CodeEditor.getElementContent(node); default: return '';}} static getElementContent(element) { let ret = CodeEditor.isLineStart(element) ? CodeEditor.LineDelimiter : ''; for (const child of element.childNodes) ret += CodeEditor.getContent(child); return ret;} static isLineStart(node) { switch (node.tagName) { case 'DIV': return node.previousSibling != null || CodeEditor.IsBlankLineOne(node); case 'BR': return !CodeEditor.isFirstInBlock(node) && !CodeEditor.isLastInBlock(node);} if (node.nodeType == CodeEditor.TEXT_NODE) { if (node.previousSibling && node.previousSibling.tagName == 'DIV') { return true;}} return false;} static IsBlankLineOne(div) { if (!Utils.isNullOrWhiteSpace(div.innerText)) return false; return div.nextSibling != null && div.nextSibling.tagName != 'DIV';} static isFirstInBlock(node) { if (node.previousSibling != null) return false; let parent = node.parentElement; if (parent.tagName == 'DIV') return true; return CodeEditor.isFirstInBlock(parent);} static isLastInBlock(node) { if (node.nextSibling != null) return false; let parent = node.parentElement; if (parent.tagName == 'DIV') return true; return CodeEditor.isLastInBlock(parent);} logContent(title) {} get leftPart() { let content = this.textContent, caretIndex = this.caretPosition; if (Utils.isNullOrEmpty(content) || (caretIndex == 0)) return ''; return content.substring(0, caretIndex);} get rightPart() { let content = this.textContent, caretIndex = this.caretPosition; if (Utils.isNullOrEmpty(content) || (caretIndex == content.length)) return ''; return content.substring(caretIndex);} get currentLineIndex() { let content = this.textContent, caretIndex = this.caretPosition, lineIndex = 0; for (let idx = 0; idx < caretIndex; idx++) { if (content.charAt(idx) == '\n') lineIndex++;} return lineIndex;} get currentLineCaretIndex() { let content = this.textContent, caretIndex = this.caretPosition, lineStartIndex = 0; for (let idx = 0; idx < caretIndex; idx++) { if (content.charAt(idx) == '\n') lineStartIndex = idx + 1;} return caretIndex - lineStartIndex;} get prevLine() { return this.currentLineIndex == 0 ? null : this.textContent.split(CodeEditor.LineDelimiter)[this.currentLineIndex - 1];} get currentLine() { return this.textContent.split(CodeEditor.LineDelimiter)[this.currentLineIndex];} get currentLineLeftPart() { let content = this.currentLine, caretIndex = this.currentLineCaretIndex; if (Utils.isNullOrEmpty(content) || (caretIndex == 0)) return ''; return content.substring(0, caretIndex);} get currentLineRightPart() { let content = this.currentLine, caretIndex = this.currentLineCaretIndex; if (Utils.isNullOrEmpty(content) || (caretIndex == content.length)) return ''; return content.substring(caretIndex);} get nextChar() { let rightPart = this.rightPart; return Utils.isNullOrEmpty(rightPart) ? '' : rightPart.substring(0, 1);} recognition = null; startSpeechRecognition() { if (!Speech.isSupported || this.recognition) return; this.recognition = Speech.createRecognition(true); this.recognition.onaudiostart = e => this.element.classList.add('talking'); this.recognition.onresult = e => this.onSpeechResult(e); this.recognition.start();} stopSpeechRecognition() { this.recognition?.stop(); this.recognition = null; this.element.classList.remove('talking');} onSpeechResult(e) { this.selectedText = Speech.getTranscript(e); this.selectionRange.collapse();} replaceSelection(text, caretIndex, length = 0) { let src = this.textContent, selectedSpan = this.selectedSpan; let left = src.substring(0, selectedSpan.index), right = src.substring(selectedSpan.endIndex + 1); let dst = left + text + right; this.applyChange(dst, new Span(caretIndex, length));} applyChange(content, selection) { this.doHighlight(content, selection, () => { this.registerChange(); this.setCommandStates(); this.bubbleEvent('input');});} tabLines(reverse) { let srcLines = this.textContent.split('\n'), dstLines = []; let selection = this.selectedSpan, charIndex = 0, charCount = 0; for (const srcLine of srcLines) { let lineSpan = new Span(charIndex, srcLine.length); let dstLine = lineSpan.intersects(selection) ? this.tabLine(srcLine, reverse) : srcLine; dstLines.push(dstLine); charCount += dstLine.length - srcLine.length; charIndex += (lineSpan.length + 1);} if (charCount == 0) return; selection.length += (charCount + 1); let content = dstLines.join('\n'); this.applyChange(content, selection);} tabLine(line, reverse) { if (!reverse) return '\t' + line; if (line.startsWith(' ')) return line.substring(2); if (line.startsWith('\t') || line.startsWith(' ')) return line.substring(1); return line;} normalizeLines() { for (const div of this.content.querySelectorAll('.ce-content > div')) { if (div.querySelector('br:first-child:last-child') != null) div.classList.remove('text-line');}} getRange(span) { let startRef = CodeEditor.getNodeRef(this.content, span.index); let endRef = CodeEditor.getNodeRef(this.content, span.endIndex); if ((startRef == null) || (endRef == null)) return null; let range = document.createRange(); if (startRef.offset == - 1) { if (startRef.before == true) range.setStartBefore(startRef.node); else range.setStartAfter(startRef.node);} else { range.setStart(startRef.node, startRef.offset);} if (endRef.offset == - 1) { if (endRef.before == true) range.setEndBefore(endRef.node); else range.setEndAfter(endRef.node);} else { range.setEnd(endRef.node, endRef.offset);} return range;} getMatchCount(content, subValue) { if (Utils.isNullOrEmpty(content) || Utils.isNullOrEmpty(subValue)) return 0; return content.split(subValue).length - 1;}}; class CompleteList extends HtmlControl { rows; selectedRow; prefix; owner; originalSelection; constructor(element) { super(element); this.rows = [...this.table.rows]; this.prefix = this.dataset.prefix; this.tableBody.onclick = e => this.OnListClick(e); this.tableBody.onmouseover = e => this.OnMouseOver(e); this.tableBody.onmouseout = e => this.OnMouseOut(e); if (this.insightWindow) this.insightWindow.onclick = e => this.OnInsightClick(e);} get insightWindow() { return this.listContainer.previousElementSibling; } get insightItem() { return this.insightWindow ? this.insightWindow.querySelector('.InsightItem:not(.d-none)') : null; } get prevButton() { return this.insightItem ? this.insightItem.querySelector('.ii-navbar > [data-dir="-1"]') : null; } get nextButton() { return this.insightItem ? this.insightItem.querySelector('.ii-navbar > [data-dir="1"]') : null; } get listContainer() { return this.element.lastElementChild; } get table() { return this.listContainer.firstElementChild; } get tableBody() { return this.table.tBodies[0]; } get visibleRows() { return this.tableBody.children; } get selectedRow() { return this.tableBody.querySelector('tr.selected'); } get firstRow() { return this.tableBody.firstElementChild; } get lastRow() { return this.tableBody.lastElementChild; } get startOffset() { return parseInt(this.dataset.start); } get selectionLength() { return this.prefix.length; } get completionChars() { return this.dataset.chars; } get selectedSpan() { return new Span(this.startOffset, this.selectionLength); } get rowHeight() { return this.firstRow ? this.firstRow.offsetHeight : 0; } get rowsPerPage() { return this.rowHeight ? Math.round(this.element.offsetHeight / this.rowHeight) : 0; } get selectedRowIndex() { for (let rowIndex = 0; rowIndex < this.visibleRows.length; rowIndex++) { if (this.visibleRows[rowIndex].matches('.selected')) return rowIndex;} return 0;} initialize(owner, completeChars) { this.owner = owner; this.prefix = this.prefix + completeChars; this.originalSelection = owner.selectedSpan; if (!Utils.isNullOrEmpty(this.prefix)) this.showMatchingRows();} *getMatchingRows() { let searchText = this.prefix.toLowerCase(); for (const row of this.rows) { let value = row.dataset.value.toLowerCase(); if (value.includes(searchText)) yield row;}} getMatchingRow() { let searchText = this.prefix.toLowerCase(); for (const row of this.rows) { let value = row.dataset.value.toLowerCase(); if (value == searchText) return row;} for (const row of this.rows) { let value = row.dataset.value.toLowerCase(); if (value.startsWith(searchText)) return row;} return null;} showMatchingRows() { if (Utils.isNullOrEmpty(this.prefix)) { this.tableBody.replaceChildren(...this.rows); if (this.selectedRow) { this.selectedRow.classList.remove('selected'); this.selectedRow = null;}} else { this.tableBody.replaceChildren(...this.getMatchingRows()); if (this.tableBody.children.length == 0) { this.close();} else { let rowToSelect = this.getMatchingRow(); this.selectRow(rowToSelect ? rowToSelect : this.firstRow);}} this.moveToCaretPosition();} selectRow(row) { if (this.selectedRow) Utils.setClass(this.selectedRow, 'selected', false); Utils.setClass(row, 'selected', true); this.selectedRow = row; if (!this.rowIsVisible(row)) row.scrollIntoView({ block: 'center' }); setTimeout(() => this.showPopup(row), 25);} rowIsVisible(row) { let rowBounds = row.getBoundingClientRect(); let ctlBounds = this.element.getBoundingClientRect(); return (rowBounds.top >= ctlBounds.top) && (rowBounds.bottom <= ctlBounds.bottom);} close() { this.owner.hideCompleteList();} showPopup(row) { let rowBounds = row.getBoundingClientRect(); let listBounds = this.listContainer.getBoundingClientRect(); let point = new Point(listBounds.right + 8, rowBounds.top - 8); this.layout.showTooltip(point, row.dataset.tooltip, this.controlType);} moveToCaretPosition() { let point = this.owner.completeWindowLocation; this.element.parentElement.style.left = `${point.x}px`; this.element.parentElement.style.top = `${point.y}px`;} OnMouseOver(e) { if (e.target.tagName == 'TD') this.showPopup(e.target.parentElement);} OnMouseOut(e) { this.layout.hidePopup(this.controlType);} OnListClick(e) { let row = e.target.closest('.CompleteList tr'); let value = row.dataset.value; this.owner.content.focus(); this.owner.selectedSpan = this.originalSelection; this.owner.insertText(value, this.selectedSpan, parseInt(row.dataset.offset)); e.stopPropagation(); this.close();} OnInsightClick(e) { if (!e.target.matches('span[data-dir]')) return; let dir = parseInt(e.target.dataset.dir); if (dir == 0) return; let src = e.target.closest('.InsightItem'); let dst = dir == -1 ? src.previousElementSibling : src.nextElementSibling; Utils.setClass(src, 'd-none', true); Utils.setClass(dst, 'd-none', false);} OnKeyDown(e) { if (Utils.isModifierKey(e)) return; switch (Utils.getKeyID(e)) { case 'Escape': e.stopPropagation(); this.close(); return; case 'ArrowLeft': case 'ArrowRight': case 'Home': case 'End': this.close(); return; case 'ArrowDown': let nextRow = this.selectedRow ? this.selectedRow.nextElementSibling : this.firstRow; if (nextRow) this.selectRow(nextRow); e.preventDefault(); return; case 'ArrowUp': let prevRow = this.selectedRow ? this.selectedRow.previousElementSibling : null; if (prevRow) this.selectRow(prevRow); e.preventDefault(); return; case 'Enter': if (this.selectedRow) { this.owner.insertText(this.selectedRow.dataset.value, this.selectedSpan, parseInt(this.selectedRow.dataset.offset)); e.stopPropagation(); e.preventDefault();} this.close(); return; case 'Tab': if (this.selectedRow) { this.owner.insertText(this.selectedRow.dataset.value, this.selectedSpan, parseInt(this.selectedRow.dataset.offset)); e.stopPropagation(); e.preventDefault(); this.close(); return;} case 'Control + ArrowLeft': if (this.prevButton) this.prevButton.click(); e.preventDefault(); return; case 'Control + ArrowRight': if (this.nextButton) this.nextButton.click(); e.preventDefault(); return; case 'PageUp': let prevRowIndex = Math.max(0, this.selectedRowIndex - this.rowsPerPage); this.selectRow(this.visibleRows[prevRowIndex]); e.preventDefault(); return; case 'PageDown': let nextRowIndex = Math.min(this.visibleRows.length - 1, this.selectedRowIndex + this.rowsPerPage); this.selectRow(this.visibleRows[nextRowIndex]); e.preventDefault(); return;} if (e.altKey || e.ctrlKey) { this.close(); return; } if (e.shiftKey && (e.key.length > 1)) { this.close(); return; } if ((e.key.length == 1) && this.completionChars.includes(e.key)) { if (this.selectedRow) { this.owner.insertText(this.selectedRow.dataset.value, this.selectedSpan, parseInt(this.selectedRow.dataset.offset), e);} this.close();}} OnInput(e) { switch (e.inputType) { case 'insertText': if (e.data.length != 1) { this.close();} else if (Utils.isNullOrEmpty(this.prefix) && Utils.isDigit(e.data)) { this.close();} else { this.prefix = this.prefix + e.data; this.showMatchingRows();} break; case 'deleteContentBackward': if (Utils.isNullOrEmpty(this.prefix)) { this.close();} else { this.prefix = this.prefix.substring(0, this.prefix.length - 1); if (Utils.isNullOrEmpty(this.prefix)) this.close(); else this.showMatchingRows();} break; default: this.close(); break;}}} ; class ContextMenu extends HtmlControl { onExecute; menuOwner; static loadedEvent = 'ContextMenu:Loaded'; static closedEvent = 'ContextMenu:Closed'; static executeEvent = 'ContextMenu:Execute'; constructor(element) { super(element); element.onkeydown = e => this.OnKeyDown(e); if (this.isRoot) { element.onclick = e => this.OnClick(e); element.onmouseover = e => this.OnMouseEnter(e); element.onkeyup = e => this.OnKeyUp(e); element.oncontextmenu = e => false; element.onmousedown = e => false; this.addListener('focusout', e => this.OnFocusOut(e));}} get table() { return this.element.firstElementChild; } get tableBody() { return this.table.tBodies[0]; } get rows() { return (this.table == null) ? [] : this.table.rows; } get firstRow() { return this.rows.length == 0 ? null : this.rows[0]; } get firstItem() { return this.firstRow ? new ContextMenuItem(this.firstRow) : null; } get lastRow() { return this.rows.length == 0 ? null : this.rows[this.rows.length - 1]; } get focusedRow() { return this.findRow('tr:focus'); } get focusedItem() { return this.focusedRow ? new ContextMenuItem(this.focusedRow) : null; } get parentMenuRow() { return this.isRoot ? null : this.element.parentElement.closest('tr'); } get parentMenuElement() { return this.element.parentElement.closest('.context-menu'); } get parentMenu() { return (this.parentMenuElement == null) ? null : HtmlControl.get(this.parentMenuElement); } get rootMenu() { return this.isRoot ? this : this.parentMenu.rootMenu; } get isEmpty() { return this.rows.length == 0; } get isRoot() { return this.parentMenu == null; } get linkName() { return this.dataset.link ? this.dataset.link : null; } get disabled() { return this.isRoot ? this.hasClass('disabled') : this.parentMenu.disabled; } set disabled(value) { Utils.setClass(this.element, 'disabled', value); } get isOpen() { return !this.element.matches('.d-none'); } get eventSource() { return this.menuOwner ?? this; } *getItems() { for (const row of this.rows) yield new ContextMenuItem(row);} *getFlyouts() { for (const item of this.getItems()) { if (item.isFlyout) yield item;}} *getCommands(recursive) { for (const item of this.getItems()) { if (item.isCommand) yield item; if (item.isFlyout && recursive) { for (const childItem of item.childMenu.getCommands(recursive)) { yield childItem;}}}} get firstSelectableRow() { for (const row of this.rows) { if (this.canSelectRow(row)) return row;} return null;} get lastSelectableRow() { let ret = null; for (const row of this.rows) { if (this.canSelectRow(row)) ret = row;} return ret;} get nextRow() { if (this.focusedRow == null) return this.firstSelectableRow; let ret = this.focusedRow.nextElementSibling; while (ret != null && !this.canSelectRow(ret)) ret = ret.nextElementSibling return ret;} get prevRow() { if (this.focusedRow == null) return this.lastSelectableRow; let ret = this.focusedRow.previousElementSibling; while (ret != null && !this.canSelectRow(ret)) ret = ret.previousElementSibling return ret;} canSelectRow(row) { return row.matches('.cm-item:not(.d-none):not(.disabled), .cm-flyout')} findRow(selector) { for (const row of this.rows) { if (row.matches(selector)) return row;} return null;} disableAll() { for (const menuItem of this.getCommands(true)) menuItem.isDisabled = true;} enableAll() { for (const menuItem of this.getCommands(true)) menuItem.isDisabled = false;} setCommandState(command, enabled) { let menuItem = this.getItem(command); if (menuItem) menuItem.isDisabled = !enabled;} setCommandVisible(command, visible) { let menuItem = this.getItem(command); if (menuItem) { Utils.setClass(menuItem.row, 'd-none', !visible); Utils.setBooleanAttribute(menuItem.row, 'aria-hidden', !visible); this.setCommandState(command, visible);}} getItem(command) { for (const item of this.getCommands(true)) { if (item.shortName == command || item.command == command) return item;} return null;} getCommand(e) { let fullKey = ContextMenu.toWindowsKey(e); if (Utils.isNullOrEmpty(fullKey)) return; for (const item of this.getCommands(true)) { if (item.shortcutKey == fullKey) return item;} return null;} isCommandEnabled(command) { let menuItem = this.getItem(command); return menuItem && !menuItem.isDisabled;} clickItem(command) { let menuItem = this.getItem(command); if (menuItem && !menuItem.isDisabled) menuItem.row.click();} clear() { while (this.element.firstChild) this.element.removeChild(this.element.firstChild);} computePosition(x, y) { let bounds = this.isRoot ? this.formBounds : this.parentMenu.clientBounds; return new Point(x - bounds.left, y - bounds.top);} adjustBounds() { let parentBounds = this.isRoot ? new Rectangle(0, 0, window.innerWidth, window.innerHeight) : this.parentMenu.clientBounds; let bounds = new Rectangle(0, 0, window.innerWidth, window.innerHeight) let minY = bounds.top, maxY = bounds.bottom; let minX = bounds.left, maxX = bounds.right; let menuBounds = this.element.getBoundingClientRect(); if (menuBounds.bottom > maxY) { this.element.style.top = (maxY - menuBounds.height - parentBounds.top) + 'px';} else if (menuBounds.top < minY) { this.element.style.top = '10px';} if (menuBounds.right > maxX) { this.element.style.left = (maxX - menuBounds.width - parentBounds.left) + 'px';} else if (menuBounds.left < minX) { this.element.style.left = '10px';}} showAt(x, y) { let pos = this.computePosition(x, y); this.element.style.left = pos.x + 'px'; this.element.style.top = pos.y + 'px'; this.element.style.visibility = 'hidden'; Utils.setClass(this.element, 'd-none', false); this.adjustBounds(); this.element.style.visibility = 'visible'; this.element.focus({ preventScroll: true }); Utils.setBooleanAttribute(this.element, "aria-expanded", true);} hide(refocus = true) { if (!this.isOpen) return; this.closeFlyouts(); Utils.setClass(this.element, 'd-none', true); Utils.setBooleanAttribute(this.element, "aria-expanded", false); if (!this.isRoot) return this.parentMenuRow.focus(); this.eventSource.bubbleEvent(ContextMenu.closedEvent, { refocus: refocus });} closeFlyouts() { for (const flyout of this.getFlyouts()) flyout.close();} addSeparator() { let row = this.table.insertRow(); row.role = 'separator'; row.innerHTML = '
';} addJsCommand(id, name, icon, title, keys = '') { let row = this.table.insertRow(); row.id = id; row.className = 'cm-item'; row.dataset.mode = 'js'; row.dataset.command = id; row.role = 'menuitem'; row.innerHTML = `${icon}${name}${keys}`;} addOption(name, title, keys, checked) { let row = this.table.insertRow(); let attr = checked ? ' checked' : ''; row.className = 'cm-item'; row.dataset.mode = 'js'; row.dataset.command = name; row.role = 'menuitemcheckbox'; row.innerHTML = `${name}${keys}`;} setCheckState(name, checked) { let menuItem = this.getItem(name); menuItem.isChecked = checked;} lastRequestHash; loadNodeCommands(selectedIds, controlTypes, filterMethod = null, oncomplete = null) { if (selectedIds.length == 0) { this.clear(); return; } let ids = selectedIds.length <= 1000 ? selectedIds : selectedIds.slice(0, 1000); let postData = { itemIds: ids, controlTypes: controlTypes, filterMethod: filterMethod }; this.loadFrom('/Shared/RenderMenu', postData, oncomplete);} loadFrom(url, postData, oncomplete) { let requestHash = url + JSON.stringify(postData); this.lastRequestHash = requestHash; HTTP.post(url, postData, data => { if (requestHash == this.lastRequestHash) { this.replaceInner(data); this.eventSource.bubbleEvent(ContextMenu.loadedEvent); if (this.isRoot && this.isOpen) this.firstRow?.focus(); if (oncomplete) oncomplete();}});} replaceInner(data) { this.element.innerHTML = data; HtmlControl.initBranch(this.element); this.adjustBounds();} OnFocusOut(e) { if (e.relatedTarget && this.element.contains(e.relatedTarget)) return; this.hide(false);} OnClick(e) { let row = e.target.closest('tr'); if (row == null) return; e.stopPropagation(); let item = new ContextMenuItem(row); if (item.isDisabled) return; row.focus(); if (item.isFlyout) return; if (item.isOption) item.isChecked = !item.isChecked; this.hide(); this.eventSource.bubbleEvent(ContextMenu.executeEvent, item); if (item.mode == 'js') this.eventSource.raiseEvent(item.command, item);} OnMouseEnter(e) { let row = e.target.closest('tr'); if (row) this.startHoverTimer(row);} OnKeyUp(e) { e.stopPropagation();} OnKeyDown(e) { e.stopPropagation(); e.preventDefault(); switch (Utils.getKeyID(e)) { case 'Escape': return this.hide(); case 'ArrowUp': return this.OnArrowUp(); case 'ArrowDown': return this.OnArrowDown(); case 'ArrowLeft': return this.OnArrowLeft(); case 'ArrowRight': return this.OnArrowRight(); case 'Home': return this.OnHome(); case 'End': return this.OnEnd(); case 'Enter': return this.OnEnter(); case ' ': return this.OnEnter();} if (Utils.isCharacterKey(e)) { let item = this.focusedItem ? this.focusedItem.nextItem : this.firstItem; while (item != null) { if (this.canSelectRow(item.row) && item.rawName.toLowerCase().startsWith(e.key.toLowerCase())) return item.row.focus(); item = item.nextItem;}}} OnEnter(e) { if (this.focusedRow == null) return; let item = new ContextMenuItem(this.focusedRow); if (item.isFlyout) return this.OnArrowRight(); if (item.isCommand) return item.row.click(); return false;} OnArrowUp() { this.closeFlyouts(); this.prevRow?.focus();} OnArrowDown() { this.closeFlyouts(); this.nextRow?.focus();} OnArrowLeft() { if (!this.isRoot) { this.parentMenuRow.focus(); this.hide();} else { this.OnArrowUp();}} OnArrowRight() { if (this.focusedRow == null) return; let item = new ContextMenuItem(this.focusedRow); if (item.isFlyout) { item.open(); item.childMenu.firstRow.focus();} else { this.OnArrowDown();}} OnHome() { this.firstSelectableRow?.focus();} OnEnd() { this.lastSelectableRow?.focus();} hoverTimer; startHoverTimer(row) { this.stopHoverTimer(); this.hoverTimer = setTimeout(() => this.applyHover(row), 100);} stopHoverTimer() { if (this.hoverTimer) { clearTimeout(this.hoverTimer); this.hoverTimer = null;}} applyHover(row) { let item = new ContextMenuItem(row); if (item.isFlyout) { if (!item.childMenu.isOpen) { item.menu.closeFlyouts(); if (!item.isDisabled) item.open();}} else { item.menu.closeFlyouts(); this.element.focus();}} static toWindowsKey(e) { if (e.key == undefined) return ''; var key = (e.key.length == 1) ? e.key.toUpperCase() : e.key; switch (key) { case 'Delete': key = 'Del'; break; case 'Insert': key = 'Ins'; break; case 'ArrowLeft': key = 'Left'; break; case 'ArrowRight': key = 'Right'; break; case 'ArrowUp': key = 'Up'; break; case 'ArrowDown': key = 'Down'; break;} var parts = []; if (e.altKey) parts.push('Alt'); if (e.ctrlKey) parts.push('Ctrl'); if (e.shiftKey) parts.push('Shift'); parts.push(key); return parts.join('+');}} class ContextMenuItem { row; constructor(row) { this.row = row;} get mode() { return this.row.dataset.mode; } //can be 'auto', 'modal', or 'js'. get isCommand() { return this.row.matches('.cm-item'); } get isFlyout() { return this.row.matches('.cm-flyout'); } get childMenuElement() { return this.isFlyout ? this.row.firstElementChild.firstElementChild : null; } get childMenu() { return HtmlControl.get(this.childMenuElement); } get menu() { return HtmlControl.get(this.row.parentElement); } get command() { return this.row.dataset.command; } get rawName() { return this.row.cells[1].textContent; } get displayName() { return this.rawName.endsWith('...') ? this.rawName.substring(0, this.rawName.length - 3) : this.rawName; } get shortcutKey() { return this.row.cells[2].textContent; } get shortName() { return this.command.split(/[.+]/).pop(); } get isSerializable() { return this.row.hasAttribute('data-serializable'); } get isOption() { return this.input != null; } get input() { return this.row.querySelector('input'); } get isDisabled() { return this.row.matches('.disabled') || this.menu.disabled; } set isDisabled(value) { Utils.setClass(this.row, 'disabled', value); Utils.setBooleanAttribute(this.row, 'aria-disabled', value);} get isChecked() { return this.input.checked; } set isChecked(value) { if (value) this.input.setAttribute('checked', value); else this.input.removeAttribute('checked');} get linkName() { return this.menu.linkName; } get parentCommand() { return new ContextMenuItem(this.row.parentElement.closest('tr[data-command]')); } get nextItem() { return this.row.nextElementSibling == null ? null : new ContextMenuItem(this.row.nextElementSibling);} open() { let rowBounds = this.row.getBoundingClientRect(); this.childMenu.showAt(rowBounds.right, rowBounds.top);} close() { this.childMenu.hide(); }} class DropButton extends HtmlControl { closeTime; constructor(element) { super(element); this.button.onclick = e => this.OnClick(e); this.content.addEventListener('focusout', e => this.OnFocusOut(e)); this.closeTime = performance.now(); this.content.onkeydown = e => this.OnKeyDown(e);} get button() { return this.element.firstElementChild; } get content() { return this.element.lastElementChild; } get isOpen() { return this.hasClass('open'); } get timeSinceClose() { return performance.now() - this.closeTime; } close() { this.removeClass('open'); this.content.tabIndex = null;} OnClick(e) { let button = e.target.closest('button'); if (!Utils.isButtonEnabled(button)) return; if (this.timeSinceClose < 300) return; this.focusTimer = null; this.content.style.visibility = 'hidden'; this.content.tabIndex = 0; Utils.toggleClass(this.element, 'open'); this.content.tabIndex = this.element.classList.contains('open') ? 0 : null; if (this.isOpen) this.setPosition(); e.stopPropagation();} lastFileInputClick; OnFocusOut(e) { let now = performance.now(); if (e.relatedTarget) { if (this.content.contains(e.relatedTarget)) { return;} else if (this.element.contains(e.relatedTarget)) { this.lastFileInputClick = now; return;}} if (this.lastFileInputClick && (now - this.lastFileInputClick) < 300) return; this.close(); this.closeTime = now;} OnKeyDown(e) { if (Utils.getKeyID(e) == 'Escape' && this.isOpen) { this.close(); e.stopPropagation();}} setPosition() { let formBounds = this.formBounds; let contentRect = this.content.getBoundingClientRect(); let limit = formBounds.right - 16; if (contentRect.right > limit) { let diff = contentRect.right - limit; let left = contentRect.left - diff; this.content.style.left = `${left - formBounds.left}px`;} this.content.style.visibility = 'visible'; this.content.focus();}}; class HtmlTree extends HtmlControl { static selectionChangedEvent = 'HtmlTree:SelectionChanged'; static loadChildrenEvent = 'HtmlTree:LoadChildren'; constructor(element) { super(element) this.element.onclick = e => this.OnClick(e); this.element.ondblclick = e => this.OnDoubleClick(e); this.element.onkeydown = e => this.OnKeyDown(e);} get focusedElement() { return this.element.querySelector('li.focused'); } get focusedNode() { return this.focusedElement ? new HtmlTreeNode(this.focusedElement) : null; } get firstLeaf() { return HtmlTreeNode.fromElement(this.element.querySelector('li')); } get rootUL() { return this.element.firstElementChild; } get rootNodes() { return [...this.getRootNodes()]; } *getRootNodes() { for (const li of this.rootUL.children) yield new HtmlTreeNode(li);} clear() { this.rootUL.innerHTML = '';} clearFocus() { if (this.focusedNode) this.focusedNode.isFocused = false;} selectRoot() { this.rootNodes[0].setFocus(false);} OnClick(e) { if (!e.target.matches('li, li > span, li > span *, li > div, li > div *')) return; if (e.offsetX < 0) return this.OnDoubleClick(e); let node = new HtmlTreeNode(e.target); node.setFocus();} OnDoubleClick(e) { if (!e.target.matches('li, li > span, li > div, li > div *')) return; let node = new HtmlTreeNode(e.target); if (!node.isExpanded && node.needsLoad) this.bubbleEvent(HtmlTree.loadChildrenEvent, { node: node }); if (node.isExpandable) node.isExpanded = !node.isExpanded;} OnKeyDown(e) { let node = this.focusedNode; if (node == null) { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(Utils.getKeyID(e))) this.selectRoot();} else { if (node.handleNavKey(e)) e.preventDefault();}}} class HtmlTreeNode { element; constructor(element) { this.element = element.closest('li');} get dataset() { return this.element.dataset; } get nodeId() { return this.dataset.id; } get tree() { return HtmlControl.get(this.element); } get labelElement() { let firstChild = this.element.firstElementChild; if (firstChild.matches('div')) return firstChild.querySelector('span'); else if (firstChild.matches('.material-icons')) return firstChild.nextElementSibling; else return firstChild;} get name() { return this.labelElement.innerText; } get prevNode() { return HtmlTreeNode.fromElement(this.element.previousElementSibling); } get nextNode() { return HtmlTreeNode.fromElement(this.element.nextElementSibling); } get parentNode() { return this.hasParent ? new HtmlTreeNode(this.element.parentElement) : null; } get parentUL() { return this.element.parentElement; } get hasParent() { return this.parentUL.parentElement.tagName == 'LI'; } get isRoot() { return this.parentUL.parentElement.tagName != 'LI'; } get isFocused() { return this.element.matches('.focused'); } set isFocused(value) { Utils.setClass(this.element, 'focused', value); this.element.setAttribute('aria-selected', value.toString());} get isLeaf() { return this.element.children.length == 0;} get isExpandable() { return this.element.matches('.expandable'); } get isExpanded() { return this.element.matches('.open'); } set isExpanded(value) { Utils.setClass(this.element, 'open', value); this.element.setAttribute('aria-expanded', value.toString());} get childUL() { return this.element.lastElementChild; } get childCount() { return this.childUL.children.length; } get hasChildren() { return this.isExpandable && (this.childCount > 0); } get hasVisibleChildren() { return this.hasChildren && this.isExpanded; } get firstChild() { return (!this.hasChildren) ? null : new HtmlTreeNode(this.childUL.firstElementChild); } get lastChild() { return (!this.hasChildren) ? null : new HtmlTreeNode(this.childUL.lastElementChild); } get firstVisibleChild() { return this.hasVisibleChildren ? this.firstChild : null; } get lastVisibleChild() { return this.hasVisibleChildren ? this.lastChild : null; } get downTarget() { return this.firstVisibleChild ? this.firstVisibleChild : this.nextNode; } get isVisible() { let nodeBounds = this.element.getBoundingClientRect(), controlBounds = this.tree.element.getBoundingClientRect(); return (nodeBounds.top >= controlBounds.top) && (nodeBounds.top <= controlBounds.bottom);} get childNodes() { return [...this.getChildNodes()]; } get path() { return Utils.isNullOrEmpty(this.rawPath) ? '/' : this.rawPath; } get rawPath() { return this.hasParent ? `${this.parentNode.rawPath}/${this.name}` : ''; } get needsLoad() { return this.childUL.hasAttribute('data-load'); } set needsLoad(value) { if (value) this.childUL.setAttribute('data-load', null); else this.childUL.removeAttribute('data-load'); } loadChildren(URL, postData, oncomplete = null) { HTTP.post(URL, postData, data => { this.childUL.innerHTML = data; this.needsLoad = false; if (this.childCount == 0) this.element.classList.remove('expandable', 'open'); if (oncomplete) oncomplete();});} *getChildNodes() { for (const li of this.childUL.children) yield new HtmlTreeNode(li);} ensureExpanded() { if (this.isExpandable && !this.isExpanded) this.isExpanded = true; if (this.parentNode) this.parentNode.ensureExpanded();} ensureVisible() { if (this.parentNode) this.parentNode.ensureExpanded(); if (!this.isVisible) this.element.scrollIntoView({ block: 'center' });} setFocus(byUser = true) { if (this.isFocused) return; this.tree.clearFocus(); this.isFocused = true; this.ensureVisible(); this.tree.bubbleEvent(HtmlTree.selectionChangedEvent, { byUser: byUser });} handleNavKey(e) { switch (Utils.getKeyID(e)) { case 'ArrowUp': this.OnArrowUp(); return true; case 'ArrowDown': this.OnArrowDown(); return true; case 'ArrowLeft': this.OnArrowLeft(); return true; case 'ArrowRight': this.OnArrowRight(); return true; case 'Home': this.OnHomeKey(); return true; case 'End': this.OnEndKey(); return true; default: return false;}} OnArrowUp() { if (this.prevNode == null) { if (this.parentNode) { this.parentNode.setFocus(); } return;} let ret = this.prevNode; while (ret.hasVisibleChildren) ret = ret.lastVisibleChild; ret.setFocus();} OnArrowDown() { if (this.downTarget != null) { this.downTarget.setFocus(); return; } let ancestor = this; while (ancestor != null) { if (ancestor.nextNode != null) { ancestor.nextNode.setFocus(); return; } ancestor = ancestor.parentNode;}} OnArrowLeft() { switch (true) { case this.isExpanded: this.isExpanded = false; break; case (this.parentNode != null): this.parentNode.setFocus(); break;}} OnArrowRight() { switch (true) { case !this.isExpandable: if (this.nextNode) this.nextNode.setFocus(); break; case !this.isExpanded: if (!this.isExpanded && this.needsLoad) this.tree.bubbleEvent(HtmlTree.loadChildrenEvent, { node: this }); this.isExpanded = true; break; case (this.firstChild): this.firstChild.setFocus(); break;}} OnHomeKey() { if ((this.parentNode == null) || (this.parentNode.firstVisibleChild == this)) return; this.parentNode.firstVisibleChild.setFocus();} OnEndKey() { if ((this.parentNode == null) || (this.parentNode.lastVisibleChild == this)) return; this.parentNode.lastVisibleChild.setFocus();} static fromElement(element) { return element ? new HtmlTreeNode(element) : null; }}; class ModalBox extends HtmlControl { options; isDirty; static focusablePrimary = 'input[type="text"]:not(:disabled), select:not(:disabled), textarea:not(:disabled)'; static focusableSecondary = 'button:not(:disabled), a:not(:disabled), [tabindex]:not([tabindex="-1"]):not(:disabled), input[type="checkbox"]'; static focusableItem = ModalBox.focusablePrimary + ', ' + ModalBox.focusableSecondary; constructor(element) { super(element); element.onchange = e => this.OnChange(e); this.okButton.onclick = e => this.onOkButton(e); this.cancelButton.onclick = e => this.onCancelButton(e); this.content.onkeydown = e => this.onKeyDown(e); this.header.onpointerdown = e => this.OnPtrDown(e); this.header.onpointerup = e => this.OnPtrUp(e); this.header.onpointermove = e => this.OnPtrMove(e); this.addListener(HtmlControl.formEditedEvent, e => this.OnFormEdited(e))} get content() { return this.element.firstElementChild; } get dialogBounds() { return this.content.getBoundingClientRect(); } get header() { return this.content.firstElementChild; } get captionElement() { return this.header.firstElementChild; } get toolbar() { return this.header.children.length == 3 ? this.header.children[1] : null; } get headerRight() { return this.header.lastElementChild; } get okButton() { return this.cancelButton.previousElementSibling; } get cancelButton() { return this.headerRight.lastElementChild; } get childContent() { return this.content.lastElementChild; } get childElement() { return this.childContent.firstElementChild; } get hasChildControl() { return this.childElement != null && this.childElement.hasAttribute('data-control'); } get childControl() { return this.hasChildControl ? HtmlControl.get(this.childElement) : null; } get isUpload() { return this.childContent.lastElementChild.matches('input[type="file"]'); } get fileInput() { return this.isUpload ? this.childContent.lastElementChild : null; } get firstFocusableInput() { return this.childElement.querySelector(ModalBox.focusablePrimary); } get firstFocusableControl() { return this.childElement.querySelector(ModalBox.focusableSecondary); } get firstFocusableButton() { return this.okButton.disabled ? (this.cancelButton.disabled ? null : this.cancelButton) : this.okButton; } get initialFocusedItem() { return this.firstFocusableInput ?? this.firstFocusableControl ?? this.firstFocusableButton; } get focusableChildren() { return this.content.querySelectorAll(ModalBox.focusableItem); } get firstFocusableChild() { return this.focusableChildren.length == 0 ? null : this.focusableChildren[0]; } get lastFocusableChild() { return this.focusableChildren.length == 0 ? null : this.focusableChildren[this.focusableChildren.length - 1]; } get isOkEnabled() { if (this.options.requireEdit && !this.isDirty) return false; if (this.childControl?.formIsValid != null) return this.childControl.formIsValid return true;} get caption() { return this.captionElement.innerText; } set caption(value) { if (value.includes(': ')) { let parts = value.split(': '); this.captionElement.innerHTML = `${parts[0]}${parts[1]}`; let rightPart = this.captionElement.lastElementChild; let textRight = rightPart.offsetLeft + rightPart.offsetWidth, containerRight = this.captionElement.offsetLeft + this.captionElement.offsetWidth; if (textRight + 16 > containerRight) Utils.setClass(rightPart, 'd-none', true);} else { this.captionElement.innerText = value;}} initToolbar() { if (this.toolbar) return; this.header.insertBefore(document.createElement('DIV'), this.headerRight); Utils.setClass(this.header, 'triple', true);} setSize(width, height) { this.setWidth(width); this.setHeight(height);} setWidth(width) { this.content.style.width = width;} setHeight(height) { this.childContent.style.height = height;} setDirtyState(dirty) { this.isDirty = dirty; this.setCommandStates();} setCommandStates() { if (this.isOkEnabled) { this.okButton.disabled = false;} else { this.okButton.disabled = true;}} showDialog(options) { this.layout.hidePopups(); this.options = options; this.setCommandStates(); if (options.caption) this.caption = options.caption; if (options.okText != null) this.okButton.innerText = options.okText; if (options.cancelText != null) this.cancelButton.innerText = options.cancelText; if (options.hideCancel == true) this.cancelButton.hidden = true; if (options.hideOk == true) this.okButton.hidden = true; this.setSize(options.width ? options.width : null, options.height ? options.height : null); this.content.style.visibility = "hidden"; this.element.style.display = "block"; if (options.content != null) { this.setContent(options.content, options);} else if (options.postData) { this.loadContentsPost(options);} else { HtmlControl.load(this.childContent, options.URL, () => this.afterLoad(options));}} loadContentsPost(options) { HTTP.postJSON(options.URL, options.postData, { error: (xhr, errorInfo) => { if (options.onError) options.onError(xhr, errorInfo); this.hide(); HTTP.handleError(xhr, errorInfo);}, success: (data, responseText, xhr) => { this.setContent(data, options);}});} setContent(content, options) { this.childContent.innerHTML = content; if (this.isUpload) this.childContent.onchange = e => this.OnChange(e); if (this.childContent.firstElementChild) HtmlControl.initControl(this.childContent.firstElementChild); this.afterLoad(options);} afterLoad(options) { this.setCommandStates(); if (options.caption == null && this.childControl?.typeDisplayName != null) { this.caption = this.childControl.typeDisplayName;} this.moveToCenter(); this.content.style.visibility = "visible"; if (!this.childContent.matches(':focus-within')) { let target = this.initialFocusedItem ?? this.content; setTimeout(() => { if (!this.childContent.matches(':focus-within')) { target.focus(); if (target.matches('input')) target.select(); if (options.onLoad) options.onLoad(this.childElement);}}, 50);} else { if (options.onLoad) options.onLoad(this.childElement);}} moveTo(x, y) { this.content.style.left = `${x}px`; this.content.style.top = `${y}px`;} moveToCenter() { this.fitToWindow(); let bodyBounds = Rectangle.fromDOMRect(this.layout.element.getBoundingClientRect()); let dialogBounds = Rectangle.fromDOMRect(this.content.getBoundingClientRect()); let left = bodyBounds.centerX - (dialogBounds.width / 2); let top = bodyBounds.centerY - (dialogBounds.height / 2); this.moveTo(left, top);} fitToWindow() { let bodyBounds = Rectangle.fromDOMRect(this.layout.element.getBoundingClientRect()); let dialogBounds = Rectangle.fromDOMRect(this.content.getBoundingClientRect()); let headerBounds = Rectangle.fromDOMRect(this.header.getBoundingClientRect()); let width = Math.min(dialogBounds.width, bodyBounds.width - 16); let height = Math.min(dialogBounds.height, bodyBounds.height - headerBounds.height - 16); if (width != dialogBounds.width || height != dialogBounds.height) this.setSize(`${width}px`, `${height}px`);} hide() { if (this.options && this.options.caller) this.options.caller.element.focus(); this.element.remove();} onTab(e) { if (e.ctrlKey || e.altKey) return; if (this.firstFocusableChild == null) { e.preventDefault();} else if (e.shiftKey && (document.activeElement == this.firstFocusableChild)) { this.lastFocusableChild.focus(); e.preventDefault();} else if (!e.shiftKey && (document.activeElement == this.lastFocusableChild)) { this.firstFocusableChild.focus(); e.preventDefault();}} onOkButton(e) { if (!this.isOkEnabled) return; this.layout.hidePopups(); if (this.options.onOk) this.options.onOk(this.childElement); this.hide();} onCancelButton(e) { this.layout.hidePopups(); if (this.options.onCancel) this.options.onCancel(this.childElement); this.hide();} onKeyDown(e) { switch (e.key) { case 'Enter': this.okButton.focus(); this.okButton.click(); e.cancelBubble = true; break; case 'Escape': this.cancelButton.click(); e.cancelBubble = true; break; case 'Tab': this.onTab(e); e.cancelBubble = true; break;}} OnPtrDown(e) { if (e.target.tagName == 'BUTTON') return; e.target.setPointerCapture(e.pointerId);} OnPtrUp(e) { e.target.releasePointerCapture(e.pointerId);} OnPtrMove(e) { if (e.target.hasPointerCapture(e.pointerId)) this.moveTo(this.content.offsetLeft + e.movementX, this.content.offsetTop + e.movementY);} OnFormEdited(e) { this.setDirtyState(true); e.cancelBubble = true;} OnChange(e) { if (this.isUpload) { this.okButton.disabled = (this.fileInput.files.length == 0);}}} class ModalOptions { caller; URL; postData; content; caption; okText; cancelText; onOk; onCancel; onLoad; onError; hideOk; hideCancel; requireEdit; width; height;} ; class ErrorBox extends ModalBox { constructor(element) { super(element);} get tabList() { return HtmlControl.get(this.find('[data-control="TabList"]')); } get activeTabButton() { return this.tabList.selectedButton; } get wrapButton() { return this.toolbar.querySelector('#wrap'); } get errorContent() { return this.content.querySelector('pre#error'); } get stackContent() { return this.content.querySelector('pre#stack'); } get wordWrap() { return this.wrapButton.matches('.active'); } set wordWrap(value) { Utils.setClass(this.wrapButton, 'active', value); Utils.setClass(this.element, 'nowrap', !value);} showErrorBox(message, options = null) { let endpoint = options?.endpoint ? `${options.endpoint}` : ''; let stackButton = options?.stack ? `` : ''; let stackContent = options?.stack ? `
${options.stack}
` : ''; let lines = [ ``, `
`, `
${message}
`, ` ${stackContent}`, `
` ]; this.showDialog({ content: lines.join('\n'), caption: 'Error', hideCancel: true, onOk: options?.onOk }); this.tabList.addListener(TabList.buttonClickedEvent, e => this.OnTabButtonClick(e)); this.content.focus(); this.initToolbar(); this.toolbar.appendChild(Snippets.createButton('wrap', 'wrap_text', 'Wrap text', true)); this.wordWrap = true; this.toolbar.onclick = e => this.OnButtonClick(e);} OnButtonClick(e) { switch (e.target.id) { case 'wrap': this.wordWrap = !this.wordWrap; break;}} OnTabButtonClick(e) { Utils.setClass(this.find(`pre[data-id="${this.activeTabButton.id}"]`), 'd-none', true); Utils.setClass(this.find(`pre[data-id="${e.detail.button.id}"]`), 'd-none', false); this.tabList.selectedButton = e.detail.button;}}; class InputBox extends ModalBox { requireValue = false; requireEdit = false; multiLine = false; hasChanged = false; constructor(element) { super(element); this.childContent.oninput = e => this.OnInput(e); this.childContent.onkeydown = e => this.OnKeyDown(e);} get input() { return this.childContent.querySelector('input, textarea'); } get needsValue() { return this.requireValue ? Utils.isNullOrEmpty(this.input.value) : false; } get needsEdit() { return this.requireEdit ? !this.hasChanged : false; } showInputBox(caption, label, value, onOk) { let content = this.getContent(label, value); let settings = { caption: caption, content: content, onOk: onOk }; if (this.multiLine) settings.width = '800px'; this.showDialog(settings); this.okButton.disabled = this.needsValue || this.needsEdit; this.input.select();} getContent(label, value) { if (this.multiLine) { return `
:
`;} else { return `
:
`;}} OnInput(e) { this.hasChanged = true; this.okButton.disabled = this.needsValue || this.needsEdit;} OnKeyDown(e) { if (this.multiLine && e.key == 'Enter') { e.stopPropagation(); `` }}}; class BrowseBox extends ModalBox { constructor(element) { super(element); this.addListener(TreeViewer.selectionChangedEvent, e => this.setCommandStates()); this.addListener('dblclick', e => this.OnDoubleClick(e)); Utils.setButtonState(this.okButton, false);} get treeViewer() { return HtmlControl.get(this.childElement); } get hasValidSelection() { return this.treeViewer?.focusedItem ? !this.treeViewer.focusedItem.isDisabled : false } setCommandStates() { this.okButton.disabled = !this.hasValidSelection;} OnDoubleClick(e) { if (e.target.matches('.tv-item > *') && this.hasValidSelection) this.okButton.click();}}; class ProgressBox extends ModalBox { updateTimer; autoUpdate; constructor(element) { super(element); this.childContent.innerHTML = ''; this.autoUpdate = true;} get progress() { return this.childContent.querySelector('progress'); } get progressText() { return this.childContent.firstElementChild.innerText; } set progressText(value) { this.childContent.firstElementChild.innerText = value; } get progressHTML() { return this.childContent.firstElementChild.innerHTML; } set progressHTML(value) { this.childContent.firstElementChild.innerHTML = value; } showProgress(caption, message, oncancel = null) { this.showDialog({ caption: caption, onCancel: oncancel, content: `

${message}

`, hideCancel: (oncancel == null), hideOk: true }); if (this.autoUpdate) this.startUpdateTimer();} httpPost(URL, postData, oncomplete = null) { HTTP.postJSON(URL, postData, { error: (xhr, errorInfo) => { this.hide(); HTTP.handleError(xhr, errorInfo);}, success: (data, xhr) => { this.hide(); if (oncomplete) oncomplete(data, xhr)}});} cancel() { HTTP.postJSON('/Resource/CancelProgress', { sessionId: this.layout.sessionId });} sendFiles(URL, postData, oncomplete = null) { HTTP.postFiles(URL, postData, { error: (xhr, errorInfo) => { this.hide(); HTTP.handleError(xhr, errorInfo);}, success: (data, xhr) => { this.hide(); if (oncomplete) oncomplete(data, xhr)}});} startUpdateTimer() { if (!this.autoUpdate) return; this.stopUpdateTimer(); this.updateTimer = setTimeout(() => this.doUpdate(), 1000);} stopUpdateTimer() { if (!this.updateTimer) return; clearTimeout(this.updateTimer); this.updateTimer = null;} hide() { this.stopUpdateTimer(); super.hide();} doUpdate() { HTTP.postJSON('/Resource/GetProgress', { sessionId: this.layout.sessionId }, { success: data => this.applyUpdate(data), error: () => this.startUpdateTimer()});} applyUpdate(data) { if (Utils.isNullOrEmpty(data)) return; this.cancelButton.hidden = !data.supportsCancelation; this.progress.max = data.total; if (data.total == 0) { this.progress.removeAttribute('value');} else { this.progress.value = data.completed;} if (data.total != 0) { this.updateProgressMessage(data.message, data.itemName, data.itemVerb);} else if (data.completed != 0) { this.updateProgressMessage_NoTotal(data.message, data.itemName, data.itemVerb, data.completed);} else if (!Utils.isNullOrEmpty(data.message)) { this.progressHTML = `
${data.message}
`;} this.startUpdateTimer();} updateProgressMessage(message, itemName, itemVerb) { let msg = Utils.isNullOrEmpty(message) ? ' ' : message; let progress = this.progress.max == 0 ? '' : `${this.progress.value.toLocaleString()} / ${this.progress.max.toLocaleString()} ${itemName} ${itemVerb}`; this.progressHTML = `
${msg}
${progress}
`;} updateProgressMessage_NoTotal(message, itemName, itemVerb, completed) { let msg = Utils.isNullOrEmpty(message) ? ' ' : message; let progress = `${completed.toLocaleString()} ${itemName} ${itemVerb}`; this.progressHTML = `
${msg}
${progress}
`;}}; class RepositoryPicker extends ModalBox { constructor(element) { super(element); this.addListener(ObjectList.selectionChangedEvent, e => this.setCommandStates()); this.addListener('dblclick', e => this.OnDoubleClick(e)); Utils.setButtonState(this.okButton, false);} get list() { return HtmlControl.get(this.childElement); } get focusedRow() { return this.list?.focusedRow; } get hasValidSelection() { return this.focusedRow && this.focusedRow.cells[3].innerText == 'Ok'; } showRepositoryPicker(page) { this.showDialog({ width: 'fit-content', height: '300px', caption: 'Change Repository', URL: '/Home/RenderRepositoryList', postData: {}, onLoad: () => this.selectRepository(this.repositoryId), onOk: childElement => { let list = HtmlControl.get(childElement); let repId = list.focusedRow.dataset.repositoryId; if (repId != this.repositoryId) window.location.href = Utils.mapPath(`/Shared/ChangeRepository/?rep=${repId}&page=${page}`);}});} selectRepository(repId) { for (const row of this.list.rows) { if (row.dataset.repositoryid == repId) return this.list.focusRow(row);}} setCommandStates() { this.okButton.disabled = !this.hasValidSelection;} OnDoubleClick(e) { if (e.target.matches('td') && this.hasValidSelection) this.okButton.click();}}; class TextBox extends ModalBox { constructor(element) { super(element);} showTextBox(caption, message) { let content = `
${message}
`; this.showDialog({ content: content, caption: caption, hideCancel: true }); this.content.focus();}}; class ObjectList extends HtmlControl { static selectionChangedEvent = 'ObjectList.SelectionChanged'; static rowDoubleClickEvent = 'ObjectList.rowDoubleClick'; static checkStateChangedEvent = 'ObjectList.CheckStateChanged'; constructor(element) { super(element); element.onkeydown = e => this.OnKeyDown(e); element.onkeyup = e => this.OnKeyUp(e); element.oncontextmenu = e => e.preventDefault(); element.onpointerdown = e => this.OnPointerDown(e); element.onpointerup = e => this.OnPointerUp(e); element.onpointermove = e => this.OnPointerMove(e); element.onclick = e => this.OnClick(e); element.onchange = e => this.OnChange(e); element.ondblclick = e => this.OnDoubleClick(e); this.addListener('focusin', e => this.OnFocusIn(e)); this.scrollContainer.onscroll = () => this.headerTable.style.transform = `translateX(-${this.scrollContainer.scrollLeft}px)`; if (this.focusedRow) { setTimeout(() => this.focusedRow.scrollIntoView({ block: 'center' }), 20);} else if (this.autoSelect && this.tableBody.firstElementChild) { this.tableBody.firstElementChild.classList.add('focused', 'selected');}} get listOptions() { return parseInt(this.dataset.options); } get multiSelect() { return (this.listOptions & ObjectList.ListOptions.MultiSelect) != 0; } get autoSelect() { return (this.listOptions & ObjectList.ListOptions.AutoSelect) != 0; } get showCheckboxes() { return (this.listOptions & ObjectList.ListOptions.ShowCheckboxes) != 0; } get isScrollable() { return this.element.children.length == 2; } get isDisabled() { return this.element.matches('.disabled'); } get headerTable() { return this.element.firstElementChild; } get headerRow() { return this.headerTable == null ? null : this.headerTable.rows[this.headerTable.rows.length - 1]; } get headerColGroup() { return this.headerTable.firstElementChild; } get firstHeaderCell() { return this.headerRow?.cells[0]; } get hasRowNumbers() { return this.hasRowHeaders ? this.firstHeaderCell.innerText == '#' : false; } get hasRowHeaders() { return this.firstHeaderCell?.hasAttribute('data-header') ?? false; } get scrollContainer() { return this.element.lastElementChild; } get table() { return this.isScrollable ? this.scrollContainer.firstElementChild : this.element.firstElementChild; } get hiddenHeader() { return this.table.rows[0]; } get colGroup() { return this.table.firstElementChild; } get tableBody() { return this.table.tBodies[0]; } get rows() { return this.tableBody.rows; } get focusedRow() { return this.tableBody.querySelector('tr.focused'); } get focusedRowIndex() { return this.focusedRow?.rowIndex; } get selectedRows() { return [...this.getSelectedRows()]; } get selectedIndices() { return [...this.getSelectedIndices()]; } get selectionCount() { return [...this.getSelectedRows()].length; } get checkedIndices() { return [...this.getCheckedIndices()]; } get rowCount() { return this.tableBody.childElementCount; } get canMoveDown() { return (this.rowCount > 0) ? this.focusedRowIndex < (this.rowCount - 1) : false; } get canMoveUp() { return (this.rowCount > 0) ? this.focusedRowIndex > 0 : false; } get isNodeList() { return this.element.hasAttribute('data-node-list'); } *getNodeIds() { for (const row of this.tableBody.rows) { yield row.dataset.id; }} *getSelectedRows() { for (const row of this.tableBody.rows) { if (row.matches('.selected')) yield row; }} *getSelectedIndices() { for (const row of this.tableBody.rows) { if (row.matches('.selected')) yield row.rowIndex; }} *getCheckedIndices() { for (const row of this.tableBody.rows) { if (row.matches('.checked')) yield row.rowIndex; }} setUserSelect(enabled) { if (enabled) this.element.classList.remove('no-select'); else this.element.classList.add('no-select');} clear(includeHeader = false) { this.tableBody.innerHTML = ''; if (includeHeader) this.headerTable.innerHTML = '';} rowIsVisible(row) { let controlBounds = this.scrollContainer.getBoundingClientRect(); let rowBounds = row.getBoundingClientRect(); return (rowBounds.top >= controlBounds.top) && (rowBounds.bottom <= controlBounds.bottom);} clearSelection() { for (let row of this.tableBody.rows) { row.classList.remove('selected', 'focused'); }} clearCheckboxes() { for (let row of this.tableBody.rows) { row.classList.remove('checked'); row.querySelector('input').checked = false;}} invertCheckboxes() { for (let row of this.tableBody.rows) { Utils.toggleClass(row, 'checked');// row.classList.remove('checked'); let input = row.querySelector('input'); input.checked = !input.checked;}} focusRow(row, clearCurSelection = true, raiseEvent = true) { if (!row) return; if (row.matches('.focused')) { if (!this.rowIsVisible(row)) row.scrollIntoView({ block: 'center' }); if (!(clearCurSelection || raiseEvent)) return; if (this.selectionCount == 1) return;}; if (this.focusedRow) this.focusedRow.classList.remove('focused'); if (clearCurSelection || !this.multiSelect) this.clearSelection(); row.classList.add('focused', 'selected'); if (!this.rowIsVisible(row)) row.scrollIntoView({ block: 'center' }); if (raiseEvent) this.bubbleEvent(ObjectList.selectionChangedEvent);} selectRow(row) { if (!row) return; if (row.matches('.selected')) return; row.classList.add('selected'); if (!this.rowIsVisible(row)) row.scrollIntoView({ block: "center" });} selectRows(indices) { for (let idx of indices) this.rows[idx].classList.add('selected');} toggleSelectedState(row) { if (row.classList.contains('selected')) this.focusedRow.classList.remove('selected'); this.focusRow(row, false);} toggleSelection() { for (const row of this.selectedRows) { this.toggleCheckState(row);}} toggleCheckState(row) { if (!this.showCheckboxes) return; let input = row.querySelector('input'); input.checked = !input.checked; Utils.setClass(row, 'checked', input.checked); this.bubbleEvent(ObjectList.checkStateChangedEvent);} selectAllRows() { if (!this.multiSelect) return; for (const row of this.tableBody.rows) Utils.setClass(row, 'selected', true); this.bubbleEvent(ObjectList.selectionChangedEvent); return this.tableBody.rows.length;} removeSelectedRows() { let rowsToRemove = [...this.selectedRows]; let indices = this.selectedIndices.sort((a, b) => a - b); let firstIndex = indices[0], lastIndex = indices[indices.length - 1]; let rowToFocus = null; if (lastIndex < this.rows.length - 1) rowToFocus = this.rows[lastIndex + 1]; else if (firstIndex > 0) rowToFocus = this.rows[firstIndex - 1]; for (const row of rowsToRemove) row.remove(); this.renumberList(); if (rowToFocus) this.focusRow(rowToFocus);} removeRow(row) { let rowToFocus; if (row == this.focusedRow) { rowToFocus = row.nextElementSibling == null ? row.previousElementSibling : row.nextElementSibling; } row.remove(); this.renumberList(); if (rowToFocus) this.focusRow(rowToFocus);} moveRow(direction) { let newIndex = this.focusedRowIndex + direction; let row = this.focusedRow; row.remove(); if (this.tableBody.children.length == newIndex) { this.tableBody.appendChild(row);} else { let nextNode = this.tableBody.children[newIndex]; this.tableBody.insertBefore(row, nextNode);} if (!this.rowIsVisible(row)) row.scrollIntoView(); this.renumberList();} updateFocusedRow(data) { let tmpTable = document.createElement('TABLE'); tmpTable.innerHTML = data; let row = tmpTable.rows[0]; let focusedRow = this.focusedRow; focusedRow.innerHTML = row.innerHTML; Utils.copyAttributes(row, focusedRow); focusedRow.classList.add('selected', 'focused');} addRow(data) { let tmpTable = document.createElement('TABLE'); tmpTable.innerHTML = data; let row = tmpTable.rows[0]; this.tableBody.appendChild(row); this.focusRow(row);} createNewRow(id, name) { let index = this.tableBody.children.length + 1; let row = document.createElement('TR'); row.dataset.id = id; if (this.hasRowNumbers) { row.innerHTML = `${index}${name}`;} else { row.innerHTML = `${name}`;} this.tableBody.appendChild(row);} renumberList() { if (!this.hasRowNumbers) return; for (const row of this.tableBody.rows) { row.cells[0].innerText = row.rowIndex.toLocaleString();}} getRowById(id) { for (const row of this.tableBody.rows) { if (row.dataset.id == id) return row;} return null;} selectRange(row1, row2) { let curRow = (row1.rowIndex < row2.rowIndex) ? row1 : row2; let endRow = (row1.rowIndex > row2.rowIndex) ? row1 : row2; while (curRow != endRow) { Utils.setClass(curRow, 'selected', true); curRow = curRow.nextElementSibling;} this.focusRow(row2, false, false); this.bubbleEvent(ObjectList.selectionChangedEvent);} nextVisibleRow(row) { let curRow = row.nextElementSibling; while (curRow != null) { if (!curRow.matches('.hidden')) return curRow; curRow = curRow.nextElementSibling;} return null;} prevVisibleRow(row) { let curRow = row.previousElementSibling; while (curRow != null) { if (!curRow.matches('.hidden')) return curRow; curRow = curRow.previousElementSibling;} return null;} OnFocusIn(e) { if (e.target.matches('input[type="checkbox"]')) { this.focusRow(e.target.closest('tr'));}} OnChange(e) { let row = e.target.closest('tr'); Utils.setClass(row, 'checked', e.target.checked); this.bubbleEvent(ObjectList.checkStateChangedEvent);} OnClick(e) { if (this.isDisabled || !e.target.matches('tbody tr *')) return; e.stopPropagation(); let row = e.target.closest('tr'); switch (true) { case e.shiftKey: this.selectRange(this.focusedRow, row); break; case e.ctrlKey && row.matches('.focused'): break; case e.ctrlKey && row.matches('.selected'): row.classList.remove('selected'); this.bubbleEvent(ObjectList.selectionChangedEvent); break; case e.ctrlKey: this.focusRow(row, false); break; default: this.focusRow(row);}} OnDoubleClick(e) { if (e.target.matches('thead tr *')) { this.OnHeaderDoubleClick(e);} else if (this.showCheckboxes && e.target.matches('tbody tr *')) { this.toggleCheckState(e.target.closest('tr'));} else if (e.target.matches('tbody tr *')) { this.bubbleEvent(ObjectList.rowDoubleClickEvent); let row = e.target.closest('tr'); if (this.isNodeList && this.parentPage.navigateTo) { this.parentPage.navigateTo(row.dataset.id);}}} OnHeaderDoubleClick(e) { let th = e.target.closest('th'); if (th.matches('tr.parent-header th')) { let startIndex = this.getStartingColumnIndex(th); let colSpan = th.colSpan ?? 1; let endIndex = startIndex + colSpan - 1; for (let idx = startIndex; idx <= endIndex; idx++) { this.autoSizeColumn(idx);}} else { this.autoSizeColumn(th.cellIndex);}} OnKeyDown(e) { if (this.isDisabled) return; let row = this.focusedRow; if (row == null) { if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key) && this.tableBody.firstElementChild) { this.focusRow(this.tableBody.firstElementChild); return false;} return;} switch (Utils.getKeyID(e)) { case 'Shift': this.setUserSelect(false); break; case 'Shift + ArrowUp': this.toggleSelectedState(row.previousElementSibling); break; case 'Shift + ArrowDown': this.toggleSelectedState(row.nextElementSibling); break; case 'Shift + Home': this.selectRange(row, this.tableBody.firstElementChild); break; case 'Shift + End': this.selectRange(row, this.tableBody.lastElementChild); break; case 'ArrowUp': this.focusRow(this.prevVisibleRow(row)); break; case 'ArrowDown': this.focusRow(this.nextVisibleRow(row)); break; case 'Control + A': this.selectAllRows(); e.preventDefault(); break; case 'Home': this.focusRow(this.tableBody.firstElementChild); break; case 'End': this.focusRow(this.tableBody.lastElementChild); break; case ' ': this.toggleSelection(); break; default: return;} return false;} OnKeyUp(e) { if (this.isDisabled) return; switch (Utils.getKeyID(e)) { case 'Shift': this.setUserSelect(true); break;}} activeColumn; OnPointerDown(e) { if (!e.target.matches('th > .handle')) return; this.activeColumn = e.target.parentElement; e.target.setPointerCapture(e.pointerId);} OnPointerUp(e) { if (e.target.hasPointerCapture(e.pointerId)) e.target.releasePointerCapture(e.pointerId); this.activeColumn = null;} OnPointerMove(e) { if (!e.target.hasPointerCapture(e.pointerId) || (e.movementX == 0)) return; let bounds = this.activeColumn.getBoundingClientRect(); let width = Math.max(e.pageX - bounds.left, 32); this.setColumnWidth(this.activeColumn.cellIndex, width);} setColumnWidth(columnIndex, width) { let col = this.headerColGroup.children[columnIndex]; let headerCol = this.colGroup.children[columnIndex]; let delta = width - this.getCssWidth(col); this.setWidth(col, width); this.setWidth(headerCol, width); let above = this.headerRow.previousElementSibling; while (above != null) { this.updateParentHeaderWidth(columnIndex, above, delta); above = above.previousElementSibling;}} updateParentHeaderWidth(columnIndex, row, delta) { let curIndex = 0; for (const cell of row.cells) { let colSpan = cell.colSpan ?? 1; let span = new Span(curIndex, colSpan); if (span.includes(columnIndex)) { let curWidth = this.getCssWidth(cell); let width = curWidth + delta; this.setWidth(cell, width);} curIndex += colSpan;}} getColumnWidth(columnIndex) { return this.getCssWidth(this.headerRow.cells[columnIndex]);} getCssWidth(th) { return parseFloat(th.style.width.replace('px', ''));} setWidth(th, width) { th.style.width = `${width}px`; th.style.minWidth = `${width}px`; th.style.maxWidth = `${width}px`;} autoSizeColumn(columnIndex) { let th = this.headerRow.cells[columnIndex]; if (th.matches('[data-header]')) return; this.setColumnWidth(columnIndex, 32); let maxWidth = th.firstElementChild.scrollWidth + 4; for (const row of this.rows) { let td = row.cells[columnIndex]; maxWidth = Math.max(maxWidth, td.scrollWidth);} let newWidth = Math.min(1000, maxWidth + 8); this.setColumnWidth(columnIndex, newWidth);} getStartingColumnIndex(th) { let startIndex = 0; for (const cell of th.parentElement.children) { if (cell == th) break; let colSpan = cell.colSpan ?? 1; startIndex += colSpan;} return startIndex;} static ListOptions = { None: 0, FixedWidth: 1, MultiSelect: 2, AutoSelect: 4, IncludeRowNumbers: 8, HighlightDirty: 16, FlagInvalid: 32, SelectAll: 64, ShowCheckboxes: 128};}; class ObjectListErr extends ObjectList { constructor(element) { super(element); this.tableBody.ondblclick = () => this.OnNavigate(); this.element.onkeydown = e => this.OnKeyDown(e);} OnNavigate() { this.parentPage.navigateTo(this.focusedRow.dataset.nodeId); this.parentDialog.hide();} OnKeyDown(e) { if (this.isDisabled) return; switch (Utils.getKeyID(e)) { case "Enter": this.OnNavigate(); break; default: super.OnKeyDown(e);}}}; class PageNavigator extends HtmlControl { static pageChangedEvent = "PageNavigator:pagechanged"; DisplayTimer; DisplayDocId; constructor(element, skipInit = false) { super(element); this.oldPageNo = parseInt(this.pageNoInput.value); this.element.onclick = e => this.OnClick(e); this.element.onkeydown = e => this.OnKeyDown(e); this.pageNoInput.onchange = e => this.displayCurrentPage(); this.setInputWidth();} get PageNo() { let newPageNo = parseInt(this.pageNoInput.value); if (!isNaN(newPageNo)) { this.oldPageNo = newPageNo; return newPageNo;} else { this.pageNoInput.value = this.oldPageNo return this.oldPageNo;}} set PageNo(value) { if (value != this.PageNo) { this.pageNoInput.value = value; this.setCommandStates();}} get isIndeterminate() { return parseInt(this.dataset.pages) == -1; } get PageCount() { return this.isIndeterminate ? null : parseInt(this.dataset.pages); } set PageCount(value) { if (this.isIndeterminate) return; this.dataset.pages = value; this.pageCountElement.innerText = value.toLocaleString(); Utils.setClass(this.element, 'disabled', value < 2); if (this.PageNo > this.PageCount) this.PageNo = this.PageCount; this.setInputWidth(); this.setCommandStates();} get PageIndex() { return this.PageNo - 1; } get firstPageButton() { return this.find('#first'); } get prevPageButton() { return this.find('#previous'); } get nextPageButton() { return this.find('#next'); } get lastPageButton() { return this.find('#last'); } get pageNoInput() { return this.find('input'); } get pageCountElement() { return this.find('#count'); } get inputWidth() { return this.isIndeterminate ? '59px' : `${(16 + this.digits * 10)}px`; } get digits() { return this.PageCount == 0 ? 1 : Math.floor(Math.log10(this.PageCount, 10)) + 1; } setInputWidth() { this.pageNoInput.style.width = this.inputWidth;} enable() { this.pageNoInput.disabled = false; this.removeClass('disabled');} disable(clear = false) { this.pageNoInput.disabled = true; this.addClass('disabled'); if (clear) { this.PageNo = 1; this.PageCount = 1;}} moveRelative(offset, delay = 0) { if (this.pageNoInput.disabled) return; let newPageNo = this.isIndeterminate ? this.PageNo + offset : Math.max(1, Math.min(this.PageCount, this.PageNo + offset)); if (newPageNo == this.PageNo) return; if (delay > 0) { this.PageNo = newPageNo; this.startDisplayTimer(delay);} else { this.displayPage(newPageNo);}} displayCurrentPage() { this.displayPage(this.PageNo);} displayPage(pageNo) { if (this.pageNoInput.disabled) return; this.PageNo = this.isIndeterminate ? pageNo : Math.max(1, Math.min(this.PageCount, pageNo)); this.setCommandStates(); this.bubbleEvent(PageNavigator.pageChangedEvent);} setCommandStates() { Utils.setButtonState(this.firstPageButton, this.PageNo > 1); Utils.setButtonState(this.prevPageButton, this.PageNo > 1); Utils.setButtonState(this.nextPageButton, this.isIndeterminate || this.PageNo < this.PageCount); Utils.setButtonState(this.lastPageButton, !this.isIndeterminate && this.PageNo < this.PageCount); this.pageNoInput.disabled = this.PageCount < 2;} startDisplayTimer(delay) { if (this.DisplayTimer != null) clearTimeout(this.DisplayTimer); this.DisplayTimer = setTimeout(e => this.displayCurrentPage(), delay, this);} getDisplayText(pageSize, totalCount, name) { let totalText = `${totalCount.toLocaleString()} ${name}`; if (this.PageCount <= 1) return totalText; let startPos = this.PageIndex * pageSize + 1, endPos = Math.min(startPos + pageSize - 1, totalCount); return `${startPos.toLocaleString()} to ${endPos.toLocaleString()} of ${totalText}`;} OnKeyDown(e) { if (this.pageNoInput.disabled || Utils.isModifierKey(e)) return; if (this.triggerButtonShortcut(e)) { e.stopPropagation(); return false;}} OnClick(e) { if (!Utils.isButtonEnabled(e.target)) return; switch (e.target.id) { case 'first': return this.displayPage(1); case 'previous': return this.moveRelative(-1); case 'next': return this.moveRelative(1); case 'last': return this.displayPage(this.PageCount);}}}; class PropertyGrid extends HtmlControl { focusedProperty; activeEditor; shiftIsPressed; controlIsPressed; static editedEvent = 'PropertyGrid:Edited'; constructor(element) { super(element); this.propDIV.onchange = e => this.OnChange(e); this.propDIV.onclick = e => this.OnClick(e); this.propDIV.ondblclick = e => this.OnDoubleClick(e); this.propDIV.onkeydown = e => this.OnKeyDown(e); this.propDIV.onkeyup = e => this.OnKeyUp(e); this.propDIV.oncontextmenu = e => this.OnContextMenu(e); this.propDIV.onscroll = e => this.closeEditor(); this.propDIV.addEventListener("focusin", e => this.OnFocusIn(e)); this.menu.addListener(ContextMenu.executeEvent, e => this.OnExecute(e.detail)); this.menu.addListener(ContextMenu.closedEvent, e => this.OnMenuClosed(e)); HtmlControl.initBranch(this.element); this.initCheckBoxes(); this.syncHelpVisibility(); this.initMenu();} get propDIV() { return this.element.firstElementChild; } get helpDIV() { return this.element.children[1]; } get errorList() { return this.propDIV.firstElementChild; } get propTable() { return this.propDIV.children[1]; } get formIsValid() { return this.propTable.hasAttribute('data-valid'); } get focusedInput() { return this.propTable.querySelector('input:focus'); } get menuElement() { return this.element.lastElementChild; } get menu() { return HtmlControl.get(this.menuElement); } get typeDisplayName() { return this.dataset.desc; } get typeName() { return this.dataset.type; } get gridId() { return this.element.id; } get isEdited() { return this.element.querySelector('.pg-control.edited') != null; } get editedCount() { return this.element.querySelectorAll('.pg-property[data-level="1"] .pg-control.edited').length; } initMenu() { this.menu.addSeparator(); this.menu.addOption('Show Help Window', 'Hide or show the help window.', 'Alt + H', this.helpIsVisible);} refreshProperties() { this.refreshProps('/PropertyGrid/LoadProperties', false, { gridId: this.gridId });} setPropertyValue(propertyName, value, raiseChangeEvent = true) { let postData = { gridId: this.gridId, propertyName: propertyName, value: value }; this.refreshProps('/PropertyGrid/PropertyChanged', raiseChangeEvent, postData);} setListValue(propertyName, values) { let postData = { gridId: this.gridId, propertyName: propertyName, values: values }; this.refreshProps('/PropertyGrid/ListChanged', true, postData);} advancePropertyValue(propertyName, offset) { this.refreshProps(`/PropertyGrid/NextListValue?gridId=${this.gridId}&propertyName=${propertyName}&offset=${offset}`, true);} setFocus(propertyName) { if ((propertyName == null) || (propertyName == '')) return; let input = this.element.querySelector('input#' + propertyName); if (input) input.focus();} getPropertyValue(propertyName) { let input = this.getInputElement(propertyName); return input ? input.value : null;} getInputElement(propertyName) { if (Utils.isNullOrEmpty(propertyName)) return null; return this.element.querySelector('input#' + propertyName);} refreshProps(URL, changed, postData = null, oncomplete = null) { this.closeEditor(); if (postData) { HTTP.post(URL, postData, data => { this.updateFrom(data, changed); if (oncomplete) oncomplete();});} else { HTTP.get(URL, data => { this.updateFrom(data, changed); if (oncomplete) oncomplete();});}} updateFrom(data, changed) { this.propDIV.innerHTML = data; HtmlControl.initBranch(this.propDIV); this.initCheckBoxes(); this.setFocus(this.focusedProperty); if (changed) { this.bubbleEvent(PropertyGrid.editedEvent); this.bubbleEvent(HtmlControl.formEditedEvent);}} initCheckBoxes() { for (const checkbox of this.propTable.querySelectorAll('input[type="checkbox"]')) { let row = new PropertyRow(checkbox); if (row.input.value == '') checkbox.indeterminate = true;}} closeEditor() { if (this.activeEditor) this.activeEditor.propertyRow.closeDropDown(); this.activeEditor = null;} getRow(element) { let row = new PropertyRow(element); if ((row.element == null) || (row.grid != this)) return null; return row;} get helpIsVisible() { return this.settings.showHelp ?? true; } syncHelpVisibility() { if (this.helpIsVisible) this.helpDIV.classList.remove('d-none'); else this.helpDIV.classList.add('d-none');} toggleHelpVisibility() { this.settings.showHelp = !this.settings.showHelp; this.saveSettings(); if (this.helpIsVisible) { this.helpDIV.classList.remove('d-none'); this.showHelp(this.propTable.rows[0]);} else this.helpDIV.classList.add('d-none');} showHelp(row, oncomplete = null) { if (!this.helpIsVisible) { if (oncomplete) oncomplete(); return;} let propertyName = row.propertyPath; if (propertyName == this.focusedProperty) return; this.focusedProperty = propertyName; let postData = { gridId: this.gridId, propertyName: propertyName }; HTTP.post('/PropertyGrid/RenderPropertyInfo', postData, data => { let content = HTML.parseElement(data); this.helpDIV.innerHTML = content.firstElementChild.innerHTML; this.menu.replaceInner(content.lastElementChild.innerHTML); this.initMenu(); this.setCommandStates(row); if (oncomplete) oncomplete();});} executeCommand(menuItem) { let commandType = menuItem.mode == 'auto' ? menuItem.command : null; let commandProperties = { PropertyIds: this.runtimeSelection }; let postData = { gridId: this.gridId, typeName: commandType, properties: JSON.stringify(commandProperties) }; HTTP.post('/PropertyGrid/Execute', postData, data => { if (menuItem.shortName != 'CopyProperties') this.updateFrom(data, true);});} OnMenuClosed(e) { if (e.detail.refocus) this.setFocus(this.focusedProperty);} OnExecute(menuItem) { if (menuItem.mode == 'js') return this.OnJsCommandExecute(menuItem); if (menuItem.mode == 'auto') return this.executeCommand(menuItem); let lastSettings = Utils.fetchObjectData(menuItem.command); this.layout.showModal({ caller: this, okText: 'Apply', caption: menuItem.displayName, URL: '/PropertyGrid/NewCommand', postData: { gridId: this.gridId, typeName: menuItem.command, lastSettings: JSON.stringify(lastSettings) }, onOk: () => { PropertyGrid.saveSettings('CurrentCommand', menuItem.command, () => { this.executeCommand(menuItem);});}});} OnJsCommandExecute(menuItem) { if (menuItem.command == 'Show Help Window') { this.toggleHelpVisibility();}} OnContextMenu(e) { if (e.target.closest('[data-control]') != this.element) return; if (e.target.closest('.PropertyGrid') != this.element) return; e.preventDefault(); if (e.ctrlKey || e.shiftKey) return; if (e.target.tagName == 'TABLE') return; let row = new PropertyRow(e.target); if (row.grid != this || row.isErrorList) return; if (row.isProperty && !row.isSelected && (row.propertyPath != this.focusedProperty)) row.input.focus(); this.setCommandStates(row); if (this.menu) this.menu.showAt(e.pageX - 1, e.pageY - 1);} setCommandStates(focusedRow) { let focusedRows = focusedRow.isProperty ? [focusedRow] : []; let rows = this.hasSelection ? [...this.getSelectedRows()] : focusedRows; this.menu.setCommandVisible('Clear', this.canClear(rows)); this.menu.setCommandState('Reset', this.canReset(rows)); this.menu.setCommandState('ResetAll', this.hasEditedProperties); this.menu.setCommandState('CopyProperties', rows.length > 0);} canReset(rows) { for (const row of rows) { if (row.input.matches('.edited')) return true;} return false;} canClear(rows) { for (const row of rows) { if (row.canClear && !Utils.isNullOrEmpty(row.input.value)) return true;} return false;} get hasEditedProperties() { for (const rowElement of this.propTable.rows) { let row = new PropertyRow(rowElement); if (row.isProperty && row.input.matches('.edited')) return true;} return false;} OnChange(e) { if (e.target.matches('input.pg-control')) this.setPropertyValue(e.target.id, e.target.value);} OnClick(e) { this.saveModifierKeyState(e); let row = this.getRow(e.target); if (row == null) return; switch (true) { case e.target.id == 'errors': return this.OnErrorListToggle(row); case e.target.matches('.pg-category .pg-expander') : return this.OnCategoryToggle(row); case e.target.matches('.pg-property .pg-expander') : return this.OnPropertyToggle(row); case row.element.matches('.pg-category'): return this.OnCategoryLabelClick(e); case e.target.matches('.pg-dropdown:not(.disabled) .pg-dropdown-bullet'): return this.OnEditorToggle(row); case e.target.matches('.pg-property > .pg-labelcell, .pg-property > .pg-labelcell > label'): return this.OnPropertyLabelClick(e, row); case e.target.matches('.pg-property input[type="checkbox"]'): return this.OnCheckboxClick(e, row);}} OnCheckboxClick(e) { if (e.ctrlKey || e.shiftKey || e.altKey) return; let row = this.getRow(e.target); this.advancePropertyValue(row.propertyPath, 1); if (row.propertyPath != this.focusedProperty) row.input.focus(); return false;} OnCategoryLabelClick(e) { if (e.ctrlKey) { for (const row of this.propTable.querySelectorAll('.pg-property')) row.classList.add('selected');} else { this.showHelp(new PropertyRow(e.target)); this.clearSelection();}} OnPropertyLabelClick(e, row) { if (Utils.getModifierKeys(e) == 'Control') return this.selectRow(row); if (Utils.getModifierKeys(e) == 'Shift') return this.selectRange(row); row.input.focus(); if (!row.input.readOnly) row.input.select();} OnDoubleClick(e) { let row = this.getRow(e.target); if (row == null) return; switch (true) { case row.isCategory: return this.OnCategoryToggle(row); case !row.isProperty: return; case e.target.matches('input.pg-control[data-ref]'): return this.OnReferenceClick(e); case (e.target.closest('.pg-labelcell') != null) && row.isExpandable: return this.OnPropertyToggle(row); case (e.target.closest('.pg-labelcell') != null) && (row.isList || row.isBoolean) && (!row.isReadOnly): return this.advancePropertyValue(row.propertyPath, 1); case (e.target.tagName == 'INPUT') && (row.isList || row.isBoolean): return this.advancePropertyValue(row.propertyPath, 1);}} OnReferenceClick(e) { if (this.parentPage.navigateTo == undefined) return; this.parentPage.navigateTo(e.target.dataset.ref); this.layout.hidePopups(); this.layout.closeAllModals();} OnFocusIn(e) { if (!e.target.matches('input.pg-control')) return; let row = this.getRow(e.target); if (row == null) return; if (!this.controlIsPressed && !this.shiftIsPressed && !row.isSelected) { this.clearSelection(); this.selectRow(row);} this.showHelp(row); this.closeEditor();} saveModifierKeyState(e) { if (e.repeat) return; this.controlIsPressed = e.ctrlKey; this.shiftIsPressed = e.shiftKey;} OnKeyDown(e) { this.saveModifierKeyState(e); if (Utils.isModifierKey(e)) return; this.clearSelection(); if (!e.target.matches('input.pg-control')) return; let row = this.getRow(e.target); if (row == null) return; switch (Utils.getKeyID(e)) { case 'F12': this.startSpeechRecognition(); e.preventDefault(); e.stopPropagation(); return;} if (row.isReadOnly || e.shiftKey || e.altKey) return; if (row.canEditText && !e.ctrlKey) { return; } switch (true) { case e.key == 'ArrowLeft' && row.isList: this.advancePropertyValue(row.propertyPath, -1); return false; case e.key == 'ArrowRight' && row.isList: this.advancePropertyValue(row.propertyPath, 1); return false; case ((e.key == 'ArrowDown') || (e.key == ' ')) && row.isDropDown: row.openDropDown(); return false; case ['Delete', 'Backspace'].includes(e.key) && this.canReset([row]): this.advancePropertyValue(row.propertyPath, 0); return false;} let isAlphaKey = Utils.isMatchingCharacterKey(e, /[a-z0-9]/i); if (isAlphaKey && row.isList && !row.canEditText) { let rowToSelect = row.listEditor.findRow(e.key); if (rowToSelect) this.setPropertyValue(row.propertyPath, rowToSelect.cells[0].lastElementChild.innerText);}} OnKeyUp(e) { switch (Utils.getKeyID(e)) { case 'Control': this.controlIsPressed = false; break; case 'Shift': this.shiftIsPressed = false; break; case 'F12': this.stopSpeechRecognition(); e.preventDefault(); e.stopPropagation(); break;}} OnErrorListToggle() { Utils.toggleClass(this.errorList, 'd-none');} OnCategoryToggle(row) { this.refreshProps(`/PropertyGrid/CategoryExpanded?gridId=${this.gridId}&categoryName=${row.categoryName}`, false);} OnPropertyToggle(row) { this.refreshProps(`/PropertyGrid/PropertyExpanded?gridId=${this.gridId}&propertyName=${row.propertyPath}`, false, null, () => { let input = this.getInputElement(row.propertyPath); input?.focus();});} OnEditorToggle(row) { if (row.isEditorOpen) { this.activeEditor = null; row.closeDropDown();} else { this.closeEditor(); row.openDropDown(); this.showHelp(row);}} get selectedPropNames() { return [...this.getSelectedPropNames()]; } get selectedRowElements() { return this.propTable.querySelectorAll('.pg-property.selected'); } get firstSelectedRowElement() { return this.propTable.querySelector('.pg-property.selected'); } get firstSelectedRow() { return this.firstSelectedRowElement ? new PropertyRow(this.firstSelectedRowElement) : null; } get hasSelection() { return this.firstSelectedRow != null; } get focusedItemArray() { return Utils.isNullOrEmpty(this.focusedProperty) ? [] : [this.focusedProperty]; } get runtimeSelection() { return this.hasSelection ? this.selectedPropNames : this.focusedItemArray; } *getSelectedPropNames() { for (const row of this.getSelectedRows()) yield row.propertyPath;} *getSelectedRows() { for (const rowElement of this.selectedRowElements) yield new PropertyRow(rowElement);} clearSelection() { for (const row of this.selectedRowElements) { row.classList.remove('selected');}} selectRow(row) { if ((this.firstSelectedRow == null) || (this.firstSelectedRow.parentPropertyId == row.parentPropertyId)) { Utils.toggleClass(row.element, 'selected');} else { return false;}} selectRange(row) { if (this.firstSelectedRow == null) return Utils.setClass(row.element, 'selected', true); let startIdx = Math.min(this.firstSelectedRow.rowIndex, row.rowIndex); let endIdx = Math.max(this.firstSelectedRow.rowIndex, row.rowIndex); for (let idx = startIdx; idx <= endIdx; idx++) { let tr = this.propTable.rows[idx]; tr.classList.add('selected');}} recognition = null; startSpeechRecognition() { if (!Speech.isSupported || this.recognition) return; let input = this.focusedInput; this.recognition = Speech.createRecognition(true); this.recognition.onaudiostart = e => { input.classList.add('talking');}; this.recognition.onresult = e => this.onSpeechResult(e); this.recognition.start();} stopSpeechRecognition() { this.recognition?.stop(); this.recognition = null; this.focusedInput.classList.remove('talking');} onSpeechResult(e) { let text = Speech.getTranscript(e); let input = this.propTable.querySelector('input:focus'); input.value = text;} static saveSettings(gridId, typeName, oncomplete = null) { HTTP.post('/PropertyGrid/GetObject', {gridId: gridId}, data => { Utils.saveObjectData(typeName, data); if (oncomplete) oncomplete();});}} const PropertyFlags = { None: 0, CanClear: 1} ; class PropertyRow { element; constructor(elementOrDescendant) { this.element = elementOrDescendant.closest('.pg-proplist > tbody > tr');} get grid() { return HtmlControl.get(this.element, 'PropertyGrid'); } get layout() { return this.grid?.layout; } get propertyPath() { return this.input ? this.input.id : null; } get pathSegments() { return this.propertyPath.split('-'); } get parentPropertyId() { let segments = this.pathSegments; segments.splice(segments.length - 1, 1); return segments.join('-');} get fullPath() { return `${this.grid.element.id}-${this.propertyPath}`; } get className() { return this.element.className; } get isCategory() { return this.element.matches('.pg-category'); } get labelCell() { return this.element.firstElementChild; } get label() { return this.labelCell.querySelector('label'); } get inputCell() { return this.element.lastElementChild; } get inputRoot() { return this.inputCell.firstElementChild; } get isDropDown() { return this.inputRoot.matches('.pg-dropdown'); } get isModal() { return this.inputRoot.matches('.pg-dropdown.pg-modal'); } get dropDown() { return this.isDropDown ? this.inputRoot : null; } get editorCard() { return this.isDropDown ? this.inputRoot.lastElementChild : null; } get listEditor() { return this.isList ? HtmlControl.get(this.editorCard.firstElementChild) : null; } get isEditorOpen() { return this.isDropDown && this.editorCard.matches('.visible'); } get isErrorList() { return this.element.children.length == 1 && this.element.matches('.pg-error-list'); } get isSpacer() { return this.element.matches('.pg-spacer'); } get isProperty() { return this.element.matches('.pg-property'); } get isSelected() { return this.element.matches('.selected'); } get flags() { return parseInt(this.input.dataset.flags); } get canClear() { return (this.flags & PropertyFlags.CanClear) != 0; } get isList() { return !this.isCategory && this.isDropDown && this.inputRoot.hasAttribute('data-list'); } get isBoolean() { return this.inputCell.lastElementChild.matches('input[type="checkbox"]'); } get input() { return this.inputCell.querySelector('input'); } get isReadOnly() { return this.input.matches('.readonly'); } get canEditText() { return !this.input.hasAttribute('readonly'); } get expanderBullet() { return this.isCategory ? this.labelCell.lastElementChild : (this.isProperty ? this.labelCell.firstElementChild : null); } get isExpandable() { return this.isProperty && (this.expanderBullet.innerText != ''); } get categoryName() { return this.labelCell.firstElementChild.innerHTML; } get rowIndex() { return this.element.rowIndex; } openModal() { this.layout.showModal({ caller: this, caption: this.label.innerText, requireEdit: true, URL: '/PropertyGrid/RenderEditor', postData: { gridId: this.grid.gridId, propertyName: this.input.id }, onLoad: element => { let editor = HtmlControl.get(element); this.grid.activeEditor = editor; editor.OnOpen(this);}, onOk: element => { HtmlControl.get(element).commitChange(); this.grid.activeEditor = null; }, onCancel: element => { this.grid.activeEditor = null; this.input.focus();}});} openDropDown() { if (this.isModal) return this.openModal(); let inputBounds = this.input.getBoundingClientRect(); let point = new Point(inputBounds.left, inputBounds.bottom + 2); if (this.isList) { this.showPopup(point, this.editorCard.innerHTML);} else { let postData = { gridId: this.grid.gridId, propertyName: this.input.id }; HTTP.post('/PropertyGrid/RenderEditor', postData, data => this.showPopup(point, data));}} showPopup(point, content) { let popup = this.layout.showPopup(this.propertyPath, point, content, 'ev06 editor'); popup.style.minWidth = `${this.input.offsetWidth}px`; HtmlControl.initBranch(popup); let control = HtmlControl.get(popup.querySelector('[data-control]')); this.grid.activeEditor = control; control.OnOpen(this);} closeDropDown() { this.layout?.hidePopup(this.propertyPath);}} ; class PropertyGridEditor extends HtmlControl { focusTimer; propertyRow; constructor(element) { super(element); this.element.onkeydown = e => this.OnKeyDown(e);} commitChange() { } OnOpen(propertyRow) { this.propertyRow = propertyRow; if (!this.element.matches(':focus-within')) this.element.focus(); this.addListener('focusin', e => this.OnFocusIn(e)) this.addListener('focusout', e => this.OnFocusOut(e))} get dropDown() { return this.propertyRow.dropDown; } get card() { return this.element.parentElement; } get input() { return this.dropDown.querySelector('input.pg-control'); } get grid() { return this.propertyRow.grid; } get parentModalElement() { return this.element.closest('.ModalBox'); } get parentModal() { return HtmlControl.get(this.parentModalElement); } get propertyName() { return this.input.id; } setDirtyState(isDirty) { this.parentModal.setDirtyState(isDirty);} OnFocusOut(e) { if (this.propertyRow.isModal) return; this.focusTimer = setTimeout(() => { if (this.propertyRow?.grid) this.propertyRow.closeDropDown();}, 150, this);} OnFocusIn(e) { if (this.propertyRow.isModal) return; clearTimeout(this.focusTimer);} OnKeyDown(e) { if (this.propertyRow.isModal) return; switch (e.key) { case 'Escape': this.input.focus(); e.stopPropagation(); return false;}}} ; class AclEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setWidth('auto'); this.parentModal.setHeight('500px'); this.addListener(ObjectList.selectionChangedEvent, () => this.OnSelectionChanged()); this.addListener('keydown', e => this.OnKeyDown(e)); this.resultContainer.addEventListener('dblclick', e => this.OnResultDoubleClick(e)); this.addListener('click', e => this.OnToolbarClick(e)); this.setCommandStates(); this.displayCount();} commitChange() { let selectedNodeIds = [...this.getPrincipalIds()]; this.grid.setListValue(this.propertyName, selectedNodeIds);} get leftPanel() { return this.element.firstElementChild; } get rightPanel() { return this.element.lastElementChild; } get principalList() { return HtmlControl.get(this.leftPanel.lastElementChild); } get resultContainer() { return this.rightPanel.lastElementChild; } get resultList() { return HtmlControl.get(this.resultContainer.firstElementChild); } get leftToolbar() { return this.leftPanel.firstElementChild; } get leftCaption() { return this.leftToolbar.firstElementChild; } get deleteButton() { return this.find('#delete'); } get toggleButton() { return this.find('#toggle'); } get searchInput() { return this.find('#search-input'); } get searchButton() { return this.find('#search'); } get showGroups() { return this.toggleButton.textContent == 'group'; } set showGroups(value) { this.toggleButton.textContent = value ? 'group' : 'person'; this.toggleButton.title = value ? 'Groups - click to show users.' : 'Users - click to show groups.';} *getPrincipalIds() { for (const row of this.principalList.rows) { yield row.dataset.id;}} removeFocusedRow() { let rows = [...this.principalList.getSelectedRows()]; for (const row of rows) { row.remove();} this.parentModal.setDirtyState(true);} hasPrincipal(principalId) { for (const row of this.principalList.rows) { if (row.dataset.id == principalId) return true;} return false;} loadSearchResults() { HtmlControl.loadPost(this.resultContainer, '/PropertyGrid/SearchLDAP', { searchText: this.searchInput.value, groupSearch: this.showGroups });} displayCount() { this.leftCaption.innerText = this.principalList.rowCount.toLocaleString() + ' entries';} setCommandStates() { Utils.setButtonState(this.deleteButton, (this.principalList.focusedRow != null));} OnSelectionChanged() { this.setCommandStates();} OnKeyDown(e) { let key = Utils.getKeyID(e); if (key == 'Delete' && !this.deleteButton.matches('.disabled') && e.target == this.principalList.element) { this.removeFocusedRow();} else if (key == 'Enter' && e.target == this.resultList.element) { this.OnResultDoubleClick(); e.stopPropagation();} else if (key == 'Enter' && e.target == this.searchInput) { this.searchButton.click(); e.stopPropagation();}} OnToolbarClick(e) { switch (true) { case e.target == this.toggleButton: return this.OnToggleButton(); case e.target == this.searchButton: return this.loadSearchResults(); case e.target == this.deleteButton: return this.removeFocusedRow();}} OnToggleButton() { this.showGroups = !this.showGroups; this.loadSearchResults();} OnResultDoubleClick(e) { let rows = [...this.resultList.getSelectedRows()]; for (const row of rows) { if (this.hasPrincipal(row.dataset.id)) return; let clone = row.cloneNode(true); clone.classList.remove('focused', 'selected'); this.principalList.tableBody.appendChild(clone);} this.parentModal.setDirtyState(true);}}; class AnchorEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('1340px', '800px'); this.testSource.addListener(TreeViewer.selectionChangedEvent, () => this.docViewer.loadDocument(this.testSource.focusedNodeId, 1, () => this.LoadAnchorList())); this.docViewer.addListener(ImageViewer.imageLoadedEvent, () => this.OnImageLoaded());} commitChange() { let url = `/PropertyGrid/WriteProperty/${this.gridElement.id}/${this.grid.element.id}?propertyName=${this.propertyRow.propertyPath}` this.grid.refreshProps(url, true);} get leftPanel() { return this.element.firstElementChild; } get rightPanel() { return this.element.lastElementChild; } get gridElement() { return this.leftPanel.firstElementChild; } get testSource() { return HtmlControl.get(this.leftPanel.lastElementChild); } get docViewer() { return HtmlControl.get(this.rightPanel.firstElementChild); } get imageViewer() { return this.docViewer.imageViewer; } get instanceListElement() { return this.rightPanel.lastElementChild; } get resultList() { return HtmlControl.get(this.instanceListElement); } get selectionPageNo() { return parseInt(this.resultList.focusedRow.cells[1].innerText); } get selectedLocation() { return Rectangle.fromParamList(this.resultList.focusedRow.dataset.location).cloneRounded(2); } OnImageLoaded() { Utils.setButtonVisible(this.docViewer.pageViewer.selectButton, false); Utils.setButtonVisible(this.docViewer.pageViewer.copyButton, false); this.DisplayLocation();} LoadAnchorList() { let propName = `${this.propertyRow.parentPropertyId}-LabelExtractor`; let URL = Utils.mapPath('/PropertyGrid/GetAnchors'); let postData = { gridId: this.grid.gridId, propertyName: propName, sourceId: this.testSource.focusedNodeId }; HTTP.post(URL, postData, data => { this.instanceListElement.outerHTML = data; HtmlControl.initControl(this.instanceListElement); this.resultList.addListener(ObjectList.selectionChangedEvent, () => this.OnResultSelected());});} OnResultSelected() { let json = JSON.stringify(this.selectedLocation.toRectangleExF); let postData = { dstGridId: this.gridElement.id, json: json }; HTTP.post(Utils.mapPath('/PropertyGrid/WriteJson'), postData, data => { this.gridElement.outerHTML = data; HtmlControl.initControl(this.gridElement); this.setDirtyState(true);}); if (this.docViewer.PageNo != this.selectionPageNo) this.docViewer.displayPage(this.selectionPageNo); this.DisplayLocation(json);} DisplayLocation(json = null) { this.docViewer.loadAnnotations(Utils.mapPath('/Shared/GetAnnotation'), { json: json });}}; class ChoiceEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentDialog.setSize('auto', '400px'); this.parentDialog.initToolbar(); if (this.choiceList.showCheckboxes) { this.buttonCell.appendChild(Snippets.createButton('invert', 'radio_button_partial', 'Invert all checkboxes.', true)); this.buttonCell.appendChild(Snippets.createButton('clear', 'layers_clear', 'Clear all checkboxes.', this.hasValue)); this.buttonCell.onclick = e => this.OnButtonClick(e); this.addListener(ObjectList.checkStateChangedEvent, () => this.OnValueChanged());} else { this.addListener(ObjectList.selectionChangedEvent, () => this.OnValueChanged()); this.choiceList.addListener('dblclick', e => this.OnDoubleClick(e));} this.setCommandStates();} commitChange() { let indices = this.choiceList.showCheckboxes ? this.choiceList.checkedIndices : this.choiceList.selectedIndices; let postData = { choiceListId: this.element.id, indices: indices, dstGridId: this.grid.gridId, propertyName: this.propertyName }; this.grid.refreshProps('/PropertyGrid/WriteChoiceProperty', true, postData);} get choiceList() { return HtmlControl.get(this.element.firstElementChild); } get buttonCell() { return this.parentDialog.toolbar; } get clearButton() { return this.buttonCell.querySelector('#clear'); } setCommandStates() { if (this.clearButton) Utils.setButtonState(this.clearButton, this.choiceList.checkedIndices.length > 0);} OnValueChanged() { this.setDirtyState(true); this.setCommandStates();} OnButtonClick(e) { if (!Utils.isButtonEnabled(e.target)) return; if (!e.target.id == 'clear') return; switch (e.target.id) { case 'clear': this.choiceList.clearCheckboxes(); Utils.setButtonState(this.clearButton, false); this.setDirtyState(true); this.setCommandStates(); break; case 'invert': this.choiceList.invertCheckboxes(); this.setDirtyState(true); this.setCommandStates(); break;}} OnDoubleClick(e) { if (this.choiceList.focusedRow.contains(e.target)) this.parentDialog.okButton.click();}} ; class CmisQueryEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(element); this.parentModal.setSize('90vw', '85vh'); this.cmisSearcher.addListener(CmisRepositorySearcher.objectDirtied, () => this.OnDirtied());} OnOpen(propertyRow) { super.OnOpen(propertyRow) this.OnDirtied()} get cmisSearcher() { return HtmlControl.get(this.element.firstElementChild); } commitChange() { let content = this.cmisSearcher.codeEditor.textContent; this.grid.setPropertyValue(this.propertyRow.propertyPath, content);} OnDirtied() { this.parentModal.setDirtyState(true);}}; class CodePropEditor extends PropertyGridEditor { constructor(element) { super(element); this.parentModal.setWidth('800px'); this.parentModal.setHeight('600px'); this.parentModal.caption = this.dataset.caption; HtmlControl.initBranch(element); this.codeEditor.addListener('input', () => this.parentModal.setDirtyState(true)); this.addAiButton();} commitChange() { if (this.isStringList) { let content = this.codeEditor.textContent.replaceAll('\xA0', ' '); let lines = content.split(CodeEditor.LineDelimiter); this.grid.setListValue(this.propertyRow.propertyPath, lines);} else { let content = this.codeEditor.textContent.replaceAll(CodeEditor.LineDelimiter, '\r\n'); content = content.replaceAll('\xA0', ' '); this.grid.setPropertyValue(this.propertyRow.propertyPath, content);}} get topPanel() { return this.element.firstElementChild; } get codeEditor() { return HtmlControl.get(this.topPanel); } get isStringList() { return this.element.hasAttribute('data-list'); } get toolsEnabled() { return this.element.dataset.tools != undefined; } OnButtonClick(e) { if (!Utils.isButtonEnabled(e.target)) return; if (e.target.id == 'AiGenerator') this.showHelper();} addAiButton() { this.parentModal.initToolbar(); let toolbar = this.parentModal.toolbar; toolbar.appendChild(Snippets.createButton('AiGenerator', 'cognition', 'Use AI to generate the text for you.', true)); toolbar.onclick = e => this.OnButtonClick(e);} showHelper() { if (this.toolsEnabled) { let propName = this.element.dataset.propName; let label = `AI will provide a value for the '${propName}' property using the instructions provided here:`; let instructions = this.getInstructions(); this.layout.showModal({ caller: this, width: '650px', caption: 'AI Helper', URL: '/Design/RenderAIHelper', postData: { lastText: instructions, inputLabel: label }, onLoad: element => { element.querySelector('.ce-content').focus(); }, onOk: element => { let editor = HtmlControl.get(element.firstElementChild.lastElementChild, 'CodeEditor'); this.onHelperOk(editor.textContent);}});} else { this.layout.showToast('AI Helper', 'A tools chat model must be configured on the LLM Connector repository option to use this feature.');}} onHelperOk(text) { if (text != '') { this.saveInstructions(text); let modal = this.layout.showProgress(`AI is processing your request`, `Generating response...`); let postData = { gridId: this.grid.gridId, propertyName: this.propertyName, message: text }; modal.httpPost('/CodeEditor/GetAiResponse', postData, (data) => { this.codeEditor.setText(data); this.setDirtyState(true);});}} getInstructions() { if (this.settings.instructions) { for (const curSettings of this.settings.instructions) { if (curSettings.propertyName == this.propertyName) return curSettings.instructions;}} return null;} saveInstructions(instructions) { if (this.settings.instructions == null) this.settings.instructions = []; let curSettings = this.settings.instructions.find(s => s.propertyName == this.propertyName); if (curSettings) curSettings.instructions = instructions; else { curSettings = { propertyName: this.propertyName, instructions: instructions }; this.settings.instructions.push(curSettings);} this.saveSettings();}} ; class CollationEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('90vw', '85vh'); this.parentModal.caption = this.dataset.caption;} commitChange() { let srcGridId = this.propertyRow.fullPath; this.grid.refreshProps(`/PropertyGrid/WriteProperty/${srcGridId}/${this.grid.element.id}?propertyName=${this.propertyRow.propertyPath}`, true)}}; class CollectionEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('auto', '500px'); this.parentModal.caption = this.dataset.caption; this.objectList.addListener(ObjectList.selectionChangedEvent, () => this.OnSelectionChanged()); this.objectList.addListener('keydown', e => this.OnListKeyDown(e)); this.addListener(PropertyGrid.editedEvent, e => this.OnPropertyEdited(e)); this.buttonCell.onclick = e => this.OnToolbarClick(e); this.element.onclick = () => this.addDropDown?.close(); this.setCommandStates(); this.displayCount();} commitChange() { let dstGridId = this.grid.element.id; this.grid.refreshProps(`/PropertyGrid/WriteListProperty/${this.propertyRow.fullPath}/${dstGridId}?propertyName=${this.propertyRow.propertyPath}`, true)} get leftPanel() { return this.element.firstElementChild; } get rightPanel() { return this.element.lastElementChild; } get focusedRowIndex() { return this.objectList.focusedRowIndex; } get selectedRows() { return this.objectList.selectedRows; } get selectedIndices() { return this.objectList.selectedIndices; } get hasSingleSelection() { return this.selectedRows.length == 1; } get childGrid() { return HtmlControl.get(this.rightPanel.firstElementChild); } get objectListParent() { return this.leftPanel.lastElementChild; } get objectListElement() { return this.objectListParent.firstElementChild; } get objectList() { return HtmlControl.get(this.objectListElement); } get toolbar() { return this.leftPanel.firstElementChild; } get captionCell() { return this.toolbar.firstElementChild; } get buttonCell() { return this.toolbar.lastElementChild; } get addButton() { return this.buttonCell.lastElementChild; } get addDropDownElement() { return this.buttonCell.querySelector('.DropButton'); } get addDropDown() { return HtmlControl.get(this.addDropDownElement); } get addDropDownlist() { return this.addDropDownElement.lastElementChild; } get typeList() { return this.addButton ? this.addButton.lastElementChild : null; } get moveUpButton() { return this.buttonCell.querySelector('#move_up'); } get moveDownButton() { return this.buttonCell.querySelector('#move_down'); } get deleteButton() { return this.buttonCell.querySelector('#delete'); } get copyButton() { return this.buttonCell.querySelector('#copy'); } get pasteButton() { return this.buttonCell.querySelector('#paste'); } get singleInstance() { return this.element.hasAttribute('data-single-instance'); } displayCount() { this.captionCell.innerText = this.objectList.rowCount.toLocaleString() + ' items';} addItem(typeName) { let postData = { gridId: this.grid.gridId, listId: this.propertyRow.fullPath, propId: this.propertyRow.propertyPath, typeName: typeName, options: this.objectList.listOptions }; HTTP.post('/PropertyGrid/AddCollectionElement', postData, data => { this.objectList.addRow(data); this.displayCount(); this.setDirtyState(true); this.setCommandStates();})} setCommandStates() { if (this.moveUpButton) Utils.setButtonState(this.moveUpButton, this.hasSingleSelection && this.objectList.canMoveUp); if (this.moveDownButton) Utils.setButtonState(this.moveDownButton, this.hasSingleSelection && this.objectList.canMoveDown); if (this.deleteButton) Utils.setButtonState(this.deleteButton, this.objectList.focusedRow != null); if (this.copyButton) Utils.setButtonState(this.copyButton, this.objectList.focusedRow != null); this.setAddButtonStates();} setAddButtonStates() { if (!this.singleInstance) return; for (const button of this.addDropDownlist.querySelectorAll('button')) { Utils.setButtonState(button, !this.hasType(button.dataset.type));}} hasType(type) { for (const row of this.objectList.rows) { if (row.dataset.type == type) return true;} return false;} OnSelectionChanged() { this.addDropDown?.close(); this.childGrid?.closeEditor(); HtmlControl.load(this.rightPanel, `/PropertyGrid/RenderCollectionElement?listId=${this.element.id}&index=${this.objectList.focusedRowIndex}`) this.setCommandStates();} OnPropertyEdited(e) { if (e.target != this.childGrid.element) return; let postData = { listId: this.propertyRow.fullPath, index: this.objectList.focusedRowIndex, options: this.objectList.listOptions } HTTP.post('/PropertyGrid/RenderCollectionEntry', postData, data => { let row = HTML.parseRow(data); this.objectList.focusedRow.innerHTML = row.innerHTML; Utils.setClass(this.objectList.focusedRow, 'dirty', row.classList.contains('dirty')); Utils.setClass(this.objectList.focusedRow, 'invalid', row.classList.contains('invalid'));});} OnToolbarClick(e) { if (e.target.dataset.type) return this.addItem(e.target.dataset.type); if (!Utils.isButtonEnabled(e.target)) return; switch (e.target.id) { case 'add': return this.onAddButton(e); case 'delete': return this.onDelete(); case 'move_up': return this.onMove(-1); case 'move_down': return this.onMove(1); case 'copy': return this.onCopy(false); case 'paste': return this.onPaste();}} onAddButton(e) { e.cancelBubble = true; return this.addItem(this.dataset.type);} OnListKeyDown(e) { this.triggerButtonShortcut(e);} onCopy() { let postData = { gridId: this.gri, propId: this.propId, listId: this.element.id, indices: this.selectedIndices}; HTTP.post("/PropertyGrid/CopyRows", postData, () => { Utils.setButtonState(this.pasteButton, true);});} onPaste() { let postData = { gridId: this.grid.gridId, propId: this.propertyRow.propertyPath, listId: this.element.id, options: this.objectList.listOptions }; HTTP.post("/PropertyGrid/PasteRows", postData, data => { let ctl = HTML.parseElement(data); for (const row of ctl.querySelectorAll('tbody > tr')) { this.objectList.tableBody.appendChild(row);} let lastRow = this.objectList.rows.length == 0 ? null : this.objectList.rows[this.objectList.rows.length - 1]; if (lastRow != null) this.objectList.focusRow(lastRow); this.parentModal.setDirtyState(true); this.setCommandStates()});} onMove(direction) { let postData = { listId: this.element.id, index: this.focusedRowIndex, direction: direction }; HTTP.post("/PropertyGrid/MoveListItem", postData, () => { this.objectList.moveRow(direction); this.setDirtyState(true); this.displayCount(); this.setCommandStates();});} onDelete() { HTTP.post("/PropertyGrid/DeleteListItems", { listId: this.element.id, indices: this.selectedIndices }, () => { this.objectList.removeSelectedRows(); if (this.objectList.rowCount == 0) this.rightPanel.innerHTML = ''; this.setDirtyState(true); this.displayCount(); this.setCommandStates();});}}; class ExtractorPropEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('90vw', '85vh'); this.parentModal.caption = this.dataset.caption; this.parentModal.element.firstElementChild.classList.add('resizable');} commitChange() { let srcGridId = this.propertyRow.fullPath; this.grid.refreshProps(`/PropertyGrid/WriteProperty/${srcGridId}/${this.grid.element.id}?propertyName=${this.propertyRow.propertyPath}`, true)}}; class FolderEditor extends PropertyGridEditor { isEditing = false; constructor(element) { super(element); this.parentModal.setSize('600px', '500px'); this.addListener(HtmlTree.selectionChangedEvent, e => this.OnSelectionChanged(e)); this.addListener(HtmlTree.loadChildrenEvent, e => this.OnLoadChildren(e)); this.addListener('input', e => this.OnInput(e)); this.addListener('change', e => this.OnChange(e)); this.addListener('keydown', e => this.OnKeyDown(e)); HtmlControl.initBranch(element); this.treeElement.focus(); this.scrollToSelection(); this.setCommandStates();} commitChange() { let path = this.formatPath(this.tree.focusedNode.path); this.grid.setPropertyValue(this.propertyName, path, true);} get toolbar() { return this.element.firstElementChild; } get treeElement() { return this.element.lastElementChild; } get tree() { return HtmlControl.get(this.treeElement); } get delim() { return this.dataset.delim; } get pathInput() { return this.toolbar.querySelector('input'); } get progress() { return this.toolbar.querySelector('progress'); } scrollToSelection() { if (this.tree.focusedElement) this.tree.focusedElement.scrollIntoView({ block: 'center' });} formatPath(path) { return this.delim == '\\' ? '\\\\' + path.substr(1).replaceAll('/', '\\') : path;} setCommandStates() { this.parentModal.setDirtyState(this.tree.focusedNode != null);} OnSelectionChanged(e) { this.pathInput.value = this.formatPath(this.tree.focusedNode.path); this.setCommandStates();} OnLoadChildren(e) { this.progress.classList.remove('v-hidden'); let node = e.detail.node; let postData = { gridId: this.grid.gridId, propertyName: this.propertyName, path: this.formatPath(node.path) }; node.loadChildren('/PropertyGrid/FolderEditor/GetChildren', postData, () => { this.progress.classList.add('v-hidden');});} OnInput(e) { this.isEditing = true;} OnChange(e) { this.isEditing = false; this.progress.classList.remove('v-hidden'); let postData = { gridId: this.grid.gridId, propertyName: this.propertyName, path: this.pathInput.value }; HTTP.post('/PropertyGrid/FolderEditor/RenderTree', postData, data => { this.treeElement.outerHTML = data; HtmlControl.initControl(this.treeElement); this.scrollToSelection(); this.setCommandStates(); this.progress.classList.add('v-hidden');});} OnKeyDown(e) { if (Utils.getKeyID(e) == 'Enter' && e.target.matches('input')) { if (this.isEditing) { e.stopPropagation();}}}} ; class ListEditor extends PropertyGridEditor { constructor(element) { super(element); element.onclick = e => this.OnClick(e); element.onkeydown = e => this.OnKeyDown(e);} OnOpen(propertyRow) { super.OnOpen(propertyRow); this.element.focus(); if (this.selectedRow && !this.isVisible(this.selectedRow)) this.selectedRow.scrollIntoView();} get selectedRow() { return this.element.querySelector('tr.selected'); } get selectedValue() { return this.selectedRow ? this.selectedRow.innerText : ''; } get table() { return this.element.firstElementChild; } get rows() { return this.table.rows; } get rowCount() { return this.rows.length; } get firstRow() { return this.rowCount == 0 ? null : this.rows[0]; } get lastRow() { return this.rowCount == 0 ? null : this.rows[this.rowCount - 1]; } isVisible(row) { let rowBounds = row.getBoundingClientRect(), myBounds = this.card.getBoundingClientRect(); return (rowBounds.top >= myBounds.top) && (rowBounds.bottom <= myBounds.bottom);} rowMatches(row, filter) { let labelCell = row.cells[0].lastElementChild; return labelCell.innerText.toLowerCase().startsWith(filter.toLowerCase())} findRow(filter) { if (this.selectedRow != null) { let nextRow = this.selectedRow.nextElementSibling; while (nextRow) { if (this.rowMatches(nextRow, filter)) return nextRow; nextRow = nextRow.nextElementSibling}} for (const row of this.rows) { if (row === this.selectedRow) return null; if (this.rowMatches(row, filter)) return row;}} selectRow(row) { if (this.selectedRow) { this.selectedRow.removeAttribute('aria-selected'); this.selectedRow.classList.remove('selected');} if (!this.isVisible(row)) row.scrollIntoView(); row.classList.add('selected'); row.setAttribute('aria-selected', 'true');} OnKeyDown(e) { switch (e.key) { case 'ArrowUp': let prevRow = (this.selectedRow == null) ? this.lastRow : this.selectedRow.previousElementSibling; if (prevRow) this.selectRow(prevRow); e.preventDefault(); return false; case 'ArrowDown': let nextRow = (this.selectedRow == null) ? this.firstRow : this.selectedRow.nextElementSibling; if (nextRow) this.selectRow(nextRow); e.preventDefault(); return false; case 'Enter': if (this.selectedRow) this.selectedRow.firstElementChild.click(); e.preventDefault(); e.stopPropagation(); return false} if ((e.key.length == 1) && e.key.match(/[a-z]/i)) { let row = this.findRow(e.key); if (row) this.selectRow(row);} return super.OnKeyDown(e);} OnClick(e) { let cell = e.target.closest('td'); let valueText = cell.lastElementChild.innerText; let value = (valueText == '(none)') ? '' : valueText; this.grid.setPropertyValue(this.propertyName, value); e.stopPropagation();}} ; class MultiReferenceEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.treeViewerElement.onchange = () => this.parentModal.setDirtyState(true);} OnOpen(propertyRow) { super.OnOpen(propertyRow); this.treeViewer.renderChildrenURL = `/PropertyGrid/NodeReferenceEditor/GetChildren/${this.grid.element.id}/${this.propertyName}`;} commitChange() { let selectedNodeIds = [...this.treeViewer.getCheckedNodeIds()]; this.grid.setListValue(this.propertyName, selectedNodeIds);} get treeViewerElement() { return this.element.lastElementChild; } get treeViewer() { return HtmlControl.get(this.treeViewerElement); }}; class OAuthLoginEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); element.onclick = e => this.OnClick(e);} commitChange() {} OnClick(e) { switch (e.target.id) { case 'login': return this.OnLogin(e); case 'logout': return this.OnLogout(e);}} OnLogin(e) { if (this.parentPage.isDirty) this.layout.showToast('Save Required', 'Changes must be saved before logging in.', 5); else window.open(e.target.dataset.url, '_blank'); this.parentDialog.cancelButton.click();} OnLogout(e) { window.open(e.target.dataset.url, '_blank'); let grid = this.grid, propertyPath = this.propertyRow.propertyPath; this.parentDialog.cancelButton.click(); grid.setPropertyValue(propertyPath, null, false);}}; class ObjectEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.caption = this.dataset.caption;} commitChange() { let srcGridId = this.propertyRow.fullPath; this.grid.refreshProps(`/PropertyGrid/WriteProperty/${srcGridId}/${this.grid.element.id}?propertyName=${this.propertyRow.propertyPath}`, true)}} ; class OrderedReferenceEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setWidth('auto'); this.parentModal.setHeight('400px'); this.parentModal.caption = this.dataset.caption; this.treeViewerElement.onchange = e => this.OnTreeViewChange(e); this.treeViewer.addListener(TreeViewer.selectionChangedEvent, () => this.OnTreeViewSelectionChanged()); this.objectList.addListener(ObjectList.selectionChangedEvent, () => this.OnObjectListSelectionChange()); this.toolbar.onclick = e => this.OnToolbarClick(e); this.setCommandStates(); this.displayCount();} OnOpen(propertyRow) { super.OnOpen(propertyRow); this.treeViewer.renderChildrenURL = `/PropertyGrid/NodeReferenceEditor/GetChildren/${this.grid.gridId}/${this.propertyName}`;} commitChange() { let selectedNodeIds = [...this.objectList.getNodeIds()]; this.grid.setListValue(this.propertyName, selectedNodeIds);} get leftPanel() { return this.element.firstElementChild; } get rightPanel() { return this.element.lastElementChild; } get objectListParent() { return this.leftPanel.lastElementChild; } get objectListElement() { return this.objectListParent.firstElementChild; } get objectList() { return HtmlControl.get(this.objectListElement); } get treeViewerElement() { return this.rightPanel.firstElementChild; } get treeViewer() { return HtmlControl.get(this.treeViewerElement); } get toolbar() { return this.leftPanel.firstElementChild; } get buttonCell() { return this.toolbar.lastElementChild; } get moveUpButton() { return this.buttonCell.firstElementChild; } get moveDownButton() { return this.moveUpButton.nextElementSibling; } get captionCell() { return this.toolbar.firstElementChild; } get treeViewSelectionId() { return this.treeViewer.focusedItem?.nodeId; } get objectListSelectionId() { return this.objectList.focusedRow?.dataset.id; } setCommandStates() { Utils.setButtonState(this.moveUpButton, this.objectList.canMoveUp); Utils.setButtonState(this.moveDownButton, this.objectList.canMoveDown);} displayCount() { let noun = this.objectList.rowCount == 1 ? ' reference' : ' references'; this.captionCell.innerText = this.objectList.rowCount.toLocaleString() + noun;} getNewRow(id, name) { return `${name}`;} OnTreeViewChange(e) { let node = new TreeNode(e.target); if (e.target.checked) { this.objectList.addRow(this.getNewRow(node.nodeId, node.name));} else { this.objectList.removeRow(this.objectList.getRowById(node.nodeId));} this.displayCount(); this.setDirtyState(true); this.setCommandStates(); e.cancelBubble = true;} OnTreeViewSelectionChanged() { let row = this.objectList.getRowById(this.treeViewSelectionId); this.objectList.focusRow(row);} OnObjectListSelectionChange() { let node = this.treeViewer.getNode(this.objectListSelectionId); node.focus(true, true, false); let parent = node.parentNode; while (parent) { if (!parent.isExpanded) { parent.toggleExpanded(); } parent = parent.parentNode;} this.setCommandStates();} OnToolbarClick(e) { if (!Utils.isButtonEnabled(e.target)) return; switch (e.target.id) { case 'move_up': e.cancelBubble = true; this.objectList.moveRow(-1); this.setCommandStates(); break; case 'move_down': e.cancelBubble = true; this.objectList.moveRow(1); this.setCommandStates(); break;} this.setDirtyState(true);}}; class PreviewImageEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('750px', '750px');} OnOpen(propertyRow) { super.OnOpen(propertyRow); let url = Utils.mapPath('/DocView/GetBarcodeDetectedPreview?gridId=' + this.grid.gridId); this.viewer.imageViewer.showImage(url);} get viewer() { return HtmlControl.get(this.element.firstElementChild); }}; class NodeReferenceEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.treeViewer.addListener(TreeViewer.nodeClickedEvent, e => this.OnNodeClicked(e)); this.treeViewer.addListener("keydown", e => this.OnKeyDown(e)); this.searchInput.onkeydown = e => this.OnSearchKey(e);} get treeViewer() { return HtmlControl.get(this.treeViewerElement); } get treeViewerElement() { return this.element.lastElementChild; } get searchInput() { return this.element.firstElementChild.lastElementChild; } OnOpen(propertyRow) { super.OnOpen(propertyRow); this.treeViewer.renderChildrenURL = `/PropertyGrid/NodeReferenceEditor/GetChildren/${this.grid.element.id}/${this.propertyName}`; this.treeViewer.element.focus(); this.treeViewer.focusedItem.ensureVisible();} OnNodeClicked(e) { if (this.treeViewer.focusedItem.isDisabled) return; let value = this.treeViewer.focusedId; this.grid.setPropertyValue(this.propertyName, value);} OnKeyDown(e) { switch (e.key) { case 'Enter': this.OnNodeClicked(e); e.stopPropagation(); break;} if ((e.key.length == 1) && !e.ctrlKey && !e.altKey && (e.target.tagName != 'INPUT')) { this.searchInput.focus();} super.OnKeyDown(e);} searchTimer; startSearchTimer() { if (this.searchTimer != null) clearTimeout(this.searchTimer); if (Utils.isNullOrWhiteSpace(this.searchInput.value)) return; this.searchTimer = setTimeout(() => this.executeSearch(), 250);} executeSearch() { this.searchTimer = null; let postData = { searchText: this.searchInput.value }; let URL = `/PropertyGrid/NodeReferenceEditor/Search/${this.grid.gridId}/${this.propertyName}`; HTTP.post(URL, postData, data => this.displaySearchResults(data));} displaySearchResults(data) { this.treeViewer.replaceRoots(data, this.searchInput.value.toLowerCase());} OnSearchKey(e) { switch (e.key) { case 'Delete': this.startSearchTimer(); break; case 'Backspace': this.startSearchTimer(); break; case 'ArrowDown': this.treeViewerElement.focus(); break; default: if (e.key.length == 1) this.startSearchTimer(); break;}}} ; class SampleImageEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setSize('auto', '600px'); this.parentModal.caption = 'Sample Image Editor'; this.addListener(ObjectList.selectionChangedEvent, e => this.OnSelectionChanged(e)) this.addListener('keydown', e => this.OnKeyDown(e)); this.buttonCell.onclick = e => this.OnToolbarClick(e); this.setCommandStates(); this.displayCount();} commitChange() { this.grid.refreshProperties();} get nodeId() { return this.dataset.node; } get leftPanel() { return this.element.firstElementChild; } get docViewer() { return HtmlControl.get(this.element.lastElementChild); } get sampleListParent() { return this.leftPanel.lastElementChild; } get sampleList() { return HtmlControl.get(this.sampleListParent.firstElementChild); } get focusedRow() { return this.sampleList.focusedRow; } get focusedSampleName() { return this.focusedRow?.cells[1].innerText; } get focusedFileName() { return this.focusedRow?.dataset.value; } get toolbar() { return this.leftPanel.firstElementChild; } get captionCell() { return this.toolbar.firstElementChild; } get buttonCell() { return this.toolbar.lastElementChild; } get pasteButton() { return this.buttonCell.querySelector('#paste'); } get renameButton() { return this.buttonCell.querySelector('#rename'); } get deleteButton() { return this.buttonCell.querySelector('#delete'); } get nextSampleName() { return this.focusedRow?.nextElementSibling?.cells[1].innerText; } get prevSampleName() { return this.focusedRow?.previousElementSibling?.cells[1].innerText; } displayCount() { this.captionCell.innerText = this.sampleList.rowCount.toLocaleString() + ' items';} setCommandStates() { Utils.setButtonState(this.renameButton, this.focusedRow != null); Utils.setButtonState(this.deleteButton, this.focusedRow != null);} afterLoad(selectedSampleName) { this.setDirtyState(true); this.displayCount(); if (selectedSampleName) this.selectSample(selectedSampleName);} findRow(sampleName) { for (const row of this.sampleList.rows) { if (row.cells[1].innerText == sampleName) return row;} return null;} sampleNameExists(sampleName) { for (const row of this.sampleList.rows) { if (row.cells[1].innerText.toLowerCase() == sampleName.toLowerCase()) return true;} return false;} selectSample(sampleName) { let row = this.findRow(sampleName); if (row) this.sampleList.focusRow(row);} verifyName(name) { if (Utils.isNullOrWhiteSpace(name)) { this.layout.errorBox('Name cannot be blank.'); return false;} if (this.sampleNameExists(name)) { this.layout.errorBox(`A sample named "${name}" already exists.`); return false;} return true;} OnSelectionChanged(e) { this.docViewer.loadDoc(this.nodeId, this.sampleList.focusedRow.dataset.value); this.setCommandStates();} OnToolbarClick(e) { if (e.target.hasAttribute('data-type')) return this.addItem(e.target.dataset.type); if (!Utils.isButtonEnabled(e.target)) return; switch (e.target.id) { case 'paste': return this.OnPaste(e); case 'rename': return this.OnRename(e); case 'delete': return this.OnDelete(e);}} OnPaste(e) { e.cancelBubble = true; let modal = this.layout.createModal('InputBox'); modal.requireValue = true; modal.showInputBox('Paste Sample Image', 'Sample Name', '', (childElement) => { let input = childElement.querySelector('input'); if (!this.verifyName(input.value)) return; let postData = { gridId: this.grid.gridId, propertyName: this.propertyRow.propertyPath, sampleName: input.value }; HtmlControl.loadPost(this.sampleListParent, '/PropertyGrid/SampleImageEditor/Paste', postData, () => this.afterLoad(input.value));});} OnRename(e) { e.cancelBubble = true; let modal = this.layout.createModal('InputBox'); modal.requireValue = true; modal.requireEdit = true; modal.showInputBox('Rename Sample Image', 'New Name', this.focusedSampleName, childElement => { let input = childElement.querySelector('input'); if (!this.verifyName(input.value)) return; let postData = { gridId: this.grid.gridId, propertyName: this.propertyRow.propertyPath, oldName: this.focusedSampleName, newName: input.value }; HtmlControl.loadPost(this.sampleListParent, '/PropertyGrid/SampleImageEditor/Rename', postData, () => this.afterLoad(input.value));});} OnDelete(e) { e.cancelBubble = true; this.layout.confirmBox(`Delete "${this.focusedSampleName}"?`, () => { let targetSampleName = this.nextSampleName ?? this.prevSampleName; let postData = { gridId: this.grid.gridId, propertyName: this.propertyRow.propertyPath, sampleName: this.focusedSampleName }; HtmlControl.loadPost(this.sampleListParent, '/PropertyGrid/SampleImageEditor/Delete', postData, () => this.afterLoad(targetSampleName));});} OnKeyDown(e) { this.triggerButtonShortcut(e);}}; class ZoneEditor extends PropertyGridEditor { constructor(element) { super(element); HtmlControl.initBranch(this.element); this.parentModal.setWidth('1250px'); this.parentModal.setHeight('800px'); this.docViewer.addListener(ImageViewer.imageLoadedEvent, () => this.DisplayZone()); this.docViewer.addListener(ImageViewer.selectionChangedEvent, () => this.OnZoneSelected()); this.addListener(PropertyGrid.editedEvent, () => this.DisplayZone()); this.viewerLink = new ViewerLink(this);} commitChange() { let srcGridId = this.zoneGridElement.id; let dstGridId = this.grid.element.id; let propName = this.propertyRow.propertyPath; this.grid.refreshProps(`/PropertyGrid/WriteProperty/${srcGridId}/${dstGridId}?propertyName=${propName}`, true);} get leftPanel() { return this.element.firstElementChild; } get zoneGridElement() { return this.leftPanel.firstElementChild; } get testSource() { return HtmlControl.get(this.leftPanel.lastElementChild); } get docViewer() { return HtmlControl.get(this.element.lastElementChild); } get imageViewer() { return this.docViewer.imageViewer; } get isLogical() { return this.dataset.type == 'LogicalRectangle'; } get left() { return this.zoneGridElement.querySelector("#Left, #LeftEdge").value; } get top() { return this.zoneGridElement.querySelector("#Top, #TopEdge").value; } get width() { return this.zoneGridElement.querySelector("#Width").value; } get height() { return this.zoneGridElement.querySelector("#Height").value; } get pageWidth() { return this.imageViewer?.logicalWidth; } get pageHeight() { return this.imageViewer?.logicalHeight; } get hasRect() { return !(this.left == '' || this.top == '' || this.width == '' || this.height == '') } OnZoneSelected() { let rawRect = this.imageViewer.logicalSelectionBounds.cloneRounded(2); let rect = (this.isLogical) ? rawRect.toLogicalRect : rawRect.toRectangleExF; let URL = Utils.mapPath('/PropertyGrid/WriteJson'); let postData = { dstGridId: this.zoneGridElement.id, json: JSON.stringify(rect) }; HTTP.post(URL, postData, data => { this.zoneGridElement.outerHTML = data; HtmlControl.initControl(this.zoneGridElement); this.setDirtyState(true);});} DisplayZone() { if (this.imageViewer && this.hasRect) { if (this.isLogical) { let URL = Utils.mapPath('/PropertyGrid/GetLogicalRectCoordinates'); HTTP.post(URL, { gridId: this.zoneGridElement.id, pageWidth: this.pageWidth, pageHeight: this.pageHeight }, data => { this.AfterDisplayZone(new Rectangle(data.left, data.top, data.width, data.height));});} else { this.AfterDisplayZone(new Rectangle(parseFloat(this.left), parseFloat(this.top), parseFloat(this.width), parseFloat(this.height)));}}} AfterDisplayZone(rect) { this.imageViewer.logicalSelectionBounds = rect; this.imageViewer.drawImage();}}; class Splitter extends HtmlControl { static minPosition = 0.1; static maxPosition = 0.9; static panelCollapseEvent = 'Splitter.panelCollapsed'; currentPosition; constructor(element) { super(element) this.element.setAttribute('role', 'separator'); this.element.setAttribute('tabindex', '0'); this.setDividerPosition(this.startupPosition); element.onclick = e => this.OnClick(e); element.onpointerdown = e => this.OnPointerDown(e); element.onpointerup = e => this.OnPointerUp(e); element.onpointermove = e => this.OnPointerMove(e); element.onkeydown = e => this.OnKeyDown(e); if (this.canCollapsePanel1) this.element.innerHTML = this.generateCollapseHandle(this.panel1Collapsed); else if (this.canCollapsePanel2) this.element.innerHTML = this.generateCollapseHandle(!this.panel2Collapsed);} get container() { return this.element.parentElement; } get panel1() { return this.element.previousElementSibling; } get panel2() { return this.element.nextElementSibling; } get isVertical() { return this.element.parentElement.matches('.vertical'); } get isSwapped() { return this.element.parentElement.matches('.swap'); } get startupPosition() { return this.positionMap.has(this.settingsKey) ? this.positionMap.get(this.settingsKey) : this.defaultPosition; } get settingsSuffix() { return Utils.isNullOrEmpty(this.element.id) ? '' : `-${this.element.id}`; } get settingsKey() { return this.parentControl.controlType + this.settingsSuffix; } get defaultPosition() { return Utils.isNullOrEmpty(this.dataset.default) ? 0.5 : parseFloat(this.dataset.default) / 100.0; } get collapseHandle() { return this.findChild('span'); } setDividerPosition(percent) { let pct = Utils.applyLimit(percent, Splitter.minPosition, Splitter.maxPosition); if (this.currentPosition == pct) return; else this.currentPosition = pct; let pctText = `${pct * 100}%`; this.positionMap.set(this.settingsKey, pct); this.panel1.style.flexBasis = pctText; this.element.setAttribute('aria-valuenow', (pct * 100).toString()); this.element.setAttribute('aria-valuetext', pctText); Splitter.resizeControls(this.panel1); Splitter.resizeControls(this.panel2);} get canCollapsePanel1() { return this.container.classList.contains('c1'); } get canCollapsePanel2() { return this.container.classList.contains('c2'); } get panel1Collapsed() { return this.panel1.matches('.d-none'); } set panel1Collapsed(value) { this.element.innerHTML = this.generateCollapseHandle(value); Utils.setClass(this.panel1, 'd-none', value); Utils.setClass(this.element, 'c1', value);} get panel2Collapsed() { return this.panel2.matches('.d-none'); } set panel2Collapsed(value) { this.element.innerHTML = this.generateCollapseHandle(!value); Utils.setClass(this.panel2, 'd-none', value); Utils.setClass(this.element, 'c2', value);} generateCollapseHandle(isCollapsed) { return '' + this.getCollapseChar(isCollapsed) + ''} getCollapseChar(isCollapsed) { if (this.isVertical) return isCollapsed ? '❱' : '❰'; else return isCollapsed ? '🡃' : '🡁';} map; get positionMap() { if (this.map == null) return this.map = this.loadMap(); return this.map;} loadMap() { if (Utils.isNullOrEmpty(this.settings.map)) return new Map(); return new Map(JSON.parse(this.settings.map));} saveMap() { if (this.map == null) return; this.settings.map = JSON.stringify(Array.from(this.map.entries())); this.saveSettings();} OnClick(e) { if (this.canCollapsePanel1 && (e.target.matches('span') || this.panel1Collapsed)) { this.panel1Collapsed = !this.panel1Collapsed; this.bubbleEvent(Splitter.panelCollapseEvent);} else if (this.canCollapsePanel2 && (e.target.matches('span') || this.panel2Collapsed)) { this.panel2Collapsed = !this.panel2Collapsed; this.bubbleEvent(Splitter.panelCollapseEvent);}} OnKeyDown(e) { switch (Utils.getKeyID(e)) { case 'ArrowLeft': if (this.isVertical) this.setDividerPosition(this.currentPosition - .01); break; case 'ArrowRight': if (this.isVertical) this.setDividerPosition(this.currentPosition + .01); break; case 'ArrowUp': if (!this.isVertical) this.setDividerPosition(this.currentPosition - .01); break; case 'ArrowDown': if (!this.isVertical) this.setDividerPosition(this.currentPosition + .01); break; case 'Control + ArrowLeft': if (this.isVertical && this.canCollapsePanel1 && !this.panel1Collapsed) this.collapseHandle?.click(); if (this.isVertical && this.canCollapsePanel2 && this.panel2Collapsed) this.collapseHandle?.click(); break; case 'Control + ArrowRight': if (this.isVertical && this.canCollapsePanel1 && this.panel1Collapsed) this.collapseHandle?.click(); if (this.isVertical && this.canCollapsePanel2 && !this.panel2Collapsed) this.collapseHandle?.click(); break;}} OnPointerDown(e) { if (!e.target.matches('span')) e.target.setPointerCapture(e.pointerId);} OnPointerUp(e) { e.target.releasePointerCapture(e.pointerId); this.saveMap();} OnPointerMove(e) { if (!e.target.hasPointerCapture(e.pointerId)) return; let movement = this.isVertical ? e.movementX : e.movementY; if (movement == 0) return; let bounds = this.element.parentElement.getBoundingClientRect(); if (this.isVertical) { let clientX = e.pageX - bounds.left; let position = clientX / bounds.width; if (this.isSwapped) position = 1 - position; this.setDividerPosition(position);} else { let clientY = e.pageY - bounds.top; let position = clientY / bounds.height; if (this.isSwapped) position = 1 - position; this.setDividerPosition(position);}} static resizeControls(root) { for (const element of root.querySelectorAll('[data-control]')) { let control = HtmlControl.get(element); if (control.OnResize) control.OnResize();}}}; class TabList extends HtmlControl { static buttonClickedEvent = "TabList:ButtonClicked"; constructor(element) { super(element); this.addListener('click', e => this.OnClick(e)); this.addListener('keydown', e => this.OnKeyDown(e));} get buttons() { return [...this.element.children]; } get buttonCount() { return this.element.children.length; } get selectedButton() { return this.findChild('button[aria-selected="true"]'); } set selectedButton(value) { if (this.selectedButton) TabList.setTabState(this.selectedButton, false); TabList.setTabState(value, true);} get selectedButtonIndex() { return this.buttons.indexOf(this.selectedButton); } set selectedButtonIndex(value) { this.selectedButton = this.element.children[value]; } get focusedButton() { return this.findChild('button:focus'); } get prevButton() { return this.selectedButton?.previousElementSibling ?? this.lastButton; } get nextButton() { return this.selectedButton?.nextElementSibling ?? this.firstButton; } get firstButton() { return this.buttonCount == 0 ? null : this.element.children[0]; } get lastButton() { return this.buttonCount == 0 ? null : this.element.children[this.buttonCount - 1]; } static setTabState(button, isActive) { Utils.setClass(button, 'active', isActive); button.setAttribute('aria-selected', isActive.toString());} OnClick(e) { let button = e.target.closest('button'); if (button == null) return; if (!Utils.isButtonEnabled(button) || button.matches('[aria-selected="true"]')) return; this.raiseEvent(TabList.buttonClickedEvent, { button: button });} OnKeyDown(e) { switch (Utils.getKeyID(e)) { case 'ArrowLeft': case 'ArrowUp': return this.prevButton?.focus(); case 'ArrowRight': case 'ArrowDown': return this.nextButton?.focus();}}}; class DragDropHandler { tree; constructor(tree) { this.tree = tree; tree.element.ondrop = e => this.OnDrop(e); tree.element.ondragenter = e => this.OnDragEnter(e); tree.element.ondragover = e => this.OnDragOver(e); tree.element.ondragleave = e => this.OnDragLeave(e); tree.element.ondragstart = e => this.OnDragStart(e);} get focusedId() { return this.tree.focusedId; } get selectedIds() { return this.tree.selectedIds; } get focusedItem() { return this.tree.focusedItem; } dropTargetTypes; OnDragStart(e) { this.dropTargetTypes = null; let node = TreeViewer.getClickedNode(e); if (node == null || !node.isSelected) return false; e.dataTransfer.setData('application/json', JSON.stringify(this.selectedIds)); let postData = { childIds: this.selectedIds }; HTTP.post('/TreeViewer/GetDropTargetTypes', postData, data => { this.dropTargetTypes = data;});} OnDrop(e) { e.preventDefault(); let node = this.getDropTarget(e); if (node == null) { this.dropTargetTypes = null; return;} let pos = this.getDropPosition(e, node); this.dropTargetTypes = null; node.item.classList.remove('drop-target', 'before', 'after'); if (this.isFileData(e.dataTransfer)) { this.dropFiles(e, node, pos)} else { this.dropNodes(e, node, pos);}} OnDragEnter(e) { this.OnDragOver(e);} OnDragOver(e) { e.preventDefault(); let node = this.getDropTarget(e); if (node == null) return; this.setDropClass(e, node);} OnDragLeave(e) { e.preventDefault(); let node = TreeViewer.getClickedNode(e); if (node == null) return; let targetNode = TreeViewer.getNodeFromElement(e.relatedTarget); if (targetNode?.nodeId == node.nodeId) return; node.item.classList.remove('drop-target', 'before', 'after');} isBatchesBranch(node) { let path = node.path; return path.startsWith('/Batches/Production') || path.startsWith('/Batches/Test');} isFileData(dataTransfer) { return dataTransfer.types.length == 1 && dataTransfer.types[0] == "Files";} getDropTarget(e) { let node = TreeViewer.getClickedNode(e); if (node == null) return null; if (this.isFileData(e.dataTransfer)) { return this.canDropFiles(e, node) ? node : null;} else if (this.dropTargetTypes) { return this.canDropNodes(e, node) ? node : null;} else { return null;}} getDropPosition(e, node) { let bounds = node.bounds; let topPct = (e.pageY - bounds.top) / bounds.height, bottomPct = 1 - topPct; let canDropBefore = this.canDropBefore(e, node); let canDropInside = this.canDropInside(e, node); let canDropAfter = this.canDropAfter(e, node); let beforeRange = this.getRangeSize(canDropBefore, canDropInside, canDropAfter); let afterRange = this.getRangeSize(canDropAfter, canDropInside, canDropBefore); if (beforeRange > 0 && topPct < beforeRange) return PastePosition.Before; if (afterRange > 0 && bottomPct < afterRange) return PastePosition.After; return PastePosition.Inside;} getRangeSize(canDrop, canDropInside, canDropOther) { return !canDrop ? 0 : (canDropInside ? .25 : (canDropOther ? .5 : 1));} setDropClass(e, node) { let pos = this.getDropPosition(e, node); node.item.classList.add('drop-target'); Utils.setClass(node.item, 'before', pos == PastePosition.Before); Utils.setClass(node.item, 'after', pos == PastePosition.After);} canDropNodes(e, node) { let pos = this.getDropPosition(e, node); if (this.selectedIds.includes(node.nodeId)) return false; //can't drop items onto themselves if (this.isBatchesBranch(this.focusedItem)) return this.isBatchesBranch(node) && node.type == 'Grooper.Folder'; switch (pos) { case PastePosition.Before: return this.canDropBefore(e, node); case PastePosition.After: return this.canDropAfter(e, node); default: return this.canDropInside(e, node);}} canDropInside(e, node) { if (this.isFileData(e.dataTransfer)) { return this.isFileDropTarget(node);} else { if (node.nodeId == this.focusedItem.parentNode?.nodeId) return false; //can't drop an item on its parent if (this.dropTargetTypes.includes(node.type)) return true; return node.type == 'Grooper.Folder';}} canDropBefore(e, node) { if (node.type == 'Grooper.Core.LocalResourcesFolder') return false; if (this.isFileData(e.dataTransfer)) { if (node.type != 'Grooper.Core.BatchFolder' && node.type != 'Grooper.Core.BatchPage') return false; return node.parentNode?.type == 'Grooper.Core.BatchFolder';} else { if (this.selectedIds.includes(node.prevNode?.nodeId)) return false; if (this.selectedIds.includes(node.parentNode?.nodeId)) return false; return node.isRoot ? false : this.dropTargetTypes.includes(node.parentNode.type)}} canDropAfter(e, node) { if (node.type == 'Grooper.Core.LocalResourcesFolder') return false; if (this.isFileData(e.dataTransfer)) { if (node.type != 'Grooper.Core.BatchFolder' && node.type != 'Grooper.Core.BatchPage') return false; return node.parentNode?.type == 'Grooper.Core.BatchFolder';} else { if (this.selectedIds.includes(node.nextNode?.nodeId)) return false; if (this.selectedIds.includes(node.parentNode?.nodeId)) return false; return node.isRoot ? false : this.dropTargetTypes.includes(node.parentNode.type)}} dropNodes(e, node, pos) { let json = e.dataTransfer.getData('application/json'); if (json == null) return; let childIds = JSON.parse(json); let postData = { parentId: node.nodeId, childIds: childIds, pos: pos }; let suffix = childIds.length == 1 ? '' : ` + ${childIds.length - 1} more`; let posText = pos == PastePosition.After ? 'after' : (pos == PastePosition.Before) ? 'before' : 'inside'; let message = `Move "${this.focusedItem.name}"${suffix} ${posText} "${node.name}"?`; this.tree.layout.confirmBox(message, () => { HTTP.post(this.tree.baseURL + '/Move', postData, data => this.tree.update(data));});} isFileDropTarget(node) { switch (node.type) { case "Grooper.Core.Batch": return true; case "Grooper.Core.BatchFolder": return true; case "Grooper.Core.LocalResourcesFolder": return true; case "Grooper.Project": return true; case "Grooper.Folder": return node.isDescendantOfType('Grooper.Project');}} canDropFiles(e, node) { switch (this.getDropPosition(e, node)) { case PastePosition.Inside: return this.isFileDropTarget(node); default: return !node.isRoot;}} dropFiles(e, node, pos) { let nodeIndex = -1, parentId = node.nodeId; switch (pos) { case PastePosition.Before: nodeIndex = node.nodeIndex; parentId = node.parentNode.nodeId; break; case PastePosition.After: nodeIndex = node.nodeIndex + 1; parentId = node.parentNode.nodeId; break;} let URL = Utils.mapPath(`/TreeViewer/AddFile?parentId=${parentId}&iconSize=${this.tree.iconSize}`); let uploader = new FileUploader(this.tree, e.dataTransfer.files, URL, nodeIndex); uploader.oncomplete = () => this.tree.bubbleEvent(TreeViewer.selectionChangedEvent); uploader.onupdate = data => this.tree.update(data, true); uploader.startUpload();}}; class FileUploader { owner; files; modal; URL; totalCount = 0; totalBytes = 0; uploadCount = 0; uploadBytes = 0; currentFileName = null; currentFileSize = 0; currentFileLoaded = 0; oncomplete; nodeIndex; firstUploadedId; constructor(owner, files, URL, nodeIndex) { this.owner = owner; this.files = [...files]; this.URL = URL; this.totalCount = this.files.length; this.totalBytes = this.totalFileSize; this.nodeIndex = nodeIndex; this.uploadCount = 0;} get totalFileSize() { return this.files.reduce((total, file) => total + file.size, 0); } get remainingBytes() { return this.totalBytes - this.uploadBytes + this.currentFileSize - this.currentFileLoaded; } get completedBytes() { return this.totalBytes - this.remainingBytes; } get fileStatus() { return `${this.uploadCount.toLocaleString()} / ${this.totalCount.toLocaleString()}`; } get byteStatus() { return `${this.formatByteCount(this.uploadBytes)} / ${this.formatByteCount(this.totalBytes)}`; } get fileProgress() { return `${this.formatByteCount(this.currentFileLoaded)} / ${this.formatByteCount(this.currentFileSize)}`; } get status() { let overallStatus = `${this.fileStatus} files complete
${this.byteStatus}`; if (this.currentFileName == null) return overallStatus; let fileProgress = this.currentFileLoaded >= this.currentFileSize ? 'Saving...' : 'Uploading...' + this.fileProgress; return `${overallStatus}

${this.currentFileName}
${fileProgress}`;} get toastStatus() { return `${this.uploadCount.toLocaleString()} files uploaded (${this.uploadBytes.toLocaleString()} bytes)`; } afterUpload(data) { this.modal.progress.value++; this.uploadCount++; this.modal.progressHTML = this.status; if (this.owner.update) this.owner.update(data, true); if (this.nodeIndex != null && this.nodeIndex != -1) this.nodeIndex++; setTimeout(() => this.uploadNext(), 10);} startUpload() { this.modal = this.owner.layout.showProgress('File Upload', this.status, () => { this.files = []; }); this.modal.progress.max = this.files.length; this.modal.progress.value = 0; this.uploadNext();} uploadNext() { if (this.files.length == 0) { this.modal.hide(); this.modal = null; this.owner.layout.showToast('File Upload Complete', this.toastStatus); if (this.oncomplete) this.oncomplete(); if (this.owner.loadMenu) this.owner.loadMenu();} else { this.sendFile(this.files.shift());}} sendFile(file) { this.currentFileName = file.name; this.currentFileSize = file.size; this.currentFileLoaded = 0; let postData = new FormData(); postData.append('file', file, file.name); let url = this.nodeIndex == null ? this.URL : `${this.URL}&nodeIndex=${this.nodeIndex}`; HTTP.postFiles(url, postData, { success: data => this.afterUpload(data), error: (xhr, errorInfo) => this.handleUploadError(xhr, errorInfo), progress: (e) => { this.uploadBytes += file.size; this.currentFileLoaded = e.loaded; this.modal.progressHTML = this.status;}});} uploadAll() { this.modal = this.owner.layout.showProgress('File Upload', this.status, () => { this.files = []; }); this.modal.progress.max = this.files.length; this.modal.progress.value = 0; this.sendFiles(this.files);} sendFiles() { let postData = new FormData(); for (const file of this.files) postData.append('file', file, file.name); let url = this.nodeIndex == null ? this.URL : `${this.URL}&nodeIndex=${this.nodeIndex}`; HTTP.postFiles(url, postData, { success: data => this.afterSendFiles(data), error: (xhr, errorInfo) => this.handleUploadError(xhr, errorInfo), progress: e => this.onSendFilesProgress(e)});} onSendFilesProgress(e) { this.uploadBytes = e.loaded; this.currentFileLoaded = e.loaded; this.modal.progressHTML = this.status; let fileIndex = this.getFileIndex(); this.uploadCount = fileIndex + 1; this.modal.progress.value = this.uploadCount; let file = this.files[fileIndex]; this.currentFileName = file.name; this.currentFileSize = file.size; this.currentFileLoaded = this.uploadBytes - this.getByteCount(fileIndex); this.modal.progressHTML = this.status;} getByteCount(fileIndex) { return this.files.slice(0, fileIndex).reduce((total, file) => total + file.size, 0);} getFileIndex() { let remaining = this.uploadBytes; for (let i = 0; i < this.files.length; i++) { if (this.files[i].length > remaining) return i; remaining -= this.files[i].length;} return this.files.length - 1;} afterSendFiles(data) { this.modal.progressHTML = this.status; if (this.owner.update) this.owner.update(data, true); if (this.nodeIndex != null && this.nodeIndex != -1) this.nodeIndex++; this.modal.hide(); this.owner.layout.showToast('File Upload Complete', this.toastStatus); if (this.oncomplete) this.oncomplete();} formatByteCount(bytes) { const KB = 1024, MB = KB * KB, GB = MB * KB; if (bytes < KB) { return bytes.toLocaleString() + ' bytes';} else if (bytes < MB) { return this.round(bytes / KB).toLocaleString() + ' KB';} else if (bytes < GB) { return this.round(bytes / MB).toLocaleString() + ' MB';} else { return this.round(bytes / GB).toLocaleString() + ' GB';}} round(value) { return Math.round(value * 100) / 100;} handleUploadError(xhr, errorInfo) { HTTP.handleError(xhr, errorInfo); this.modal.hide();}}; class NodeChangeSet { added = []; moved = []; deleted = []; reordered = []; edited = []; newFocusedNode; focusTarget; childEdited; constructor(nodes, focusedNode) { this.childEdited = false; for (const node of nodes) this.addNode(node, node.nodeId == focusedNode.nodeId); if (this.refocus) { this.focusTarget = this.getNodeToFocus(focusedNode);} else { for (const node of nodes) { if (node.parentId == focusedNode.nodeId) this.childEdited = true;}}} get focusChange() { return (this.newFocusedNode == null) ? ChangeKinds.None : this.newFocusedNode.change; } get refocus() { return (this.focusChange == ChangeKinds.Deleted) || (this.focusChange == ChangeKinds.Moved); } get redisplay() { return (this.focusChange == ChangeKinds.Saved) || this.childEdited; } get focusedNodeReordered() { return this.focusChange == ChangeKinds.Reordered; } get isSingleAdd() { return this.added.length == 1;} getNodeToFocus(focusedNode) { let node = focusedNode.nextNode; while (node != null) { if (this.isFocusable(node)) return node.nodeId; node = node.nextNode;} node = focusedNode.prevNode; while (node != null) { if (this.isFocusable(node)) return node.nodeId; node = node.prevNode;} return focusedNode.parentNode?.nodeId;} arrayContains(array, node) { for (const refNode of array) { if (refNode.nodeId == node.nodeId) return true;} return false;} isFocusable(node) { return !this.arrayContains(this.deleted, node) && !this.arrayContains(this.moved, node);} addNode(node, isFocused) { if (isFocused) this.newFocusedNode = node; switch (node.change) { case ChangeKinds.Added: this.added.push(node); break; case ChangeKinds.Reordered: this.reordered.push(node); break; case ChangeKinds.Edited: this.edited.push(node); break; case ChangeKinds.Moved: this.moved.push(node); break; case ChangeKinds.Saved: this.edited.push(node); break; case ChangeKinds.Deleted: this.deleted.push(node); break;}}}; class TreeNode { item; constructor(child) { this.item = child.closest('li.tv-node');} get tree() { return HtmlControl.get(this.item); } get nodeId() { return this.item.id; } get parentId() { return this.dataset.parent; } get nodeIndex() { return parseInt(this.dataset.index); } get dataset() { return this.item.dataset; } get type() { return this.dataset.type; } get change() { return parseInt(this.dataset.change); } get nameElement() { return this.item.querySelector('span.caption'); } get name() { return this.nameElement.innerText; } set name(value) { this.nameElement.innerText = value; } get innerItem() { return this.item.firstElementChild; } get attachmentName() { return this.innerItem.querySelector('.attachment > div')?.innerText ?? ''; } get image() { return this.item.querySelector('.tv-item > img'); } get isExpandable() { return this.item.matches('li.tv-open, li.tv-closed'); } get isExpanded() { return this.item.matches('.tv-open'); } get isLoaded() { return this.item.hasAttribute('data-loaded'); } get isFixedContent() { return this.item.hasAttribute('data-fixed'); } get isSystem() { return this.item.hasAttribute('data-system'); } get isRoot() { return this.item.parentElement?.hasAttribute('data-root') ?? true; } get parentNodeIsVisible() { return this.parentNode?.isGrooperNode ?? false; } get isGrooperNode() { return this.nodeId != Constants.emptyGuid; } get isFolder() { return this.type == 'Grooper.Core.BatchFolder' || this.isBatch; } get isBatch() { return this.type == 'Grooper.Core.Batch'; } get isDocument() { return this.isGrooperNode && (!this.isBatch || this.hasContentType); } get isSelected() { return this.item.matches('.selected'); } set isSelected(value) { Utils.setClass(this.item, 'selected', value); this.item.setAttribute('aria-selected', value.toString());} get isFocused() { return this.item.matches('.focused'); } set isFocused(value) { Utils.setClass(this.item, 'focused', value);} get isDisabled() { return this.item.matches('.disabled'); } get hasContentType() { return this.item.hasAttribute('data-hasct'); } get checkbox() { return this.innerItem.querySelector('input[type="checkbox"]'); } get isChecked() { return this.checkbox.checked; } get parentUL() { return this.item.parentElement; } get parentLI() { return this.parentUL.parentElement.tagName == 'LI' ? this.parentUL.parentElement : null; } get parentNode() { return this.isRoot || this.parentLI == null ? null : new TreeNode(this.parentLI); } get parentTreeNode() { return this.parentLI ? new TreeNode(this.parentLI) : null; } get nextNode() { return TreeNode.isNode(this.item.nextElementSibling) ? new TreeNode(this.item.nextElementSibling) : null; } get prevNode() { return TreeNode.isNode(this.item.previousElementSibling) ? new TreeNode(this.item.previousElementSibling) : null; } get rootLevel() { return this.dataset.level ? parseInt(this.dataset.level) : 0; } get level() { return this.isRoot ? this.rootLevel : this.parentNode.level + 1; } get path() { return this.isRoot ? '/' : (this.parentNode.isRoot ? `/${this.name}` : `${this.parentNode.path}/${this.name}`); } get hasChildren() { return this.isExpandable && (this.childCount > 0); } get childCount() { return parseInt(this.dataset.count); } set childCount(value) { this.dataset.count = value; } get hasVisibleChildren() { return this.hasChildren && this.isExpanded; } get childElements() { return this.childList.children; } get childList() { return this.item.lastElementChild; } get firstChild() { return (!this.hasChildren) ? null : new TreeNode(this.childElements[0]); } get lastChild() { return (!this.hasChildren) ? null : new TreeNode(this.childElements[this.childElements.length - 1]); } get firstVisible() { return this.hasVisibleChildren ? this.firstChild : null; } get lastVisible() { return this.hasVisibleChildren ? this.lastChild : null; } get downNode() { return (this.firstVisible != null) ? this.firstVisible : this.nextNode; } get bounds() { return this.innerItem.getBoundingClientRect(); } get isVisible() { let myBounds = this.bounds, parentBounds = this.tree.clientBounds; return (myBounds.top >= parentBounds.top) && (myBounds.bottom <= parentBounds.bottom);} get isShown() { if (this.isRoot) return true; return this.parentNode.isExpanded && this.parentNode.isShown;} static isNode(element) { return element ? element.matches('.tv-node') : false; } static isMatch(node, searchText, enabledOnly) { if (enabledOnly && node.isDisabled) return false; return node.name.toLowerCase().includes(searchText);} getFirstMatch(searchText, enabledOnly) { if (!this.isExpandable) return null; for (const childElement of this.childElements) { let child = new TreeNode(childElement); if (TreeNode.isMatch(child, searchText, enabledOnly)) return child; if (child.isExpandable) { let grandChild = child.getFirstMatch(searchText); if (grandChild != null) return grandChild;}} return null;} changeIndex(index) { if (index == this.nodeIndex) return; this.dataset.index = index; if (this.isFolder) { this.name = this.name.replace(/\(\d+\)$/, `(${index + 1})`);} else { this.name = `Page ${index + 1}`;}} remove() { if (this.parentNode) this.parentNode.childCount--; this.item.remove();} updateFrom(node) { let isFocused = this.isFocused, isSelected = this.isSelected, isLoaded = this.isLoaded, isExpanded = this.isExpanded; let shouldRefresh = node.change == ChangeKinds.Saved || this.innerItem.innerHTML != node.innerItem.innerHTML; let level = this.dataset.level; Utils.copyAttributes(node.item, this.item); this.isFocused = isFocused; this.isSelected = isSelected; if (level != null) this.dataset.level = level; if (isLoaded) this.item.setAttribute('data-loaded', null); if (shouldRefresh) this.innerItem.innerHTML = node.innerItem.innerHTML; this.innerItem.title = node.innerItem.title; if (this.hasChildren) { Utils.setClass(this.item, 'tv-open', isExpanded); Utils.setClass(this.item, 'tv-closed', !isExpanded);} else { this.item.classList.remove('tv-open', 'tv-closed');}} getClickPart(e) { let rect = this.innerItem.getBoundingClientRect(); if ((e.pageY < rect.top) || (e.pageY > rect.bottom)) return null; if ((e.pageX < rect.left) && this.isExpandable) return 'marker'; return 'li';} toggleExpanded() { if (this.isExpanded) this.collapse(); else this.expand();} collapse() { if (!this.isExpanded) return; this.item.classList.remove('tv-open'); this.item.classList.add('tv-closed'); this.item.setAttribute('aria-expanded', false); if (this.tree.focusedItem && this.tree.focusedItem.isDescendantOf(this.nodeId)) this.focus(true, true, true);} expand(oncomplete = null) { if (this.isExpandable && !this.isExpanded) { this.item.classList.remove('tv-closed'); this.item.classList.add('tv-open'); this.item.setAttribute('aria-expanded', true); if (!this.isLoaded) return this.loadChildren(oncomplete);} if (oncomplete) oncomplete();} ensureVisible() { if (!this.isVisible) this.innerItem.scrollIntoView({ block: "center" });} select() { this.isSelected = true;} unSelect() { this.isSelected = false; this.isFocused = false;} focus(unselect, register, updateMenu) { this.tree.clearFocus(unselect); this.isFocused = true; this.isSelected = true; this.ensureVisible(); this.tree.focusNode(this, register, updateMenu);} insertChild(node) { let childList = this.childList; let existing = childList.children[node.nodeIndex]; if (childList.children.length == 0) this.item.classList.add('tv-open'); if (existing != null) { childList.insertBefore(node.item, existing);} else { childList.append(node.item);} this.childCount++;} *getAncestors() { let ancestor = this.parentNode; while (ancestor) { yield ancestor; ancestor = ancestor.parentNode;}} isDescendantOf(nodeId) { for (const ancestor of this.getAncestors()) { if (ancestor.nodeId == nodeId) return true;} return false;} isDescendantOfType(typeName) { for (const ancestor of this.getAncestors()) { if (ancestor.type == typeName) return true;} return false;} *getChildren() { if (!this.isExpandable) return; for (const child of this.childElements) yield new TreeNode(child);} *getDescendants() { if (!this.isExpandable) return; for (const child of this.item.querySelectorAll('.tv-node')) yield new TreeNode(child);} *getDescendantIds() { if (this.isExpandable) { for (const child of this.item.querySelectorAll('.tv-node')) yield child.id;}} loadChildren(oncomplete = null) { let postData = { parentId: this.nodeId }; HTTP.post(this.tree.renderChildrenURL, postData, data => { this.childList.innerHTML = data; this.item.setAttribute('data-loaded', null); this.tree.mapChildren(this); if (oncomplete) oncomplete();});} unloadChildren() { if (!this.isFolder || !this.isLoaded) return 0; let ids = [...this.getDescendantIds()]; for (const id of ids) this.tree.removeMapping(id); this.item.classList.remove('tv-open'); this.item.classList.add('tv-closed'); this.item.setAttribute('aria-expanded', false); this.childList.innerHTML = ''; this.item.removeAttribute('data-loaded'); return ids.length;} HandleNavKey(e) { if (Utils.isModifierKey(e)) return false; let key = Utils.getKeyID(e); switch (key) { case 'ArrowUp': this.ArrowUp(); return true; case 'ArrowDown': this.ArrowDown(); return true; case 'ArrowLeft': this.ArrowLeft(); return true; case 'ArrowRight': this.ArrowRight(); return true; case 'Home': this.HomeKey(); return true; case 'End': this.EndKey(); return true; case 'Shift + ArrowUp': this.ShiftArrowUp(); return true; case 'Shift + ArrowDown': this.ShiftArrowDown(); return true; case 'Shift + Home': this.ShiftHomeKey(); return true; case 'Shift + End': this.ShiftEndKey(); return true; case 'Control + A': this.SelectAll(); return true; case ' ': if (this.checkbox) { this.ToggleCheckbox(); return true; } default: return false;}} SelectAll() { if (this.parentTreeNode == null) return; let tree = this.tree; for (const child of this.parentTreeNode.getChildren()) { if (tree.canMultiSelect(this, child)) { child.select(); tree.selectNode(child);}} this.tree.registerFocus(true);} HomeKey() { if ((this.parentTreeNode == null) || (this.parentTreeNode.firstVisible == this)) return; this.parentTreeNode.firstVisible.focus(true, false, false);} EndKey() { if ((this.parentTreeNode == null) || (this.parentTreeNode.lastVisible == this)) return; this.parentTreeNode.lastVisible.focus(true, false, false);} ArrowLeft() { switch (true) { case this.isExpanded: this.toggleExpanded(); break; case (this.parentTreeNode != null): this.parentTreeNode.focus(true, false, false); break;}} ArrowRight() { switch (true) { case !this.isExpandable: return this.ArrowDown(); case !this.isExpanded: this.toggleExpanded(); break; case (this.firstChild != null): this.firstChild.focus(true, false, false); break;}} ArrowUp() { if (this.prevNode == null) { if (this.parentTreeNode != null) { this.parentTreeNode.focus(true, false, false); } return;} var ret = this.prevNode; while (ret.hasVisibleChildren) { ret = ret.lastVisible; } ret.focus(true, false, false);} ArrowDown() { if (this.downNode != null) { this.downNode.focus(true, false, false); return; } var ancestor = this; while ((ancestor != null) && !ancestor.isRoot) { if (ancestor.nextNode != null) { ancestor.nextNode.focus(true, false, false); return; } ancestor = ancestor.parentTreeNode;}} ShiftArrowUp() { if (!this.tree.canMultiSelect(this, this.prevNode)) return; let alreadySelected = this.prevNode.isSelected; this.prevNode.focus(false, false, false); if (alreadySelected) this.unSelect();} ShiftArrowDown() { if (!this.tree.canMultiSelect(this, this.nextNode)) return; let alreadySelected = this.nextNode.isSelected; this.nextNode.focus(false, false, false); if (alreadySelected) this.unSelect();} ShiftHomeKey() { if (this.selectMode == SelectModes.Single) return; if ((this.parentTreeNode == null) || (this.parentTreeNode.firstVisible == this)) return; let viewer = this.tree, item = this.prevNode, typeName = this.type; let itemToFocus = null; while (viewer.canMultiSelect(this, item)) { itemToFocus = item; item.select(); viewer.selectNode(item); item = item.prevNode;} if (itemToFocus) itemToFocus.focus(false, false, false);} ShiftEndKey() { if (this.selectMode == SelectModes.Single) return; if ((this.parentTreeNode == null) || (this.parentTreeNode.lastVisible == this)) return; var viewer = this.tree, item = this.nextNode, typeName = this.type; let itemToFocus = null; while (viewer.canMultiSelect(this, item)) { itemToFocus = item; item.select(); viewer.selectNode(item); item = item.nextNode;} if (itemToFocus) itemToFocus.focus(false, false, false);} ToggleCheckbox() { let cb = this.checkbox; cb.checked = !this.checkbox.checked; cb.dispatchEvent(new CustomEvent('change', { 'bubbles': true }));} ControlClick() { let refNode = this.tree.firstSelection; if (!this.tree.canMultiSelect(refNode, this)) return; if (this.isFocused) { if (this.tree.selectedIds.length < 2) return; this.tree.selectedIds = this.tree.selectedIds.filter(id => id != this.nodeId); this.unSelect(); this.tree.firstSelection.focus(false, true, true);} else if (this.isSelected) { this.tree.selectedIds = this.tree.selectedIds.filter(id => id != this.nodeId); this.isSelected = false;} else { this.focus(false, true, true);}} ShiftClick() { if (this.isFocused) return; let refItem = this.tree.firstSelection; if (this.parentTreeNode == null || refItem.parentTreeNode == null) return false; if (this.parentTreeNode.nodeId != refItem.parentTreeNode.nodeId) return false; let startIdx = -1, endIdx = -1, children = [...this.parentTreeNode.getChildren()]; for (var idx = 0; idx < children.length; idx++) { let child = children[idx]; if ((child.nodeId == this.nodeId) || child.isSelected) { if (startIdx == -1) { startIdx = idx; } else { endIdx = idx; }}} let type = this.type, viewer = this.tree; for (let idx = startIdx; idx <= endIdx; idx++) { if (!viewer.canMultiSelect(refItem, children[idx])) return false;} this.tree.clearFocus(false); let selection = []; for (var idx = startIdx; idx <= endIdx; idx++) { children[idx].isSelected = true; selection.push(children[idx].nodeId); } this.tree.selectedIds = selection; this.focus(false, true, true); return true;} static getLoadedFolderIds(folder, context) { if (folder.isLoaded) { for (const child of folder.getChildren()) { if (child.isFolder) this.getLoadedFolderIds(child, context);} if (context.nodeCount > 0 && folder.tree.focusedId != folder.nodeId && !folder.tree.focusedItem.isDescendantOf(folder.nodeId)) { context.nodeCount -= folder.childCount; context.ids.push(folder.nodeId);}}} static getCollapsedFolderIds(folder, ids) { if (folder.isExpanded) { for (const child of folder.getChildren()) { if (child.isFolder) this.getCollapsedFolderIds(child, ids);}} else if (folder.isLoaded) ids.push(folder.nodeId);}} ; class TreeViewer extends HtmlControl { focusedId; selectedIds = []; nodeMap = new Map(); treeMap = new Map(); renderChildrenURL; disabledMessage; dragDropHandler; static selectionChangedEvent = 'TreeViewer.SelectionChanged'; static changesSavedEvent = 'TreeViewer.ChangesSaved'; static nodeClickedEvent = 'TreeViewer.NodeClicked'; constructor(element) { super(element); this.renderChildrenURL = Utils.isNullOrEmpty(this.dataset.children) ? this.baseURL + '/RenderChildren' : this.dataset.children; this.addListener('change', e => this.OnChange(e)); this.addListener('click', e => this.OnClick(e)); this.addListener('dblclick', e => this.OnDblClick(e)); this.addListener('contextmenu', e => this.OnContextMenu(e)); this.addListener('keydown', e => this.OnKeyDown(e)); this.addListener('keyup', e => this.OnKeyUp(e)); this.addListener('scroll', e => { if (this.menu) this.menu.hide(); }); if (this.menu) { this.dragDropHandler = new DragDropHandler(this); this.menu.addListener(ContextMenu.executeEvent, e => this.OnCommandClick(e.detail)); this.menu.addListener(ContextMenu.closedEvent, e => this.OnMenuClosed(e));} this.buildMap(); let itemToFocus = this.focusedItem ? this.focusedItem : this.firstRoot; if (itemToFocus) itemToFocus.focus(true, true, true); TreeViewer.inKeyDown = false;} get isDisabled() { return this.element.matches('.disabled'); } get focusedItem() { return (this.focusedId == null) ? null : this.getNode(this.focusedId); } get hasMenu() { return this.element.lastElementChild.dataset.control == 'ContextMenu'; } get menuElement() { return this.hasMenu ? this.element.lastElementChild : null; } get menu() { return HtmlControl.get(this.menuElement); } get menuFilter() { return this.rootIsFocused ? this.rootFilter : this.nonRootFilter; } get nonRootFilter() { return this.iconSize == 64 ? 'GrooperReview.Controls.TreeViewer.IsNonRootCommandEnabled64' : null; } get rootFilter() { return this.iconSize == 64 ? 'GrooperReview.Controls.TreeViewer.IsRootCommandEnabled64' : 'GrooperReview.Controls.TreeViewer.IsRootCommandEnabled'; } get firstSelection() { return (this.selectedIds.length == 0) ? null : this.selectedItems().next().value; } get rootList() { return this.element.firstElementChild; } get selectMode() { return parseInt(this.dataset.select); } get firstRoot() { return new TreeNode(this.rootList.firstElementChild); } get rootIsFocused() { return this.focusedItem == null ? false : !this.focusedItem.parentNodeIsVisible; } get isSearchResults() { return !this.firstRoot.isGrooperNode; } get iconSize() { return parseInt(this.dataset.iconsize); } get baseURL() { return '/TreeViewer/' + this.iconSize; } get hierarchyMode() { return this.element.hasAttribute('data-hierarchy-mode'); } get currentFolder() { let node = this.focusedItem; if ((node == null) || (node.isRoot)) return node; return node.isFolder ? node : node.parentNode;} get lastFolder() { const nodes = this.element.querySelectorAll('.tv-node'); const folders = Array.from(nodes).filter(n => n.dataset.type === 'Grooper.Core.BatchFolder'); return folders.length == 0 ? null : new TreeNode(folders[folders.length - 1]);} get firstEnabledNode() { for (const root of this.rootNodes) { let ret = root.firstEnabledDescendant; if (ret) return ret;} return null;} *rootNodes() { for (const child of this.rootList.children) { if (TreeNode.isNode(child)) yield new TreeNode(child);}} *allNodes() { for (const root of this.rootNodes()) { yield root; for (const descendant of root.getDescendants()) yield descendant;}} *selectedItems() { for (const id of this.selectedIds) { let node = this.getNode(id); if (node) yield node;}} *getCheckedNodeIds() { let checkedBoxes = this.element.querySelectorAll('input[type="checkbox"]'); for (const box of checkedBoxes) { if (box.checked) { let node = new TreeNode(box); yield node.nodeId;}}} enable() { if (this.menu) this.menu.enableAll(); this.element.classList.remove('disabled', 'dimmed'); this.disabledMessage = null;} disable(dimmed = true, disabledMessage = null) { this.disabledMessage = disabledMessage; if (this.menu) this.menu.disableAll(); this.element.classList.add('disabled'); if (dimmed) this.element.classList.add('dimmed');} static getClickedExpander(e) { let item = e.target.closest('.tv-node'); if (item == null) return null; let node = new TreeNode(item); return node.getClickPart(e) == 'marker' ? node : null;} static getClickedNode(e) { return TreeViewer.getNodeFromElement(e.target);} static getNodeFromElement(element) { if (element == null) return null; let item = element.closest('.tv-item'); return item ? new TreeNode(item.parentElement) : null;} getFirstMatch(searchText, enabledOnly) { for (const root of this.rootNodes()) { let ret = root.getFirstMatch(searchText, enabledOnly); if (ret) return ret;} return null;} replaceRoots(data, searchText) { this.rootList.innerHTML = data; this.buildMap(); if (!Utils.isNullOrWhiteSpace(searchText)) { let itemToFocus = this.getFirstMatch(searchText, true); if (itemToFocus == null) itemToFocus = this.getFirstMatch(searchText, false); if (itemToFocus == null) itemToFocus = this.root; this.focusedId = null; this.selectedIds = []; itemToFocus.focus(true, true, true);}} saveProperties() { HTTP.post(this.baseURL + '/SaveProperties?gridId=NodeProperties', null, data => this.update(data));} broadcastCheckState(node) { if (!node.isExpandable) return; if (!node.isExpanded) { node.expand(() => { this.broadcastToChildren(node);});} else { this.broadcastToChildren(node);}} broadcastToChildren(node) { let isChecked = node.isChecked; for (const child of node.getChildren()) { child.checkbox.checked = isChecked; child.item.setAttribute('aria-checked', isChecked.toString()); this.broadcastCheckState(child);}} applyHierarchyMode(node) { if (node.checkbox.checked) { let ancestor = node.parentNode; while (ancestor != null) { if (!ancestor.checkbox.checked) { ancestor.checkbox.checked = true; ancestor.item.setAttribute('aria-checked', true);} ancestor = ancestor.parentNode;}} else { this.broadcastCheckState(node);}} loadMenu(oncomplete = null) { if (this.menu) { this.menu.loadNodeCommands(this.selectedIds, this.controlTypes, this.menuFilter, oncomplete);} else if (oncomplete) { oncomplete();}} registerFocus(updateMenu) { if (this.menu) { this.menu.clear(); if (updateMenu) return this.loadMenu(() => this.bubbleEvent(TreeViewer.selectionChangedEvent));} this.bubbleEvent(TreeViewer.selectionChangedEvent);} focusNode(node, register, updateMenu) { this.selectNode(node); this.focusedId = node.nodeId; if (register) this.registerFocus(updateMenu);} selectNode(node) { if (!this.selectedIds.includes(node.nodeId)) this.selectedIds.push(node.nodeId);} clearFocus(unselect) { for (const node of this.selectedItems()) { node.isFocused = false; if (unselect) node.isSelected = false;} if (unselect) this.selectedIds = [];} canMultiSelect(curNode, newNode) { if ((curNode == null) || (newNode == null)) return false; switch (this.selectMode) { case SelectModes.Single: return false; case SelectModes.Multi: return (curNode.parentNode?.nodeId == newNode.parentNode?.nodeId); case SelectModes.SingleType: return (curNode.parentNode?.nodeId == newNode.parentNode?.nodeId) && (newNode.type == curNode.type);}} collapseAll(nodes) { for (const node of nodes) node.collapse();} expandAll(nodes) { if (nodes.length == 0) return; let node = nodes[0]; node.expand(() => this.expandAll(nodes.splice(1)));} toggleExpanded(node) { let items = node.isSelected ? [...this.selectedItems()] : [node]; if (node.isExpanded) this.collapseAll(items); else this.expandAll(items);} update(data, quietMode = false) { let nodes = [], items = HTML.parseList(data);; for (const item of items) nodes.push(new TreeNode(item)); let changeSet = new NodeChangeSet(nodes, this.focusedItem); changeSet.added.forEach(node => this.applyAdd(node, quietMode, changeSet.isSingleAdd)); changeSet.moved.forEach(node => this.applyMove(node)); changeSet.deleted.forEach(node => this.applyDelete(node)); this.mapTree(changeSet.reordered); changeSet.reordered.forEach(node => this.applyReorder(node)); changeSet.edited.forEach(node => this.applyEdit(node)); if (!quietMode) this.afterUpdate(changeSet); return changeSet;} applyAdd(node, quietMode, isSingleAdd) { let parent = this.getNode(node.parentId); if ((parent == null) || !parent.isLoaded) return; parent.insertChild(node); this.setMapping(node); if (!quietMode && isSingleAdd) node.focus(true, true, true);} applyMove(node) { let newParent = this.getNode(node.parentId), oldNode = this.getNode(node.nodeId); let shouldInsert = (newParent != null) && newParent.isLoaded; if (shouldInsert && (oldNode == null)) { newParent.insertChild(node); this.updateMapping(node);} else if (shouldInsert && (oldNode != null)) { oldNode.updateFrom(node); newParent.insertChild(oldNode); this.updateMapping(oldNode);} else if (oldNode != null) { oldNode.remove(); this.removeMapping(oldNode.nodeId);}} applyDelete(node) { let oldNode = this.getNode(node.nodeId); if (oldNode) { oldNode.remove(); this.removeMapping(oldNode.nodeId);}} applyReorder(node) { let oldNode = this.getNode(node.nodeId); if (oldNode == null) return; let parentNode = oldNode.parentNode; let shouldMove = (this.getTreeNode(node.nodeId) != oldNode.nodeIndex); oldNode.updateFrom(node); if (shouldMove) parentNode.insertChild(oldNode); this.updateMapping(oldNode);} applyEdit(node) { let oldNode = this.getNode(node.nodeId); if (oldNode == null) return; if (oldNode.item.outerHTML != node.item.outerHTML) oldNode.updateFrom(node);} afterUpdate(changeSet) { if (changeSet.isSingleAdd) { this.selectAddedNode(changeSet.added[0]); this.bubbleEvent(TreeViewer.changesSavedEvent, changeSet);} else if (changeSet.refocus) { this.updateFocus(changeSet.focusTarget); this.bubbleEvent(TreeViewer.changesSavedEvent, changeSet);} else { if (changeSet.focusedNodeReordered) this.focusedItem?.ensureVisible(); this.loadMenu(() => this.bubbleEvent(TreeViewer.changesSavedEvent, changeSet));} this.element.focus();} updateFocus(focusTarget) { if (this.focusedItem) { if (this.focusedItem.isShown) return this.focusedItem.ensureVisible(); this.focusedItem.unSelect();} let target = focusTarget ? this.getNode(focusTarget) : this.firstRoot; target.focus(false, true, true);} selectAddedNode(addedNode) { if (addedNode.parentId == this.focusedId && this.focusedItem.isExpandable && !this.focusedItem.isExpanded) { this.focusedItem.expand(() => { this.getNode(addedNode.nodeId)?.focus(true, true, true);});} else { this.getNode(addedNode.nodeId)?.focus(true, true, true);}} appendContent(content) { let newItem = HTML.parseElement(content); let newNode = new TreeNode(newItem); if (!newNode.isGrooperNode) return; let parent = this.getNode(newNode.parentId); parent.insertChild(newNode); this.mapBranch(newNode); let descendants = newItem.querySelectorAll('.tv-node'); let newSelection = (descendants.length == 0) ? null : descendants[descendants.length - 1]; let nodeToSelect = newSelection ? new TreeNode(newSelection) : newNode; nodeToSelect.focus(true, false, false);} renumberBelowFocus() { let index = this.focusedItem.nodeIndex + 1; let next = this.focusedItem.nextNode; while (next) { next.changeIndex(index); index++; next = next.nextNode;}} focusNext(applies, propertyName, inverse) { if (!this.focusNextItem(applies)) { let postData = { baseFolderId: this.firstRoot.nodeId, focusedId: this.focusedId, propertyName: propertyName, inverse: inverse }; HTTP.post('/TreeViewer/GetNextItem', postData, data => this.afterGetNext(data));}} afterGetNext(ids) { let ancestor = (ids.length > 0) ? this.getLastNode(ids, [], this.firstRoot) : null; if (ancestor) { this.clearFocus(true); HTTP.post('/TreeViewer/RenderBranch', { folderNodeId: ancestor.nodeId, focusedId: ids[0] }, data => { let newNode = new TreeNode(HTML.parseElement(data)); ancestor.parentNode.insertChild(newNode); ancestor.remove(); this.mapBranch(newNode); this.focusedItem.focus(true, true, true); TreeViewerUtils.collapseFolders(this);});}} focusNextItem(applies) { let next = this.getNextItem(applies); if (next) { let parent = next.parentNode; while (parent) { parent.expand(); parent = parent.parentNode;} next.focus(true, true, true); return true;} return false;} getNextItem(applies) { let first = null; let focusedEncountered = false; let okToReturnFirst = true; for (const node of this.allNodes()) { let isHit = applies(node); let isFocused = node.nodeId == this.focusedId; let isUnloadedFolder = (node.isFolder && !node.isLoaded); if (isHit && !first) first = node; if (isFocused && isUnloadedFolder) return null; else if (isFocused) focusedEncountered = true; else if (focusedEncountered && isHit) return node; else if (isUnloadedFolder && focusedEncountered) return null; else if (isUnloadedFolder && !focusedEncountered && !first) okToReturnFirst = false;} return okToReturnFirst ? first : null;} getLastNode(ids, path, element) { if (ids.includes(element.nodeId)) path.push(element); for (const ele of element.getDescendants()) { if (ids.includes(ele.nodeId)) { this.getLastNode(ids, path, ele); break;}} return path.at(-1);} focusTimer; StartFocusTimer(docId) { if (this.focusTimer != null) clearTimeout(this.focusTimer); this.focusTimer = setTimeout(() => this.OnFocusTimerTick(), 150);} OnFocusTimerTick() { clearTimeout(this.focusTimer); this.focusTimer = null; this.registerFocus(true);} buildMap() { this.nodeMap.clear(); for (const rootNode of this.rootNodes()) this.mapBranch(rootNode);} getNode(nodeId) { return this.nodeMap.has(nodeId) ? new TreeNode(this.nodeMap.get(nodeId)) : null;} updateMapping(node) { this.nodeMap.set(node.nodeId, node.item);} setMapping(node) { this.updateMapping(node); if (node.isFocused) { this.focusedId = node.nodeId; this.selectedIds = [node.nodeId];}} removeMapping(nodeId) { this.nodeMap.delete(nodeId); if (this.focusedId == nodeId) this.focusedId = null; if (this.selectedIds.includes(nodeId)) { this.selectedIds = this.selectedIds.filter(item => item != nodeId);}} mapChildren(node) { if (!node.isExpandable || !node.isLoaded) return; for (const child of node.getChildren()) { this.setMapping(child);}} mapBranch(node) { this.setMapping(node); if (!node.isExpandable || !node.isLoaded) return; for (const child of node.getChildren()) { this.mapBranch(child);}} mapTree(items) { let parentIds = []; for (const item of items) { if (!parentIds.includes(item.parentId)) parentIds.push(item.parentId);} for (const parentId of parentIds) { let parentNode = this.getNode(parentId); if (parentNode) { let index = 0; for (const element of parentNode.childElements) { this.treeMap.set(element.nodeId, index); index++;}}}} getTreeNode(nodeId) { return this.treeMap.has(nodeId) ? this.treeMap.get(nodeId) : null;} OnCommandClick(clickedItem) { switch (clickedItem.mode) { case 'add': return this.OnAddCommandClick(clickedItem); case 'auto': return this.executeServerCommand(clickedItem); case 'js': return this.OnJsCommandClick(clickedItem); case 'modal': return this.showCommandProperties(clickedItem, Utils.fetchObjectData(clickedItem.command));}} OnJsCommandClick(clickedItem) { switch (clickedItem.command) { case 'ViewProperties': return this.OnViewProperties() case 'ValidateBranch': return this.OnValidateBranch(clickedItem); case 'GotoFlagged': return this.OnGotoFlagged();}} OnAddCommandClick(clickedItem) { let menuItem = clickedItem.parentCommand; let lastSettings = Utils.fetchObjectData(menuItem.command); lastSettings.TypeParam = clickedItem.command; this.showCommandProperties(menuItem, lastSettings);} showCommandProperties(menuItem, lastSettings) { this.layout.showModal({ caller: this, okText: 'Execute', caption: menuItem.displayName, URL: '/PropertyGrid/NewCommand/CurrentCommand', postData: { typeName: menuItem.command, nodeIds: this.selectedIds, lastSettings: JSON.stringify(lastSettings), linkName: menuItem.linkName }, onOk: () => { if (menuItem.isSerializable) { PropertyGrid.saveSettings('CurrentCommand', menuItem.command, () => { if (lastSettings.TypeParam) this.executeAddCommand(menuItem); else this.executeServerCommand(menuItem);});} else { if (lastSettings.TypeParam) this.executeAddCommand(menuItem); else this.executeServerCommand(menuItem);}}});} executeAddCommand(menuItem) { this.focusedItem.expand(() => this.executeServerCommand(menuItem));} executeServerCommand(menuItem) { let commandType = (menuItem.mode == 'auto') ? menuItem.command : null; let postData = { nodeIds: this.selectedIds, commandType: commandType, linkName: menuItem.linkName }; if (menuItem.mode != 'auto' || menuItem.shortName == 'Paste') { let caption = `Executing '${menuItem.displayName}' on ${this.selectedIds.length.toLocaleString()} items.`; let modal = this.layout.showProgress(menuItem.displayName, caption, () => modal.cancel()); modal.httpPost(this.baseURL + '/Execute', postData, data => this.update(data));} else { HTTP.post(this.baseURL + '/Execute', postData, data => { if (menuItem.shortName != 'Copy') this.update(data);});}} OnViewProperties() { this.layout.showModal({ caller: this, requireEdit: true, okText: 'Save', postData: { gridId: 'NodeProperties', nodeId: this.focusedId }, URL: '/PropertyGrid/EditNode', onOk: () => this.saveProperties()});} OnGotoFlagged() { this.focusNext(e => e.item.matches('.flagged'), 'Flagged', false);} OnValidateBranch(menuItem) { let modal = this.layout.showProgress(`${menuItem.displayName} - '${this.focusedItem.name}'`, '

', () => modal.cancel()); modal.httpPost('/TreeViewer/ValidateBranch', { nodeId: this.focusedId }, data => this.OnBranchValidated(data, menuItem));} OnBranchValidated(data, menuItem) { if (Utils.isNullOrEmpty(data)) { this.layout.showToast(menuItem.displayName, 'No validation errors found.', 5);} else { let options = { content: data, caller: this, caption: `Validate Branch - ${this.focusedItem.name}`, hideCancel: true, width: 'fit-content' }; this.layout.showModal(options);}} OnChange(e) { let node = TreeViewer.getClickedNode(e); node.item.setAttribute('aria-checked', node.isChecked.toString()); if (this.altKeyPressed) this.broadcastCheckState(node); if (this.hierarchyMode) this.applyHierarchyMode(node);} OnFocus(e) { if (e.relatedTarget) { if (e.relatedTarget.matches('.ModalBox > .content, .ModalBox > .content > .title-bar button')) return; if (e.relatedTarget.matches('.context-menu')) return;} this.loadMenu();} OnKeyUp(e) { if (e.key == 'Alt') this.altKeyPressed = false; if (e.key == 'Control') this.ctlKeyPressed = false; if (this.isDisabled || e.ctrlKey || e.altKey) return; const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']; if (!navKeys.includes(e.key)) return; if (this.focusedItem != null) this.StartFocusTimer(this.focusedId);} static inKeyDown; altKeyPressed; ctlKeyPressed; OnKeyDown(e) { if (e.key == 'Alt') this.altKeyPressed = true; if (e.key == 'Control') this.ctlKeyPressed = true; if (Utils.isModifierKey(e)) return; if (this.isDisabled || TreeViewer.inKeyDown) return false; TreeViewer.inKeyDown = true; let node = this.focusedItem; if (node == null) { TreeViewer.inKeyDown = false; return; } let keyWasProcessed = node.HandleNavKey(e); if (!keyWasProcessed && this.menu) { let cmd = this.menu.getCommand(e); if (cmd != null) { cmd.row.click(); keyWasProcessed = true; }} if (e.key == "Enter") keyWasProcessed = true; if (keyWasProcessed) e.preventDefault(); TreeViewer.inKeyDown = false; return;} OnClick(e) { if (this.isDisabled) { if (this.disabledMessage != null) this.layout.showToast('Navigation Disabled', this.disabledMessage) return;} let node = TreeViewer.getClickedNode(e); let marker = TreeViewer.getClickedExpander(e); if (node == null || marker != null) { node = marker; if (node) this.toggleExpanded(node); return;} switch (true) { case e.shiftKey: return node.ShiftClick(); case e.ctrlKey: return node.ControlClick(); default: if (node.nodeId != this.focusedId) node.focus(true, true, true);} this.bubbleEvent(TreeViewer.nodeClickedEvent, node); e.stopPropagation();} OnDblClick(e) { if (this.isDisabled) return; let node = TreeViewer.getClickedNode(e); if (node == null) return; if (e.target.matches('.attachment > div, .attachment > div *')) { this.layout.confirmBox(`Download ${node.attachmentName}?`, () => { window.open(Utils.mapPath(`/TreeViewer/DownloadAttachment?folderId=${node.nodeId}`), '_blank');});} else if (!node.isExpandable) { return;} else if (this.selectedIds.length == 1) { node.toggleExpanded();} else { this.toggleExpanded(node);}} OnContextMenu(e) { e.stopPropagation(); e.preventDefault(); if (!this.isDisabled) { let node = TreeViewer.getClickedNode(e); if (node != null) { if (!node.isSelected) node.focus(true, true, true); if (this.menu) this.menu.showAt(e.pageX, e.pageY);} else if (this.menu && this.focusedItem) { let bounds = this.focusedItem.bounds this.menu.showAt(bounds.right - 100, bounds.bottom);}}} OnMenuClosed(e) { if (e.detail.refocus) this.element.focus();}} const PastePosition = { Inside: 0, Before: 1, After: 2} const SelectModes = { Single: 0, Multi: 1, SingleType: 2};; class TreeViewerUtils { static collapseFolders(treeViewer) { this.unloadCollapsedFolders(treeViewer); this.unloadFolders(treeViewer);} static unloadCollapsedFolders(viewer) { let ids = []; TreeNode.getCollapsedFolderIds(viewer.firstRoot, ids); for (const id of ids) viewer.getNode(id).unloadChildren();} static unloadFolders(viewer) { const limit = 1000; let overage = viewer.nodeMap.size - limit; if (overage > 0) { let context = { nodeCount: overage, ids: [] }; TreeNode.getLoadedFolderIds(viewer.firstRoot, context); for (const folderId of context.ids) viewer.getNode(folderId).unloadChildren();}}}; class ViewerLink { parent; constructor(parent) { this.parent = parent; this.parent.addListener(TreeViewer.changesSavedEvent, e => this.OnChangesSaved(e)); this.parent.addListener(TreeViewer.selectionChangedEvent, () => this.OnSelectionChanged()); this.parent.addListener(PageViewer.imageEditedEvent, e => this.OnImageEdited(e));} get treeViewer() { return HtmlControl.get(this.parent.find('.TreeViewer')); } get docViewer() { return HtmlControl.get(this.parent.find('.DocumentViewer')); } OnChangesSaved(e) { if (e.detail.redisplay) { this.docViewer.loadDocument(this.treeViewer.focusedId);}} OnSelectionChanged() { this.docViewer.loadDocument(this.treeViewer.focusedId);} OnImageEdited(e) { let node = this.treeViewer?.getNode(e.detail.pageId); if (node) { node.image.src = node.image.src; this.treeViewer.loadMenu();}}}; class HelpPage extends HtmlPage { searchTimer; backStack; forwardStack; lastTopic = null; highlight = null; constructor(element) { super(element); this.commands.onclick = e => this.OnCommandClick(e); this.leftNav.onclick = e => this.OnCommandClick(e); this.addListener(HtmlTree.selectionChangedEvent, e => this.OnSelectionChanged(e)); this.addListener('keydown', e => this.OnKeyDown(e)); this.leftNav.onchange = e => this.OnInput(e); this.rightPanel.onclick = e => this.OnLinkClick(e); HtmlControl.initBranch(element); this.backStack = []; this.forwardStack = []; if (this.dataset.topic) { this.highlight = this.dataset.highlight; this.gotoTopic(this.dataset.topic);} if (this.isDisabledMode) this.homeLink.nodeValue = 'https://wiki.grooper.com';} get leftPanel() { return this.element.firstElementChild; } get leftNav() { return this.leftPanel.firstElementChild; } get tree() { return HtmlControl.get(this.leftPanel.children[1]); } get searchInput() { return this.leftNav.querySelector('input'); } get hasSearchText() { return !Utils.isNullOrWhiteSpace(this.searchInput.value); } get searchText() { return this.searchInput.value.replaceAll(/\s+/g, ' ').trim(); } get rightPanel() { return this.element.lastElementChild; } get backButton() { return this.commands.querySelector('#back'); } get forwardButton() { return this.commands.querySelector('#forward'); } get clearButton() { return this.leftNav.querySelector('#clear'); } get canGoBack() { return (this.histIndex != null) && (this.histIndex > 0); } get focusedTopic() { return this.tree.focusedNode.dataset.type; } get homeLink() { return this.layout.topNav.firstElementChild.firstElementChild.attributes[0]; } get isDisabledMode() { return this.dataset.isdisabledmode.toLowerCase() === 'true'; } setCommandStates() { Utils.setButtonState(this.backButton, this.backStack.length > 0); Utils.setButtonState(this.forwardButton, this.forwardStack.length > 0); Utils.setButtonState(this.clearButton, this.hasSearchText);} gotoTopic(topic) { let node = this.tree.findTopic(topic); if (node) node.setFocus(false); this.setCommandStates();} highlightSearchWords(searchText) { if (Utils.isNullOrEmpty(searchText)) { for (const element of this.rightPanel.querySelectorAll('span.search-hit')) element.outerHTML = element.innerHTML;} else if (searchText.startsWith('/') && searchText.endsWith('/')) { let pattern = searchText.substring(1, searchText.length - 1); this.highlightRegex(pattern);} else if (searchText.startsWith('"') && searchText.endsWith('"')) { let substring = searchText.substring(1, searchText.length - 1); this.highlightRegex(Utils.escapeRegex(substring));} else { this.highlightTerms(searchText.split(' '));}} highlightTerms(terms) { let usedValues = new Set(); for (const term of terms) { if (!usedValues.has(term)) { this.highlightTerm(term); usedValues.add(term);}}} highlightTerm(term) { let pattern = `(?<=[^\\w]|^)${term}(?=[^\\w]|$)`; this.highlightRegex(pattern);} highlightRegex(pattern) { let textNodes = this.getTextNodes(this.rightPanel); for (const node of textNodes) { let content = node.textContent; for (const hit of content.matchAll(new RegExp(pattern, 'dgi'))) { let value = hit[0]; content = content.replaceAll(new RegExp(value, 'dg'), `${value}`)} if (content != node.textContent) this.updateContent(node, content);}} updateContent(node, content) { let span = document.createElement('SPAN'); span.innerHTML = content; let remarks = node.parentNode.closest('td.PropertyDetails > div.dropdown'); if (remarks) { let descCell = remarks.parentElement; let expanderCell = descCell.nextElementSibling; if (expanderCell.innerText == '►') { expanderCell.innerText = '▼'; Utils.setClass(remarks, 'd-none', false);}} node.parentNode.replaceChild(span, node);} *getTextNodes(parent) { for (const child of parent.childNodes) { switch (child.nodeType) { case CodeEditor.TEXT_NODE: yield child; break; case CodeEditor.ELEMENT_NODE: for (const grandchild of this.getTextNodes(child)) yield grandchild; break;}}} OnKeyDown(e) { if (e.target == this.searchInput) return; switch (Utils.getKeyID(e)) { case 'Backspace': if (Utils.isButtonEnabled(this.backButton)) this.OnBackButton(); break;}} OnInput(e) { this.tree.search(this.searchText); this.setCommandStates();} OnSelectionChanged(e) { if (this.focusedTopic) { if (this.lastTopic != null) { this.backStack.push(this.lastTopic); this.forwardStack = [];} this.lastTopic = this.focusedTopic; HtmlControl.loadPost(this.rightPanel, '/Help/RenderTopic', { repositoryId: this.repositoryId, typeName: this.focusedTopic }, () => { if (!Utils.isNullOrEmpty(this.searchText)) { this.highlightSearchWords(this.searchText);} else if (!Utils.isNullOrEmpty(this.highlight)) { HTML.highlightSearchString(this.rightPanel, this.highlight, "search-hit"); this.highlight = null;}});} else { this.rightPanel.innerHTML = '';} this.setCommandStates();} OnLinkClick(e) { if (!e.target.matches('a[data-type]')) return; e.preventDefault(); e.stopPropagation(); if (this.hasSearchText) this.OnClearButton(); return this.gotoTopic(e.target.dataset.type);} OnCommandClick(e) { if (!Utils.isButtonEnabled(e.target)) return; switch (e.target.id) { case 'back': return this.OnBackButton(); case 'forward': return this.OnForwardButton(); case 'clear': return this.OnClearButton();}} OnClearButton() { this.searchInput.value = ''; this.tree.clearSearch(); this.setCommandStates();} OnBackButton() { let topic = this.backStack.pop(); this.forwardStack.push(this.focusedTopic); this.lastTopic = null; return this.gotoTopic(topic);} OnForwardButton() { let topic = this.forwardStack.pop(); this.backStack.push(this.focusedTopic); this.lastTopic = null; return this.gotoTopic(topic);}}; class GCC extends HtmlControl { constructor(element) { super(element); element.onclick = e => this.OnClick(e); element.ondblclick = e => this.OnDblClick(e);} OnClick(e) { switch (e.target.id) { case 'expand': return this.OnExpanderClick(e);}} OnDblClick(e) { if (e.target.closest('.command .header') == null) return; let command = e.target.closest('.command'); command.classList.toggle('expanded'); let button = command.querySelector('button#expand'); button.innerText = command.classList.contains('expanded') ? '▲' : '▼';} OnExpanderClick(e) { let command = e.target.closest('.command'); command.classList.toggle('expanded'); e.target.innerText = command.classList.contains('expanded') ? '▲' : '▼';}}; class HelpTopic extends HtmlControl { constructor(element) { super(element); element.onclick = e => this.OnClick(e); element.ondblclick = e => this.OnDblClick(e);} get topic() { return this.dataset.topic; } OnClick(e) { switch (true) { case e.target.matches('tr.expandable td:nth-child(3):not(:empty)'): return this.OnExpanderClick(e.target);}} OnDblClick(e) { switch (true) { case e.target.matches('tr.expandable td, tr.expandable td *'): let row = e.target.closest('tr'); return this.OnExpanderClick(row.cells[2]);}} OnExpanderClick(td) { let content = td.nextElementSibling.lastElementChild; if (td.innerText == '►') { td.innerText = '▼'; Utils.setClass(content, 'd-none', false);} else { td.innerText = '►'; Utils.setClass(content, 'd-none', true);}}}; class PropertyDetails extends HtmlControl { constructor(element) { super(element);}}; class TopicTree extends HtmlTree { content; firstHit; constructor(element) { super(element); this.content = this.element.innerHTML;} findTopic(topicName) { let li = this.find(`li[data-type="${topicName}"]`); return li ? new HtmlTreeNode(li) : null;} clearSearch() { let selectedTopic = this.focusedNode ? this.focusedNode.dataset.type : null; this.element.innerHTML = this.content; if (selectedTopic) { let node = this.findTopic(selectedTopic); if (node) node.setFocus(false);}} search(searchText) { this.firstHit = null; if (Utils.isNullOrWhiteSpace(searchText)) return this.clearSearch(); HTTP.post('/Help/Search', { repositoryId: this.repositoryId, searchText: searchText }, data => { this.applySearchResults(searchText, data);});} applySearchResults(searchText, resultTypes) { this.element.innerHTML = this.content; for (const node of this.rootNodes) { let hasHit = this.searchNode(node, searchText, resultTypes); this.setHitState(node, hasHit);} if (this.firstHit) this.firstHit.setFocus(false);} searchNode(parentNode, searchText, resultTypes) { let hasHit = resultTypes.includes(parentNode.dataset.type); if (hasHit && (this.firstHit == null)) this.firstHit = parentNode; for (const node of parentNode.childNodes) { let hasChildHit = this.searchNode(node, searchText, resultTypes); if (hasChildHit) hasHit = true; this.setHitState(node, hasChildHit);} return hasHit;} setHitState(node, isHit) { Utils.setClass(node.element, 'open', isHit); if (!isHit) node.element.remove();}};