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 { CollectionViewType, DocumentType } from './client/documents/DocumentTypes'; import { Colors } from './client/views/global/globalEnums'; import { CreateImage } from './client/views/nodes/WebBoxRenderer'; export function DashColor(color: string | undefined) { 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: string | undefined) { 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 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 cleanDocumentTypeExt(type: DocumentType) { switch (type) { case DocumentType.PDF: return 'PDF'; case DocumentType.IMG: return 'Img'; case DocumentType.AUDIO: return 'Aud'; case DocumentType.COL: return 'Col'; case DocumentType.RTF: return 'Rtf'; default: return type.charAt(0).toUpperCase() + type.substring(1,3); } // prettier-ignore } export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) { switch (type) { case DocumentType.PDF: return 'PDF'; case DocumentType.IMG: return 'Image'; case DocumentType.AUDIO: return 'Audio'; case DocumentType.COL: return 'Collection:'+ (colType ?? ""); case DocumentType.RTF: return 'Text'; default: return type.charAt(0).toUpperCase() + type.slice(1); } // prettier-ignore } export function readUploadedFileAsText(inputFile: File) { 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: 0, translateX: 1, translateY: 1 }; } const rect = ele.getBoundingClientRect(); const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / (ele.offsetWidth || 1); const translateX = rect.left; const translateY = rect.top; return { scale, translateX, translateY }; } /** * A convenience method. Prepends the full path (i.e. http://localhost:) 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 }; } export 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 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 lightenRGB(rVal: number, gVal: number, bVal: number, percent: number): [number, number, number] { const amount = 1 + percent / 100; const r = rVal * amount; const g = gVal * amount; const b = bVal * amount; const threshold = 255.999; const maxVal = Math.max(r, g, b); if (maxVal <= threshold) { return [Math.round(r), Math.round(g), Math.round(b)]; } const total = r + g + b; if (total >= 3 * threshold) { return [Math.round(threshold), Math.round(threshold), Math.round(threshold)]; } const x = (3 * threshold - total) / (3 * maxVal - total); const gray = threshold - x * maxVal; return [Math.round(gray + x * r), Math.round(gray + x * g), Math.round(gray + x * b)]; } 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 { /* empty */ } const val = textArea.value; document.body.removeChild(textArea); return val; } } export function OmitKeys(obj: object, keys: string[], pattern?: string, addKeyFunc?: (dup: object) => void): { omit: { [key: string]: unknown }; extract: { [key: string]: unknown } } { const omit: { [key: string]: unknown } = { ...obj }; const extract: { [key: string]: unknown } = {}; 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: object & { [key: string]: unknown }, keys: string[], addKeyFunc?: (dup: unknown) => void) { const dup: { [key: string]: unknown } = {}; 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(ele => ele.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 = (ele: HTMLElement, value: number) => { ele.scrollTop = value; }; if (!_stop) { if (currentTime < duration) { elements.forEach((ele, i) => currentTime && setScrollTop(ele, easeFunc(transition, Math.min(currentTime, duration), starts[i], to - starts[i], duration))); requestAnimationFrame(animateScroll); } else { elements.forEach(ele => setScrollTop(ele, 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(ele => ele.scrollLeft); const startDate = new Date().getTime(); const animateScroll = () => { const currentDate = new Date().getTime(); const currentTime = currentDate - startDate; elements.forEach((ele, i) => { ele.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration); }); if (currentTime < duration) { requestAnimationFrame(animateScroll); } else { elements.forEach(ele => { ele.scrollLeft = to; }); } }; animateScroll(); } export function addStyleSheet() { const style = document.createElement('style'); const sheets = document.head.appendChild(style); return sheets.sheet; } export function addStyleSheetRule(sheet: CSSStyleSheet | null, selector: string, css: string | { [key: string]: string }, 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: CSSStyleSheet | null, rule: number) { if (sheet?.rules.length) { sheet.removeRule(rule); return true; } return false; } export function clearStyleSheetRules(sheet: CSSStyleSheet | null) { if (sheet?.rules.length) { numberRange(sheet.rules.length).map(() => sheet.removeRule(0)); return true; } return false; } export class simPointerEvent extends PointerEvent { dash?: boolean; } export class simMouseEvent extends MouseEvent { dash?: boolean; } 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 simPointerEvent(event, { view: window, bubbles: true, cancelable: true, button: 2, pointerType: 'mouse', clientX: x, clientY: y, screenX: sx, screenY: sy, }); me.dash = true; element.dispatchEvent(me); }); if (rightClick) { const me = new simMouseEvent('contextmenu', { view: window, bubbles: true, cancelable: true, button: 2, clientX: x, clientY: y, movementX: 0, movementY: 0, screenX: sx, screenY: sy, }); me.dash = true; element.dispatchEvent(me); } } export function getWordAtPoint(elem: Element, x: number, y: number): string | undefined { if (elem.tagName === 'INPUT') return 'input'; if (elem.tagName === 'TEXTAREA') return 'textarea'; if (elem.nodeType === elem.TEXT_NODE || elem.textContent) { 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) { 'expand' in range && (range.expand as (val: string) => void)('word'); // doesn't exist in firefox const ret = range.toString(); range.detach(); return ret; } currentPos += 1; } } else { Array.from(elem.children).forEach(childNode => { const range = childNode.ownerDocument?.createRange(); if (range) { 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) => void, clickEvent: (e: PointerEvent, doubleTap?: boolean) => unknown, stopPropagation: boolean = true, stopMovePropagation: boolean = true, noDoubleTapTimeout?: () => void ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const targetAny: object & { _downX: number; _downY: number; _lastX: number; _lastY: number; _doubleTap: boolean; _doubleTime?: NodeJS.Timeout; _lastTap: number; _noClick: boolean } = target as any; const doubleTapTimeout = 300; targetAny._doubleTap = Date.now() - targetAny._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 = (moveEv: PointerEvent): void => { if (moving || Math.abs(moveEv.clientX - targetAny._downX) > ClientUtils.DRAG_THRESHOLD || Math.abs(moveEv.clientY - targetAny._downY) > ClientUtils.DRAG_THRESHOLD) { moving = true; if (targetAny._doubleTime) { targetAny._doubleTime && clearTimeout(targetAny._doubleTime); targetAny._doubleTime = undefined; } if (moveEvent(moveEv, [targetAny._downX, targetAny._downY], [moveEv.clientX - targetAny._lastX, moveEv.clientY - targetAny._lastY])) { document.removeEventListener('pointermove', _moveEvent); // eslint-disable-next-line no-use-before-define document.removeEventListener('pointerup', _upEvent); } } targetAny._lastX = moveEv.clientX; targetAny._lastY = moveEv.clientY; stopMovePropagation && moveEv.stopPropagation(); }; const _upEvent = (upEv: PointerEvent): void => { const isClick = Math.abs(upEv.clientX - targetAny._downX) < 4 && Math.abs(upEv.clientY - targetAny._downY) < 4; upEvent(upEv, [upEv.clientX - targetAny._downX, upEv.clientY - targetAny._downY], isClick); if (isClick) { if (!targetAny._doubleTime && noDoubleTapTimeout) { targetAny._doubleTime = setTimeout(() => { noDoubleTapTimeout?.(); targetAny._doubleTime = undefined; }, doubleTapTimeout); } if (targetAny._doubleTime && targetAny._doubleTap) { targetAny._doubleTime && clearTimeout(targetAny._doubleTime); targetAny._doubleTime = undefined; } targetAny._noClick = clickEvent(upEv, targetAny._doubleTap) ? true : false; } document.removeEventListener('pointermove', _moveEvent); document.removeEventListener('pointerup', _upEvent, true); }; const _clickEvent = (clickev: MouseEvent): void => { if (targetAny._noClick) clickev.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 | null): number { return ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0; } export function DivWidth(ele: HTMLElement | null): number { return ele ? Number(getComputedStyle(ele).width.replace('px', '')) : 0; } export function dateRangeStrToDates(dateStr: string) { const toDate = (str: string) => { return !str.includes('T') && str.includes('-') ? new Date(Number(str.split('-')[0]), Number(str.split('-')[1]) - 1, Number(str.split('-')[2])) : new Date(str); }; // dateStr in yyyy-mm-dd format const dateRangeParts = dateStr.split('|'); // splits into from and to date if (dateRangeParts.length < 2 && !dateRangeParts[0]) return { start: new Date(), end: new Date() }; if (dateRangeParts.length < 2) return { start: toDate(dateRangeParts[0]), end: toDate(dateRangeParts[0]) }; return { start: new Date(dateRangeParts[0]), end: new Date(dateRangeParts[1]) }; } /** * converts the image to base url formate * @param imageUrl imageurl taken from the collection icon */ export async function imageUrlToBase64(imageUrl: string): Promise { try { const response = await fetch(imageUrl); const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = () => resolve(reader.result as string); reader.onerror = error => reject(error); }); } catch (error) { console.error('Error:', error); throw error; } } function replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { if (oldDiv.childNodes && newDiv) { for (let i = 0; i < oldDiv.childNodes.length; i++) { replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); } } if (oldDiv instanceof HTMLCanvasElement) { if (oldDiv.className === 'collectionFreeFormView-grid') { const newCan = newDiv as HTMLCanvasElement; const parEle = newCan.parentElement as HTMLElement; parEle.removeChild(newCan); parEle.appendChild(document.createElement('div')); } else { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element try { img.src = canvas.toDataURL(); // image source } catch (e) { console.log(e); } img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; if (newCan) { const parEle = newCan.parentElement as HTMLElement; parEle.removeChild(newCan); parEle.appendChild(img); } } } } export function UpdateIcon( filename: string, docViewContent: HTMLElement, width: number, height: number, panelWidth: number, panelHeight: number, scrollTop: number, realNativeHeight: number, noSuffix: boolean, replaceRootFilename: string | undefined, cb: (iconFile: string, nativeWidth: number, nativeHeight: number) => void ) { const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = width.toString(); newDiv.style.height = height.toString(); replaceCanvases(docViewContent, newDiv); const htmlString = new XMLSerializer().serializeToString(newDiv); const nativeWidth = width; const nativeHeight = height; return CreateImage(ClientUtils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight) .then(async dataUrl => { const returnedFilename = await ClientUtils.convertDataUri(dataUrl, filename, noSuffix, replaceRootFilename); cb(returnedFilename as string, nativeWidth, nativeHeight); }) .catch(error => console.error('oops, something went wrong!', error)); }