diff --git a/src/main/resources/assets/admin/common/js/dom/Element.ts b/src/main/resources/assets/admin/common/js/dom/Element.ts index dd4132188..7aaa35ee2 100644 --- a/src/main/resources/assets/admin/common/js/dom/Element.ts +++ b/src/main/resources/assets/admin/common/js/dom/Element.ts @@ -13,6 +13,10 @@ import {ElementRegistry} from './ElementRegistry'; import {assert, assertNotNull, assertState} from '../util/Assert'; import {ElementEvent} from './ElementEvent'; import * as DOMPurify from 'dompurify'; +import {findHTMLElements} from './util/findHTMLElements'; +import {selectHTMLElement} from './util/selectHTMLElement'; +import {show} from './util/show'; + export interface PurifyConfig { addTags?: string[]; @@ -101,8 +105,8 @@ export class ElementFromHelperBuilder } static fromString(s: string, loadExistingChildren: boolean = true): ElementFromHelperBuilder { - let htmlEl = $(s).get(0); - let parentEl; + const htmlEl = selectHTMLElement(s); + let parentEl: Element | undefined; if (htmlEl && htmlEl.parentElement) { parentEl = Element.fromHtmlElement(htmlEl.parentElement); } @@ -267,7 +271,7 @@ export class Element { } static fromHtml(html: string, loadExistingChildren: boolean = true): Element { - const htmlEl = $(html).get(0); + const htmlEl = selectHTMLElement(html); if (!htmlEl) { return null; @@ -283,14 +287,13 @@ export class Element { } static fromSelector(s: string, loadExistingChildren: boolean = true): Element[] { - return $(s).map((_index, elem) => { - let htmlEl = elem; - let parentEl; + return findHTMLElements(s).map((htmlEl) => { + let parentEl: Element | undefined; if (htmlEl && htmlEl.parentElement) { parentEl = Element.fromHtmlElement(htmlEl.parentElement); } return Element.fromHtmlElement(htmlEl, loadExistingChildren, parentEl); - }).get(); + }); } public loadExistingChildren(): Element { @@ -378,7 +381,7 @@ export class Element { show() { // Using jQuery to show, since it seems to contain some smartness - $(this.el.getHTMLElement()).show(); + show(this.el.getHTMLElement()); this.notifyShown(this, true); } diff --git a/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts b/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts index 03d2d5199..28e4319e8 100644 --- a/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts +++ b/src/main/resources/assets/admin/common/js/dom/ElementHelper.ts @@ -1,6 +1,17 @@ import {Element} from './Element'; import {StringHelper} from '../util/StringHelper'; import {assert, assertNotNull} from '../util/Assert'; +import {getData} from './util/getData'; +import {getOuterHeightWithMargin} from './util/getOuterHeightWithMargin'; +import {getOuterWidthWithMargin} from './util/getOuterWidthWithMargin'; +import {getOffset} from './util/getOffset'; +import {getOffsetParent} from './util/getOffsetParent'; +import {getPosition} from './util/getPosition'; +import {isVisible} from './util/isVisible'; +import {setData} from './util/setData'; +import {setInnerHtml} from './util/setInnerHtml'; +import {setOffset} from './util/setOffset'; + export interface ElementDimensions { top: number; @@ -100,7 +111,7 @@ export class ElementHelper { } setInnerHtml(value: string, escapeHtml: boolean = true): ElementHelper { - $(this.el).html(escapeHtml ? StringHelper.escapeHtml(value) : value); + setInnerHtml(this.el, escapeHtml ? StringHelper.escapeHtml(value) : value); return this; } @@ -109,7 +120,7 @@ export class ElementHelper { } setText(value: string): ElementHelper { - $(this.el).text(value); + this.el.textContent = value; return this; } @@ -138,13 +149,12 @@ export class ElementHelper { setData(name: string, value: string): ElementHelper { assert(!StringHelper.isEmpty(name), 'Name cannot be empty'); assert(!StringHelper.isEmpty(value), 'Value cannot be empty'); - this.el.setAttribute('data-' + name, value); - $(this.el).data(name, value); + setData(this.el, name, value); return this; } getData(name: string): string { - let data = $(this.el).data(name); + let data = getData(this.el, name); return data ? data.toString() : undefined; } @@ -330,19 +340,19 @@ export class ElementHelper { } getWidth(): number { - return $(this.el).innerWidth(); + return this.el.clientWidth; } getWidthWithoutPadding(): number { - return $(this.el).width(); + return this.el.getBoundingClientRect().width; } getWidthWithBorder(): number { - return $(this.el).outerWidth(); + return this.el.offsetWidth; } getWidthWithMargin(): number { - return $(this.el).outerWidth(true); + return getOuterWidthWithMargin(this.el); } getMinWidth(): number { @@ -364,7 +374,7 @@ export class ElementHelper { } getHeight(): number { - return $(this.el).innerHeight(); + return this.el.clientHeight; } setMaxHeight(value: string): ElementHelper { @@ -396,15 +406,15 @@ export class ElementHelper { } getHeightWithoutPadding(): number { - return $(this.el).height(); + return this.el.getBoundingClientRect().height; } getHeightWithBorder(): number { - return $(this.el).outerHeight(); + return this.el.offsetHeight; } getHeightWithMargin(): number { - return $(this.el).outerHeight(true); + return getOuterHeightWithMargin(this.el); } setTop(value: string): ElementHelper { @@ -658,12 +668,15 @@ export class ElementHelper { getOffset(): { top: number; left: number; } { - return $(this.el).offset(); + return getOffset(this.el); } + /** + * Set the coordinates of every element, in the set of matched elements, relative to the document. + */ setOffset(offset: { top: number; left: number; }): ElementHelper { - $(this.el).offset(offset); - return this; + setOffset(this.el, offset); + return this; // TODO: return this or this.el? } getDimensions(): ElementDimensions { @@ -694,7 +707,7 @@ export class ElementHelper { * @returns {HTMLElement} */ getOffsetParent(): HTMLElement { - return $(this.el).offsetParent()[0]; + return getOffsetParent(this.el); } /** @@ -704,7 +717,7 @@ export class ElementHelper { getOffsetToParent(): { top: number; left: number; } { - return $(this.el).position(); + return getPosition(this.el); } getOffsetTop(): number { @@ -757,7 +770,7 @@ export class ElementHelper { } isVisible(): boolean { - return $(this.el).is(':visible'); + return isVisible(this.el); } countChildren(): number { diff --git a/src/main/resources/assets/admin/common/js/dom/util/animateScrollTop.ts b/src/main/resources/assets/admin/common/js/dom/util/animateScrollTop.ts new file mode 100644 index 000000000..786eb637e --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/animateScrollTop.ts @@ -0,0 +1,24 @@ +export function animateScrollTop( + el: HTMLElement, + targetTop: number, + durationMs: number, + complete: () => void +) { + const startScrollTop = el.scrollTop; + const distance = targetTop - startScrollTop; + const framesPerSecond = 50; + const timeout = 1000 / framesPerSecond; + const intervals = durationMs / timeout; + const step = distance / intervals; + const timer = setInterval(() => { + if (el.scrollTop >= targetTop) { + clearInterval(timer); + if (el.scrollTop > targetTop) { // Handle overshoot (decimals) + el.scrollTop = targetTop; + } + complete(); + } else { + el.scrollTop += step; + } + }, timeout); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/elementExists.ts b/src/main/resources/assets/admin/common/js/dom/util/elementExists.ts new file mode 100644 index 000000000..f9141fc64 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/elementExists.ts @@ -0,0 +1,2 @@ +export const elementExists = (element: Element) => typeof(element) != 'undefined' && element != null; + diff --git a/src/main/resources/assets/admin/common/js/dom/util/findElements.ts b/src/main/resources/assets/admin/common/js/dom/util/findElements.ts new file mode 100644 index 000000000..c8a9e658d --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/findElements.ts @@ -0,0 +1,6 @@ +export function findElements( + selector: string, + context: Document | Element = document +) { + return Array.from(context.querySelectorAll(selector)); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/findHTMLElements.ts b/src/main/resources/assets/admin/common/js/dom/util/findHTMLElements.ts new file mode 100644 index 000000000..d1e821bda --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/findHTMLElements.ts @@ -0,0 +1,9 @@ +import {findElements} from './findElements'; +import {isHTMLElement} from './isHTMLElement'; + +export function findHTMLElements( + selector: string, + context: Document | Element = document +): HTMLElement[] { + return findElements(selector, context).filter((el) => isHTMLElement(el)) as HTMLElement[]; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/first.ts b/src/main/resources/assets/admin/common/js/dom/util/first.ts new file mode 100644 index 000000000..04a768ee3 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/first.ts @@ -0,0 +1,6 @@ +export function first( + selector: string, + context: Document | Element = document +) { + return context.querySelector(selector); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getData.ts b/src/main/resources/assets/admin/common/js/dom/util/getData.ts new file mode 100644 index 000000000..13e77228d --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getData.ts @@ -0,0 +1,3 @@ +export function getData(el: Element, name: string): string | null { + return el.getAttribute(`data-${name}`); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getInnerHeight.ts b/src/main/resources/assets/admin/common/js/dom/util/getInnerHeight.ts new file mode 100644 index 000000000..1ad8e1a2b --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getInnerHeight.ts @@ -0,0 +1,3 @@ +export function getInnerHeight(el: Element) { + return el.clientHeight; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getOffset.ts b/src/main/resources/assets/admin/common/js/dom/util/getOffset.ts new file mode 100644 index 000000000..a3b8dca7c --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getOffset.ts @@ -0,0 +1,8 @@ +export function getOffset(el: Element) { + const box = el.getBoundingClientRect(); + const docElem = document.documentElement; + return { + top: box.top + window.scrollY - docElem.clientTop, + left: box.left + window.scrollX - docElem.clientLeft + }; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getOffsetParent.ts b/src/main/resources/assets/admin/common/js/dom/util/getOffsetParent.ts new file mode 100644 index 000000000..76a4d9b99 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getOffsetParent.ts @@ -0,0 +1,6 @@ +import {isHTMLElement} from './isHTMLElement'; + +export function getOffsetParent(el: HTMLElement): HTMLElement { + const maybeHTMLElement = el.offsetParent; + return isHTMLElement(maybeHTMLElement) ? maybeHTMLElement : el; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getOuterHeightWithMargin.ts b/src/main/resources/assets/admin/common/js/dom/util/getOuterHeightWithMargin.ts new file mode 100644 index 000000000..da645ffcd --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getOuterHeightWithMargin.ts @@ -0,0 +1,9 @@ +export function getOuterHeightWithMargin(el: Element) { + const style = getComputedStyle(el); + + return ( + el.getBoundingClientRect().height + + parseFloat(style.marginTop) + + parseFloat(style.marginBottom) + ); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getOuterWidthWithMargin.ts b/src/main/resources/assets/admin/common/js/dom/util/getOuterWidthWithMargin.ts new file mode 100644 index 000000000..bbf55e69b --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getOuterWidthWithMargin.ts @@ -0,0 +1,9 @@ +export function getOuterWidthWithMargin(el: Element) { + const style = getComputedStyle(el); + + return ( + el.getBoundingClientRect().width + + parseFloat(style.marginLeft) + + parseFloat(style.marginRight) + ); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/getPosition.ts b/src/main/resources/assets/admin/common/js/dom/util/getPosition.ts new file mode 100644 index 000000000..e567ce302 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/getPosition.ts @@ -0,0 +1,8 @@ +export function getPosition(el: Element) { + const {top, left} = el.getBoundingClientRect(); + const {marginTop, marginLeft} = getComputedStyle(el); + return { + top: top - parseInt(marginTop, 10), + left: left - parseInt(marginLeft, 10) + }; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/isHTMLElement.ts b/src/main/resources/assets/admin/common/js/dom/util/isHTMLElement.ts new file mode 100644 index 000000000..fc9d63981 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/isHTMLElement.ts @@ -0,0 +1,6 @@ +export function isHTMLElement(el: unknown): el is HTMLElement { + if (typeof HTMLElement !== 'object') { + throw new Error('Your browser does not support HTMLElement. Please use a newer browser.'); + } + return el instanceof HTMLElement; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/isVisible.ts b/src/main/resources/assets/admin/common/js/dom/util/isVisible.ts new file mode 100644 index 000000000..e12232357 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/isVisible.ts @@ -0,0 +1,3 @@ +export function isVisible(el: HTMLElement) { + return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/selectHTMLElement.ts b/src/main/resources/assets/admin/common/js/dom/util/selectHTMLElement.ts new file mode 100644 index 000000000..5cb8de0a7 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/selectHTMLElement.ts @@ -0,0 +1,10 @@ +import {isHTMLElement} from './isHTMLElement'; +import {first} from './first'; + +export function selectHTMLElement( + selector: string, + context: Document | Element = document +): HTMLElement | null { + const el = first(selector, context); + return isHTMLElement(el) ? el : null; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/setData.ts b/src/main/resources/assets/admin/common/js/dom/util/setData.ts new file mode 100644 index 000000000..f90658a6f --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/setData.ts @@ -0,0 +1,4 @@ +export function setData(el: Element, name: string, value: string) { + el.setAttribute('data-' + name, value); + return el; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/setInnerHeight.ts b/src/main/resources/assets/admin/common/js/dom/util/setInnerHeight.ts new file mode 100644 index 000000000..f27a3f6cc --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/setInnerHeight.ts @@ -0,0 +1,4 @@ +export function setInnerHeight(el: HTMLElement, value: string) { + el.style.height = value; + return el; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/setInnerHtml.ts b/src/main/resources/assets/admin/common/js/dom/util/setInnerHtml.ts new file mode 100644 index 000000000..235bca0f6 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/setInnerHtml.ts @@ -0,0 +1,4 @@ +export function setInnerHtml(el: Element, value: string) { + el.innerHTML = value; + return el; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/setOffset.ts b/src/main/resources/assets/admin/common/js/dom/util/setOffset.ts new file mode 100644 index 000000000..8a8f6a748 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/setOffset.ts @@ -0,0 +1,5 @@ +export function setOffset(el: HTMLElement, {top, left}: {top: number, left: number}) { + el.style.top = `${top}px`; + el.style.left = `${left}px`; + return el; +} diff --git a/src/main/resources/assets/admin/common/js/dom/util/show.ts b/src/main/resources/assets/admin/common/js/dom/util/show.ts new file mode 100644 index 000000000..2916c9a93 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/dom/util/show.ts @@ -0,0 +1,4 @@ +export function show(el: HTMLElement) { + el.style.display = ''; + return el; +} diff --git a/src/main/resources/assets/admin/common/js/ui/mask/Mask.ts b/src/main/resources/assets/admin/common/js/ui/mask/Mask.ts index 739bde098..d4f69a707 100644 --- a/src/main/resources/assets/admin/common/js/ui/mask/Mask.ts +++ b/src/main/resources/assets/admin/common/js/ui/mask/Mask.ts @@ -4,6 +4,10 @@ import {Element} from '../../dom/Element'; import {StyleHelper} from '../../StyleHelper'; import {ElementHiddenEvent} from '../../dom/ElementHiddenEvent'; import {Body} from '../../dom/Body'; +import {elementExists} from '../../dom/util/elementExists'; +import {getInnerHeight} from '../../dom/util/getInnerHeight'; +import {getPosition} from '../../dom/util/getPosition'; + export class Mask extends DivEl { @@ -73,33 +77,33 @@ export class Mask } } - private getWrapperEl(): JQuery { - let wrapperEl: JQuery = $(this.getEl().getHTMLElement()).closest('.mask-wrapper'); - if (wrapperEl.length) { + private getWrapperEl(): HTMLElement { + let wrapperEl: HTMLElement = this.getEl().getHTMLElement().closest('.mask-wrapper'); + if (elementExists(wrapperEl)) { return wrapperEl; } if (!this.masked) { - return $(this.getEl().getOffsetParent()); + return this.getEl().getOffsetParent(); } - const maskedEl = $(this.masked.getHTMLElement()); + const maskedEl = this.masked.getHTMLElement(); wrapperEl = maskedEl; - while (wrapperEl.length && $(wrapperEl).innerHeight() === 0) { - wrapperEl = $(wrapperEl).parent(); + while (elementExists(wrapperEl) && getInnerHeight(wrapperEl) === 0) { + wrapperEl = wrapperEl.parentElement; } - if (wrapperEl.length) { + if (elementExists(wrapperEl)) { return wrapperEl; } return maskedEl; } - private maskAndWrapperHaveEqualOffset(wrapperEl: JQuery): boolean { + private maskAndWrapperHaveEqualOffset(wrapperEl: HTMLElement): boolean { const offsetParentOfMask = this.getEl().getOffsetParent(); - const offsetParentOfMaskWrapper = wrapperEl.offsetParent()[0]; + const offsetParentOfMaskWrapper = wrapperEl.offsetParent; return offsetParentOfMask === offsetParentOfMaskWrapper; } @@ -108,14 +112,14 @@ export class Mask const maskedEl = this.getWrapperEl(); const maskDimensions: { width: string; height: string } = { - width: maskedEl.outerWidth() + 'px', - height: maskedEl.outerHeight() + 'px' + width: `${maskedEl.offsetWidth}px`, + height: `${maskedEl.offsetHeight}px` }; - let maskOffset: { top: number; left: number } = maskedEl.position(); + let maskOffset: { top: number; left: number } = getPosition(maskedEl); if (!this.maskAndWrapperHaveEqualOffset(maskedEl)) { - maskOffset = maskedEl.offset(); + maskOffset = maskedEl.getBoundingClientRect(); } this.getEl() diff --git a/src/main/resources/assets/admin/common/js/ui/panel/PanelStrip.ts b/src/main/resources/assets/admin/common/js/ui/panel/PanelStrip.ts index 9195b6a5d..cafe231d0 100644 --- a/src/main/resources/assets/admin/common/js/ui/panel/PanelStrip.ts +++ b/src/main/resources/assets/admin/common/js/ui/panel/PanelStrip.ts @@ -6,6 +6,7 @@ import {LoadMask} from '../mask/LoadMask'; import {Panel} from './Panel'; import {PanelShownEvent} from './PanelShownEvent'; import {PanelStripHeader} from './PanelStripHeader'; +import {animateScrollTop} from '../../dom/util/animateScrollTop'; export class PanelStrip extends Panel { @@ -253,13 +254,13 @@ export class PanelStrip - this.offset + (headerToShow.getEl().getPaddingTop() / 2) + headerToShow.getEl().getOffsetToParent().top; - - $(this.getScrollable().getHTMLElement()).animate({ - scrollTop: scrollTop - }, { - duration: 500, - complete: callback - }); + // TODO: Can this be done with CSS instead? + animateScrollTop( + this.getScrollable().getHTMLElement(), + scrollTop, + 500, + callback + ); } private notifyPanelShown(panel: Panel, panelIndex: number, previousPanel: Panel) { diff --git a/src/main/resources/assets/admin/common/js/ui/treegrid/TreeGrid.ts b/src/main/resources/assets/admin/common/js/ui/treegrid/TreeGrid.ts index 6aed92559..514d2d3a9 100644 --- a/src/main/resources/assets/admin/common/js/ui/treegrid/TreeGrid.ts +++ b/src/main/resources/assets/admin/common/js/ui/treegrid/TreeGrid.ts @@ -325,11 +325,11 @@ export class TreeGrid this.unSelectionChanged(listener); } - private getRowByNode(node: TreeNode): JQuery { + private getRowByNode(node: TreeNode) { let rowIndex = this.getRowIndexByNode(node); let cell = this.grid.getCellNode(rowIndex, 0); - return $(cell).closest('.slick-row'); + return cell.closest('.slick-row'); } removeHighlighting(skipEvent: boolean = false) { @@ -1530,10 +1530,10 @@ export class TreeGrid private onRowHighlighted(elem: ElementHelper, data: Slick.OnClickEventArgs) { const node = this.gridData.getItem(data.row); - const clickedCell = $(elem.getHTMLElement()).closest('.slick-cell'); + const clickedCell = elem.getHTMLElement().closest('.slick-cell'); const isRowSelected = this.grid.isRowSelected(data.row); const isMultipleRowsSelected = this.grid.getSelectedRows().length > 1; - const isRowHighlighted = clickedCell.hasClass('highlight'); + const isRowHighlighted = clickedCell.classList.contains('highlight'); if (elem.hasClass('sort-dialog-trigger') && (isRowSelected || isRowHighlighted)) { if (isMultipleRowsSelected) { diff --git a/tsconfig.json b/tsconfig.json index 7e1b60feb..b731bf236 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "noImplicitThis": true, "strictNullChecks": false, "types": [ + // TODO remove jquery and jqueryui. "jquery", "jqueryui", "mousetrap",