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 = '
';
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();}};