aboutsummaryrefslogtreecommitdiff
path: root/src/ClientUtils.ts
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-04-19 11:32:46 -0400
committerbobzel <zzzman@gmail.com>2024-04-19 11:32:46 -0400
commit89e5b4e224d77c7a029ec7d9c9027095665508ac (patch)
treeb1163d7090da48d978858caa44c601401a9400f6 /src/ClientUtils.ts
parentb6229b0a6141afbfd0e78e3ec870218187864def (diff)
lint fixes.
Diffstat (limited to 'src/ClientUtils.ts')
-rw-r--r--src/ClientUtils.ts649
1 files changed, 649 insertions, 0 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
new file mode 100644
index 000000000..57e0373c4
--- /dev/null
+++ b/src/ClientUtils.ts
@@ -0,0 +1,649 @@
+import * as Color from 'color';
+import * as React from 'react';
+import { ColorResult } from 'react-color';
+import * as rp from 'request-promise';
+import { numberRange, decimalToHexString } from './Utils';
+import { DocumentType } from './client/documents/DocumentTypes';
+import { Colors } from './client/views/global/globalEnums';
+
+export function DashColor(color: string) {
+ try {
+ return color ? Color(color.toLowerCase()) : Color('transparent');
+ } catch (e) {
+ if (color.includes('gradient')) console.log("using color 'white' in place of :" + color);
+ else console.log('COLOR error:', e);
+ return Color('white');
+ }
+}
+
+export function lightOrDark(color: any) {
+ if (color === 'transparent' || !color) return Colors.BLACK;
+ if (color.startsWith?.('linear')) return Colors.BLACK;
+ if (DashColor(color).isLight()) return Colors.BLACK;
+ return Colors.WHITE;
+}
+
+export function returnTransparent() {
+ return 'transparent';
+}
+
+export function returnTrue() {
+ return true;
+}
+
+export function returnIgnore(): 'ignore' {
+ return 'ignore';
+}
+export function returnAlways(): 'always' {
+ return 'always';
+}
+export function returnNever(): 'never' {
+ return 'never';
+}
+
+export function returnDefault(): 'default' {
+ return 'default';
+}
+
+export function return18() {
+ return 18;
+}
+
+export function returnFalse() {
+ return false;
+}
+
+export function returnAll(): 'all' {
+ return 'all';
+}
+
+export function returnNone(): 'none' {
+ return 'none';
+}
+
+export function returnVal(val1?: number, val2?: number) {
+ return val1 || (val2 !== undefined ? val2 : 0);
+}
+
+export function returnOne() {
+ return 1;
+}
+
+export function returnZero() {
+ return 0;
+}
+
+export function returnEmptyString() {
+ return '';
+}
+
+export function returnEmptyFilter() {
+ return [] as string[];
+}
+
+export function returnEmptyDoclist() {
+ return [] as any[];
+}
+
+export namespace ClientUtils {
+ export const CLICK_TIME = 300;
+ export const DRAG_THRESHOLD = 4;
+ export const SNAP_THRESHOLD = 10;
+ let _currentUserEmail: string = '';
+ export function CurrentUserEmail() {
+ return _currentUserEmail;
+ }
+ export function SetCurrentUserEmail(email: string) {
+ _currentUserEmail = email;
+ }
+ export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) {
+ return Date.now() - downTime < ClientUtils.CLICK_TIME && Math.abs(x - downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(y - downY) < ClientUtils.DRAG_THRESHOLD;
+ }
+
+ export function cleanDocumentType(type: DocumentType) {
+ switch (type) {
+ case DocumentType.IMG: return 'Image';
+ case DocumentType.AUDIO: return 'Audio';
+ case DocumentType.COL: return 'Collection';
+ case DocumentType.RTF: return 'Text';
+ default: return type.charAt(0).toUpperCase() + type.slice(1);
+ } // prettier-ignore
+ }
+
+ export function readUploadedFileAsText(inputFile: File) {
+ // eslint-disable-next-line no-undef
+ const temporaryFileReader = new FileReader();
+
+ return new Promise((resolve, reject) => {
+ temporaryFileReader.onerror = () => {
+ temporaryFileReader.abort();
+ reject(new DOMException('Problem parsing input file.'));
+ };
+
+ temporaryFileReader.onload = () => {
+ resolve(temporaryFileReader.result);
+ };
+ temporaryFileReader.readAsText(inputFile);
+ });
+ }
+
+ /**
+ * Uploads an image buffer to the server and stores with specified filename. by default the image
+ * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+ * @param imageUri the bytes of the image
+ * @param returnedFilename the base filename to store the image on the server
+ * @param nosuffix optionally suppress creating multiple resolution images
+ */
+ export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) {
+ try {
+ const posting = ClientUtils.prepend('/uploadURI');
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename,
+ },
+ json: true,
+ });
+ return returnedUri;
+ } catch (e) {
+ console.log('ConvertDataURI :' + e);
+ }
+ return undefined;
+ }
+
+ export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } {
+ if (!ele) {
+ return { scale: 1, translateX: 1, translateY: 1 };
+ }
+ const rect = ele.getBoundingClientRect();
+ const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth;
+ const translateX = rect.left;
+ const translateY = rect.top;
+
+ return { scale, translateX, translateY };
+ }
+
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:<port>) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
+ export function prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+ export function fileUrl(filename: string): string {
+ return prepend(`/files/${filename}`);
+ }
+
+ export function shareUrl(documentId: string): string {
+ return prepend(`/doc/${documentId}?sharing=true`);
+ }
+
+ export function CorsProxy(url: string): string {
+ return prepend('/corsProxy/') + encodeURIComponent(url);
+ }
+
+ export function CopyText(text: string) {
+ navigator.clipboard.writeText(text);
+ }
+
+ export function colorString(color: ColorResult) {
+ return color.hex.startsWith('#') && color.hex.length < 8 ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex;
+ }
+
+ export function fromRGBAstr(rgba: string) {
+ const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
+ const r = rm ? Number(rm[1]) : 0;
+ const gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
+ const g = gm ? Number(gm[1]) : 0;
+ const bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
+ const b = bm ? Number(bm[1]) : 0;
+ const am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
+ const a = am ? Number(am[1]) : 1;
+ return { r: r, g: g, b: b, a: a };
+ }
+
+ const isTransparentFunctionHack = 'isTransparent(__value__)';
+ export const noRecursionHack = '__noRecursion';
+
+ // special case filters
+ export const noDragDocsFilter = 'noDragDocs::any::check';
+ export const TransparentBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+ export const OpaqueBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+
+ export function IsRecursiveFilter(val: string) {
+ return !val.includes(noRecursionHack);
+ }
+ export function HasFunctionFilter(val: string) {
+ if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
+ // add other function filters here...
+ return undefined;
+ }
+
+ export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
+ return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
+ }
+
+ export function HSLtoRGB(h: number, s: number, l: number) {
+ // Must be fractions of 1
+ // s /= 100;
+ // l /= 100;
+
+ const c = (1 - Math.abs(2 * l - 1)) * s;
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+ const m = l - c / 2;
+ let r = 0;
+ let g = 0;
+ let b = 0;
+ if (h >= 0 && h < 60) {
+ r = c;
+ g = x;
+ b = 0;
+ } else if (h >= 60 && h < 120) {
+ r = x;
+ g = c;
+ b = 0;
+ } else if (h >= 120 && h < 180) {
+ r = 0;
+ g = c;
+ b = x;
+ } else if (h >= 180 && h < 240) {
+ r = 0;
+ g = x;
+ b = c;
+ } else if (h >= 240 && h < 300) {
+ r = x;
+ g = 0;
+ b = c;
+ } else if (h >= 300 && h < 360) {
+ r = c;
+ g = 0;
+ b = x;
+ }
+ r = Math.round((r + m) * 255);
+ g = Math.round((g + m) * 255);
+ b = Math.round((b + m) * 255);
+ return { r: r, g: g, b: b };
+ }
+
+ export function RGBToHSL(red: number, green: number, blue: number) {
+ // Make r, g, and b fractions of 1
+ const r = red / 255;
+ const g = green / 255;
+ const b = blue / 255;
+
+ // Find greatest and smallest channel values
+ const cmin = Math.min(r, g, b);
+ const cmax = Math.max(r, g, b);
+ const delta = cmax - cmin;
+ let h = 0;
+ let s = 0;
+ let l = 0;
+ // Calculate hue
+
+ // No difference
+ if (delta === 0) h = 0;
+ // Red is max
+ else if (cmax === r) h = ((g - b) / delta) % 6;
+ // Green is max
+ else if (cmax === g) h = (b - r) / delta + 2;
+ // Blue is max
+ else h = (r - g) / delta + 4;
+
+ h = Math.round(h * 60);
+
+ // Make negative hues positive behind 360°
+ if (h < 0) h += 360; // Calculate lightness
+
+ l = (cmax + cmin) / 2;
+
+ // Calculate saturation
+ s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+
+ // Multiply l and s by 100
+ // s = +(s * 100).toFixed(1);
+ // l = +(l * 100).toFixed(1);
+
+ return { h: h, s: s, l: l };
+ }
+
+ export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
+ if (!targetHgt) return targetY; // if there's no height, then assume that
+ if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
+ return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
+ }
+ if (scrollTop >= Math.max(0, targetY - minSpacing)) {
+ return Math.max(0, Math.floor(targetY - minSpacing));
+ }
+ return undefined;
+ }
+
+ export function GetClipboardText(): string {
+ const textArea = document.createElement('textarea');
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ document.execCommand('paste');
+ } catch (err) {
+ /* empty */
+ }
+
+ const val = textArea.value;
+ document.body.removeChild(textArea);
+ return val;
+ }
+}
+
+export function OmitKeys(obj: any, keys: string[], pattern?: string, addKeyFunc?: (dup: any) => void): { omit: any; extract: any } {
+ const omit: any = { ...obj };
+ const extract: any = {};
+ keys.forEach(key => {
+ extract[key] = omit[key];
+ delete omit[key];
+ });
+ pattern &&
+ Array.from(Object.keys(omit))
+ .filter(key => key.match(pattern))
+ .forEach(key => {
+ extract[key] = omit[key];
+ delete omit[key];
+ });
+ addKeyFunc?.(omit);
+ return { omit, extract };
+}
+
+export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) {
+ const dup: any = {};
+ keys.forEach(key => {
+ dup[key] = obj[key];
+ });
+ addKeyFunc && addKeyFunc(dup);
+ return dup;
+}
+
+export function incrementTitleCopy(title: string) {
+ const numstr = title.match(/.*(\{([0-9]*)\})+/);
+ const copyNumStr = `{${1 + (numstr ? +numstr[2] : 0)}}`;
+ return (numstr ? title.replace(numstr[1], '') : title) + copyNumStr;
+}
+
+const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => {
+ if (transition === 'linear') {
+ const newCurrentTime = currentTime / duration; // currentTime / (duration / 2);
+ return start + newCurrentTime * change;
+ }
+
+ let newCurrentTime = currentTime / (duration / 2);
+ if (newCurrentTime < 1) {
+ return (change / 2) * newCurrentTime * newCurrentTime + start;
+ }
+
+ newCurrentTime -= 1;
+ return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
+};
+
+export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) {
+ stopper?.();
+ const elements = element instanceof HTMLElement ? [element] : element;
+ const starts = elements.map(element => element.scrollTop);
+ const startDate = new Date().getTime();
+ let _stop = false;
+ const stop = () => {
+ _stop = true;
+ };
+ const animateScroll = () => {
+ const currentDate = new Date().getTime();
+ const currentTime = currentDate - startDate;
+ const setScrollTop = (element: HTMLElement, value: number) => {
+ element.scrollTop = value;
+ };
+ if (!_stop) {
+ if (currentTime < duration) {
+ elements.forEach((element, i) => currentTime && setScrollTop(element, easeFunc(transition, Math.min(currentTime, duration), starts[i], to - starts[i], duration)));
+ requestAnimationFrame(animateScroll);
+ } else {
+ elements.forEach(element => setScrollTop(element, to));
+ }
+ }
+ };
+ animateScroll();
+ return stop;
+}
+
+export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) {
+ const elements = element instanceof HTMLElement ? [element] : element;
+ const starts = elements.map(element => element.scrollLeft);
+ const startDate = new Date().getTime();
+
+ const animateScroll = () => {
+ const currentDate = new Date().getTime();
+ const currentTime = currentDate - startDate;
+ elements.forEach((element, i) => {
+ element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration);
+ });
+
+ if (currentTime < duration) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ elements.forEach(element => {
+ element.scrollLeft = to;
+ });
+ }
+ };
+ animateScroll();
+}
+
+export function addStyleSheet(styleType: string = 'text/css') {
+ const style = document.createElement('style');
+ style.type = styleType;
+ const sheets = document.head.appendChild(style);
+ return (sheets as any).sheet;
+}
+export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
+ const propText =
+ typeof css === 'string'
+ ? css
+ : Object.keys(css)
+ .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p]))
+ .join(';');
+ return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length);
+}
+export function removeStyleSheetRule(sheet: any, rule: number) {
+ if (sheet.rules.length) {
+ sheet.removeRule(rule);
+ return true;
+ }
+ return false;
+}
+export function clearStyleSheetRules(sheet: any) {
+ if (sheet.rules.length) {
+ numberRange(sheet.rules.length).map(() => sheet.removeRule(0));
+ return true;
+ }
+ return false;
+}
+
+export function simulateMouseClick(element: Element | null | undefined, x: number, y: number, sx: number, sy: number, rightClick = true) {
+ if (!element) return;
+ ['pointerdown', 'pointerup'].forEach(event => {
+ const me = new PointerEvent(event, {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 2,
+ pointerType: 'mouse',
+ clientX: x,
+ clientY: y,
+ screenX: sx,
+ screenY: sy,
+ });
+ (me as any).dash = true;
+ element.dispatchEvent(me);
+ });
+
+ if (rightClick) {
+ const me = new MouseEvent('contextmenu', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 2,
+ clientX: x,
+ clientY: y,
+ movementX: 0,
+ movementY: 0,
+ screenX: sx,
+ screenY: sy,
+ });
+ (me as any).dash = true;
+ element.dispatchEvent(me);
+ }
+}
+
+export function getWordAtPoint(elem: any, x: number, y: number): string | undefined {
+ if (elem.tagName === 'INPUT') return 'input';
+ if (elem.tagName === 'TEXTAREA') return 'textarea';
+ if (elem.nodeType === elem.TEXT_NODE) {
+ const range = elem.ownerDocument.createRange();
+ range.selectNodeContents(elem);
+ let currentPos = 0;
+ const endPos = range.endOffset;
+ while (currentPos + 1 <= endPos) {
+ range.setStart(elem, currentPos);
+ range.setEnd(elem, currentPos + 1);
+ const rangeRect = range.getBoundingClientRect();
+ if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) {
+ range.expand?.('word'); // doesn't exist in firefox
+ const ret = range.toString();
+ range.detach();
+ return ret;
+ }
+ currentPos += 1;
+ }
+ } else {
+ Array.from(elem.childNodes).forEach((childNode: any) => {
+ const range = childNode.ownerDocument.createRange();
+ range.selectNodeContents(childNode);
+ const rangeRect = range.getBoundingClientRect();
+ if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) {
+ range.detach();
+ const word = getWordAtPoint(childNode, x, y);
+ if (word) return word;
+ } else {
+ range.detach();
+ }
+ return undefined;
+ });
+ }
+ return undefined;
+}
+
+export function isTargetChildOf(ele: HTMLDivElement | null, target: Element | null) {
+ let entered = false;
+ for (let child = target; !entered && child; child = child.parentElement) {
+ entered = child === ele;
+ }
+ return entered;
+}
+
+export function StopEvent(e: React.PointerEvent | React.MouseEvent | React.KeyboardEvent) {
+ e.stopPropagation();
+ e.preventDefault();
+}
+
+export function setupMoveUpEvents(
+ target: object,
+ e: React.PointerEvent,
+ moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
+ upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => any,
+ clickEvent: (e: PointerEvent, doubleTap?: boolean) => any,
+ // eslint-disable-next-line default-param-last
+ stopPropagation: boolean = true,
+ // eslint-disable-next-line default-param-last
+ stopMovePropagation: boolean = true,
+ noDoubleTapTimeout?: () => void
+) {
+ const targetAny = target as any;
+ const doubleTapTimeout = 300;
+ targetAny._doubleTap = Date.now() - (target as any)._lastTap < doubleTapTimeout;
+ targetAny._lastTap = Date.now();
+ targetAny._downX = targetAny._lastX = e.clientX;
+ targetAny._downY = targetAny._lastY = e.clientY;
+ targetAny._noClick = false;
+ let moving = false;
+
+ const _moveEvent = (e: PointerEvent): void => {
+ if (moving || Math.abs(e.clientX - (target as any)._downX) > ClientUtils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > ClientUtils.DRAG_THRESHOLD) {
+ moving = true;
+ if ((target as any)._doubleTime) {
+ clearTimeout((target as any)._doubleTime);
+ targetAny._doubleTime = undefined;
+ }
+ if (moveEvent(e, [(target as any)._downX, (target as any)._downY], [e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) {
+ document.removeEventListener('pointermove', _moveEvent);
+ // eslint-disable-next-line no-use-before-define
+ document.removeEventListener('pointerup', _upEvent);
+ }
+ }
+ targetAny._lastX = e.clientX;
+ targetAny._lastY = e.clientY;
+ stopMovePropagation && e.stopPropagation();
+ };
+ const _upEvent = (e: PointerEvent): void => {
+ const isClick = Math.abs(e.clientX - targetAny._downX) < 4 && Math.abs(e.clientY - targetAny._downY) < 4;
+ upEvent(e, [e.clientX - targetAny._downX, e.clientY - targetAny._downY], isClick);
+ if (isClick) {
+ if (!targetAny._doubleTime && noDoubleTapTimeout) {
+ targetAny._doubleTime = setTimeout(() => {
+ noDoubleTapTimeout?.();
+ targetAny._doubleTime = undefined;
+ }, doubleTapTimeout);
+ }
+ if (targetAny._doubleTime && targetAny._doubleTap) {
+ clearTimeout(targetAny._doubleTime);
+ targetAny._doubleTime = undefined;
+ }
+ targetAny._noClick = clickEvent(e, targetAny._doubleTap);
+ }
+ document.removeEventListener('pointermove', _moveEvent);
+ document.removeEventListener('pointerup', _upEvent, true);
+ };
+ const _clickEvent = (e: MouseEvent): void => {
+ if ((target as any)._noClick) e.stopPropagation();
+ document.removeEventListener('click', _clickEvent, true);
+ };
+ if (stopPropagation) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ document.addEventListener('pointermove', _moveEvent);
+ document.addEventListener('pointerup', _upEvent, true);
+ document.addEventListener('click', _clickEvent, true);
+}
+
+export function DivHeight(ele: HTMLElement): number {
+ return Number(getComputedStyle(ele).height.replace('px', ''));
+}
+export function DivWidth(ele: HTMLElement): number {
+ return Number(getComputedStyle(ele).width.replace('px', ''));
+}
+
+export function dateRangeStrToDates(dateStr: string) {
+ // dateStr in yyyy-mm-dd format
+ const dateRangeParts = dateStr.split('|'); // splits into from and to date
+ const fromParts = dateRangeParts[0].split('-');
+ const toParts = dateRangeParts[1].split('-');
+
+ const fromYear = parseInt(fromParts[0]);
+ const fromMonth = parseInt(fromParts[1]) - 1;
+ const fromDay = parseInt(fromParts[2]);
+
+ const toYear = parseInt(toParts[0]);
+ const toMonth = parseInt(toParts[1]) - 1;
+ const toDay = parseInt(toParts[2]);
+
+ return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)];
+}