From a0c37b4635e4d866ef83b00dc54824456a9267ca Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 08:07:58 -0300 Subject: [PATCH 01/70] feat: create wrappers over elements with data-superviz-id --- src/common/types/cdn.types.ts | 2 + .../comments/html-pin-adapter/index.ts | 228 ++++++++++++++++++ .../comments/html-pin-adapter/types.ts | 8 + src/components/comments/types.ts | 2 +- src/components/index.ts | 1 + src/index.ts | 2 + 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/components/comments/html-pin-adapter/index.ts create mode 100644 src/components/comments/html-pin-adapter/types.ts diff --git a/src/common/types/cdn.types.ts b/src/common/types/cdn.types.ts index 0912dee7..f89886e3 100644 --- a/src/common/types/cdn.types.ts +++ b/src/common/types/cdn.types.ts @@ -1,5 +1,6 @@ import { CanvasPin, + HTMLPin, Comments, MousePointers, Realtime, @@ -46,6 +47,7 @@ export interface SuperVizCdn { Realtime: typeof Realtime; Comments: typeof Comments; CanvasPin: typeof CanvasPin; + HTMLPin: typeof HTMLPin; WhoIsOnline: typeof WhoIsOnline; RealtimeComponentState: typeof RealtimeComponentState; RealtimeComponentEvent: typeof RealtimeComponentEvent; diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts new file mode 100644 index 00000000..dee9b310 --- /dev/null +++ b/src/components/comments/html-pin-adapter/index.ts @@ -0,0 +1,228 @@ +import { Logger, Observer } from '../../../common/utils'; +import { Annotation, PinAdapter } from '../types'; + +import { SimpleParticipant } from './types'; + +export class HTMLPin implements PinAdapter { + private logger: Logger; + private container: HTMLElement; + private isActive: boolean; + private isPinsVisible: boolean = true; + private annotations: Annotation[]; + private animateFrame: number; + public onPinFixedObserver: Observer; + private temporaryPinCoordinates: { x: number; y: number } | null = null; + private localParticipant: SimpleParticipant = {}; + private elementsWithDataId: Record = {}; + private divWrappers: HTMLElement[]; + + constructor(containerId: string) { + this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); + this.container = document.getElementById(containerId) as HTMLElement; + this.isActive = false; + + if (!this.container) { + const message = `Element with id ${containerId} not found`; + this.logger.log(message); + throw new Error(message); + } + + this.onPinFixedObserver = new Observer({ logger: this.logger }); + this.divWrappers = this.renderDivWrapper(); + this.annotations = []; + this.renderAnnotationsPins(); + + this.animateFrame = requestAnimationFrame(this.animate); + } + + /** + * @function destroy + * @description destroys the container pin adapter. + * @returns {void} + * */ + public destroy(): void { + this.removeListeners(); + this.divWrappers.forEach((divWrapper) => divWrapper.remove()); + this.onPinFixedObserver.destroy(); + this.onPinFixedObserver = null; + this.annotations = []; + + cancelAnimationFrame(this.animateFrame); + } + + private setElementsReadyToPin = (): void => { + const elementsWithDataId = this.container.querySelectorAll('[data-superviz-id]'); + const dataIdList = new Set(); + + elementsWithDataId.forEach((el: HTMLElement) => { + const id = el.getAttribute('data-superviz-id'); + dataIdList.add(id); + + if (this.elementsWithDataId[id]) return; + + this.elementsWithDataId[id] = el; + this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; + + this.elementsWithDataId[id].addEventListener('click', this.onClick); + }); + + const childrenId = Object.keys(this.elementsWithDataId); + if (childrenId.length === elementsWithDataId.length) return; + + childrenId.forEach((id) => { + if (dataIdList.has(id)) return; + this.elementsWithDataId[id].style.backgroundColor = ''; + this.elementsWithDataId[id].style.cursor = 'default'; + + this.elementsWithDataId[id].removeEventListener('click', this.onClick); + delete this.elementsWithDataId[id]; + }); + }; + + /** + * @function setPinsVisibility + * @param {boolean} isVisible - Controls the visibility of the pins. + * @returns {void} + */ + public setPinsVisibility(isVisible: boolean): void { + this.isPinsVisible = isVisible; + + if (this.isPinsVisible) { + this.renderAnnotationsPins(); + } + } + + /** + * @function setActive + * @param {boolean} isOpen - Whether the container pin adapter is active or not. + * @returns {void} + */ + public setActive(isOpen: boolean): void { + this.isActive = isOpen; + + if (this.isActive) { + this.addListeners(); + this.setElementsReadyToPin(); + return; + } + + this.removeListeners(); + } + + /** + * @function updateAnnotations + * @description updates the annotations of the container. + * @param {Annotation[]} annotations - New annotation to be added to the container. + * @returns {void} + */ + public updateAnnotations(annotations: Annotation[]): void { + this.logger.log('updateAnnotations', annotations); + + this.annotations = annotations; + + if (!this.isActive && !this.isPinsVisible) return; + + this.renderAnnotationsPins(); + } + + /** + * @function renderTemporaryPin + * @description + creates a temporary pin with the id + temporary-pin to mark where the annotation is being created + */ + public renderTemporaryPin(): void {} + + /** + * @function addListeners + * @description adds event listeners to the container element. + * @returns {void} + */ + private addListeners(): void { + Object.keys(this.elementsWithDataId).forEach((id) => { + this.elementsWithDataId[id].addEventListener('click', this.onClick); + this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; + }); + } + + public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { + this.localParticipant.avatar = avatar; + this.localParticipant.name = name; + }; + + /** + * @function removeListeners + * @description removes event listeners from the container element. + * @returns {void} + * */ + private removeListeners(): void { + Object.keys(this.elementsWithDataId).forEach((id) => { + this.elementsWithDataId[id].removeEventListener('click', this.onClick); + this.elementsWithDataId[id].setAttribute('style', ''); + }); + } + + /** + * @function animate + * @description animation frame + * @returns {void} + */ + private animate = (): void => { + if (this.isActive || this.isPinsVisible) { + this.renderAnnotationsPins(); + this.renderDivWrapper(); + } + + if (this.isActive) { + this.setElementsReadyToPin(); + } + + if (this.temporaryPinCoordinates) { + this.renderTemporaryPin(); + } + + requestAnimationFrame(this.animate); + }; + + /** + * @function renderDivWrapper + * @description Creates a div wrapper for the pins over each valid element. + * */ + private renderDivWrapper(): HTMLElement[] { + const divWrappers: HTMLElement[] = Object.values(this.elementsWithDataId).map((el) => { + const container = el; + const containerRect = container.getBoundingClientRect(); + + const divWrapper = document.createElement('div'); + const dataSupervizId = container.getAttribute('data-superviz-id'); + const id = `superviz-id-${dataSupervizId}`; + divWrapper.id = id; + + this.container.parentElement.style.position = 'relative'; + + divWrapper.style.position = 'fixed'; + divWrapper.style.top = `${containerRect.top}px`; + divWrapper.style.left = `${containerRect.left}px`; + divWrapper.style.width = `${containerRect.width}px`; + divWrapper.style.height = `${containerRect.height}px`; + divWrapper.style.pointerEvents = 'none'; + divWrapper.style.overflow = 'hidden'; + + if (!this.container.querySelector(`#${id}`)) { + container.parentElement.appendChild(divWrapper); + } + + return divWrapper; + }); + + return divWrappers; + } + + private renderAnnotationsPins(): void { + this.annotations.forEach((annotation) => {}); + } + + private onClick = (event: MouseEvent): void => {}; + + public removeAnnotationPin(uuid: string): void {} +} diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts new file mode 100644 index 00000000..481eabb6 --- /dev/null +++ b/src/components/comments/html-pin-adapter/types.ts @@ -0,0 +1,8 @@ +export interface CanvasPinAdapterProps { + onGoToPin?: (position: { x: number; y: number }) => void; +} + +export interface SimpleParticipant { + name?: string; + avatar?: string; +} diff --git a/src/components/comments/types.ts b/src/components/comments/types.ts index 3ff1be60..f5f7110a 100644 --- a/src/components/comments/types.ts +++ b/src/components/comments/types.ts @@ -33,7 +33,7 @@ export interface PinCoordinates { x: number; y: number; z?: number; - type: 'canvas' | 'matterport' | 'threejs' | 'autodesk'; + type: 'canvas' | 'matterport' | 'threejs' | 'autodesk' | 'html'; } // @NOTE - this is used for 3d annotations diff --git a/src/components/index.ts b/src/components/index.ts index 3633f486..fb8c9221 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,5 +1,6 @@ export { Comments } from './comments'; export { CanvasPin } from './comments/canvas-pin-adapter'; +export { HTMLPin } from './comments/html-pin-adapter'; export { VideoConference } from './video'; export { MousePointers } from './presence-mouse'; export { Realtime } from './realtime'; diff --git a/src/index.ts b/src/index.ts index 8a199d7f..0472b603 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { Realtime, Comments, CanvasPin, + HTMLPin, WhoIsOnline, } from './components'; import { RealtimeComponentEvent, RealtimeComponentState } from './components/realtime/types'; @@ -62,6 +63,7 @@ if (window) { Realtime, Comments, CanvasPin, + HTMLPin, WhoIsOnline, ParticipantType, LayoutPosition, From 31259be14d082dae807d1ec43a4b01ef63aaf057 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 11:15:02 -0300 Subject: [PATCH 02/70] feat: watch data-attribute changes with mutation observer --- .../comments/html-pin-adapter/index.ts | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index dee9b310..9ef3c748 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -27,6 +27,7 @@ export class HTMLPin implements PinAdapter { throw new Error(message); } + this.observeContainer(); this.onPinFixedObserver = new Observer({ logger: this.logger }); this.divWrappers = this.renderDivWrapper(); this.annotations = []; @@ -50,35 +51,80 @@ export class HTMLPin implements PinAdapter { cancelAnimationFrame(this.animateFrame); } - private setElementsReadyToPin = (): void => { - const elementsWithDataId = this.container.querySelectorAll('[data-superviz-id]'); - const dataIdList = new Set(); + private clearElement(id: string) { + const element = this.elementsWithDataId[id]; + if (!element) return; - elementsWithDataId.forEach((el: HTMLElement) => { - const id = el.getAttribute('data-superviz-id'); - dataIdList.add(id); + element.style.cursor = 'default'; + element.removeEventListener('click', this.onClick); + delete this.elementsWithDataId[id]; + } - if (this.elementsWithDataId[id]) return; + private handleObserverChanges = (changes: MutationRecord[]): void => { + if (!this.isActive) return; - this.elementsWithDataId[id] = el; - this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; + changes.forEach((change) => { + const { target, oldValue } = change; + const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); - this.elementsWithDataId[id].addEventListener('click', this.onClick); - }); + if ((!dataId && !oldValue) || dataId === oldValue) return; - const childrenId = Object.keys(this.elementsWithDataId); - if (childrenId.length === elementsWithDataId.length) return; + const attributeRemoved = !dataId && oldValue; + if (attributeRemoved) { + this.clearElement(oldValue); + return; + } - childrenId.forEach((id) => { - if (dataIdList.has(id)) return; - this.elementsWithDataId[id].style.backgroundColor = ''; - this.elementsWithDataId[id].style.cursor = 'default'; + if (oldValue && this.elementsWithDataId[oldValue]) { + this.clearElement(oldValue); + } - this.elementsWithDataId[id].removeEventListener('click', this.onClick); - delete this.elementsWithDataId[id]; + this.setElementReadyToPin(target as HTMLElement, dataId); }); }; + private observeContainer() { + const mutationObserver = new MutationObserver(this.handleObserverChanges); + + mutationObserver.observe(this.container, { + subtree: true, + attributes: true, + attributeFilter: ['data-superviz-id'], + attributeOldValue: true, + }); + } + + private setElementReadyToPin(element: HTMLElement, id: string): void { + if (this.elementsWithDataId[id]) return; + + this.elementsWithDataId[id] = element; + this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; + this.elementsWithDataId[id].addEventListener('click', this.onClick); + } + + private getElementsReady(): void { + const elementsWithDataId = this.container.querySelectorAll('[data-superviz-id]'); + + elementsWithDataId.forEach((el: HTMLElement) => { + const id = el.getAttribute('data-superviz-id'); + this.setElementReadyToPin(el, id); + }); + } + + private setAddCursor() { + Object.values(this.elementsWithDataId).forEach((el) => { + const element = el; + element.style.cursor = 'url("") 0 100, pointer'; + }); + } + + private removeAddCursor() { + Object.values(this.elementsWithDataId).forEach((el) => { + const element = el; + element.style.cursor = 'default'; + }); + } + /** * @function setPinsVisibility * @param {boolean} isVisible - Controls the visibility of the pins. @@ -102,11 +148,13 @@ export class HTMLPin implements PinAdapter { if (this.isActive) { this.addListeners(); - this.setElementsReadyToPin(); + this.setAddCursor(); + this.getElementsReady(); return; } this.removeListeners(); + this.removeAddCursor(); } /** @@ -141,7 +189,6 @@ export class HTMLPin implements PinAdapter { private addListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => { this.elementsWithDataId[id].addEventListener('click', this.onClick); - this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; }); } @@ -156,9 +203,8 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ private removeListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => { - this.elementsWithDataId[id].removeEventListener('click', this.onClick); - this.elementsWithDataId[id].setAttribute('style', ''); + Object.values(this.elementsWithDataId).forEach((el) => { + el.removeEventListener('click', this.onClick); }); } @@ -173,10 +219,6 @@ export class HTMLPin implements PinAdapter { this.renderDivWrapper(); } - if (this.isActive) { - this.setElementsReadyToPin(); - } - if (this.temporaryPinCoordinates) { this.renderTemporaryPin(); } From 84766768e6f591a803f1c1235e800c889b1a6987 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 14:17:04 -0300 Subject: [PATCH 03/70] feat: add temporary pin when clicking on element --- .../comments/html-pin-adapter/index.ts | 126 +++++++++++++++--- .../comments/html-pin-adapter/types.ts | 11 ++ src/components/comments/types.ts | 1 + .../comments/css/comment-input.style.ts | 1 + 4 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 9ef3c748..68925b95 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -1,7 +1,8 @@ import { Logger, Observer } from '../../../common/utils'; -import { Annotation, PinAdapter } from '../types'; +import { PinMode } from '../../../web-components/comments/components/types'; +import { Annotation, PinAdapter, PinCoordinates } from '../types'; -import { SimpleParticipant } from './types'; +import { HorizontalSide, Simple2DPoint, SimpleParticipant, TemporaryPinData } from './types'; export class HTMLPin implements PinAdapter { private logger: Logger; @@ -10,24 +11,28 @@ export class HTMLPin implements PinAdapter { private isPinsVisible: boolean = true; private annotations: Annotation[]; private animateFrame: number; + private mouseDownCoordinates: Simple2DPoint; public onPinFixedObserver: Observer; - private temporaryPinCoordinates: { x: number; y: number } | null = null; + private commentsSide: HorizontalSide = 'left'; + private temporaryPinCoordinates: TemporaryPinData; private localParticipant: SimpleParticipant = {}; private elementsWithDataId: Record = {}; private divWrappers: HTMLElement[]; + private pins: Map; + private movedTemporaryPin: boolean; constructor(containerId: string) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; - this.isActive = false; - if (!this.container) { const message = `Element with id ${containerId} not found`; this.logger.log(message); throw new Error(message); } + this.isActive = false; this.observeContainer(); + this.pins = new Map(); this.onPinFixedObserver = new Observer({ logger: this.logger }); this.divWrappers = this.renderDivWrapper(); this.annotations = []; @@ -44,6 +49,7 @@ export class HTMLPin implements PinAdapter { public destroy(): void { this.removeListeners(); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); + this.pins = new Map(); this.onPinFixedObserver.destroy(); this.onPinFixedObserver = null; this.annotations = []; @@ -56,7 +62,7 @@ export class HTMLPin implements PinAdapter { if (!element) return; element.style.cursor = 'default'; - element.removeEventListener('click', this.onClick); + this.removeElementListeners(id); delete this.elementsWithDataId[id]; } @@ -99,7 +105,7 @@ export class HTMLPin implements PinAdapter { this.elementsWithDataId[id] = element; this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; - this.elementsWithDataId[id].addEventListener('click', this.onClick); + this.addElementListeners(id); } private getElementsReady(): void { @@ -173,13 +179,66 @@ export class HTMLPin implements PinAdapter { this.renderAnnotationsPins(); } + /** + * @function setMouseDownCoordinates + * @description stores the mouse down coordinates + * @param {MouseEvent} event - The mouse event object. + * @returns {void} + */ + private setMouseDownCoordinates = ({ x, y }: MouseEvent) => { + this.mouseDownCoordinates = { x, y }; + }; + /** * @function renderTemporaryPin * @description creates a temporary pin with the id temporary-pin to mark where the annotation is being created */ - public renderTemporaryPin(): void {} + public renderTemporaryPin(elementId?: string): void { + let temporaryPin = this.container.querySelector('#superviz-temporary-pin') as HTMLElement; + + if (!temporaryPin) { + const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); + + temporaryPin = document.createElement('superviz-comments-annotation-pin'); + temporaryPin.id = 'superviz-temporary-pin'; + temporaryPin.setAttribute('type', PinMode.ADD); + temporaryPin.setAttribute('showInput', ''); + temporaryPin.setAttribute('containerSides', JSON.stringify(elementSides)); + temporaryPin.setAttribute('commentsSide', this.commentsSide); + temporaryPin.setAttribute('position', JSON.stringify(this.temporaryPinCoordinates)); + temporaryPin.setAttribute('annotation', JSON.stringify({})); + temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); + temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); + temporaryPin.setAttributeNode(document.createAttribute('active')); + this.elementsWithDataId[elementId].appendChild(temporaryPin); + } + + if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { + const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); + this.elementsWithDataId[this.temporaryPinCoordinates.elementId]?.removeChild(temporaryPin); + this.elementsWithDataId[elementId].appendChild(temporaryPin); + this.temporaryPinCoordinates.elementId = elementId; + temporaryPin.setAttribute('containerSides', JSON.stringify(elementSides)); + } + + const { x, y } = this.temporaryPinCoordinates; + + temporaryPin.setAttribute('position', JSON.stringify({ x, y })); + + this.pins.set('temporary-pin', temporaryPin); + } + + private addElementListeners(id: string): void { + this.elementsWithDataId[id].addEventListener('click', this.onClick, true); + this.elementsWithDataId[id].addEventListener('mousedown', this.setMouseDownCoordinates); + } + + private removeElementListeners(id: string): void { + this.elementsWithDataId[id].removeEventListener('click', this.onClick, true); + this.elementsWithDataId[id].removeEventListener('mousedown', this.setMouseDownCoordinates); + } /** * @function addListeners @@ -187,12 +246,11 @@ export class HTMLPin implements PinAdapter { * @returns {void} */ private addListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => { - this.elementsWithDataId[id].addEventListener('click', this.onClick); - }); + Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); } public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { + this.commentsSide = side; this.localParticipant.avatar = avatar; this.localParticipant.name = name; }; @@ -203,9 +261,7 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ private removeListeners(): void { - Object.values(this.elementsWithDataId).forEach((el) => { - el.removeEventListener('click', this.onClick); - }); + Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); } /** @@ -219,9 +275,9 @@ export class HTMLPin implements PinAdapter { this.renderDivWrapper(); } - if (this.temporaryPinCoordinates) { - this.renderTemporaryPin(); - } + // if (this.temporaryPinCoordinates && this.isActive) { + // this.renderTemporaryPin(); + // } requestAnimationFrame(this.animate); }; @@ -264,7 +320,41 @@ export class HTMLPin implements PinAdapter { this.annotations.forEach((annotation) => {}); } - private onClick = (event: MouseEvent): void => {}; + private onClick = (event: MouseEvent): void => { + if (!this.isActive || event.target === this.pins.get('temporary-pin')) return; + const clickedElement = event.currentTarget as HTMLElement; + const elementId = clickedElement.getAttribute('data-superviz-id'); + const rect = clickedElement.getBoundingClientRect(); + const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + const originalX = mouseDownX - rect.x; + const originalY = mouseDownY - rect.y; + + const distance = Math.hypot(x - originalX, y - originalY); + if (distance > 10) return; + + this.onPinFixedObserver.publish({ + x, + y, + type: 'html', + elementId, + } as PinCoordinates); + + this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; + this.renderTemporaryPin(elementId); + + const temporaryPin = this.container.querySelector('#superviz-temporary-pin'); + + // we don't care about the actual movedTemporaryPin value + // it only needs to trigger an update + this.movedTemporaryPin = !this.movedTemporaryPin; + temporaryPin.setAttribute('movedPosition', String(this.movedTemporaryPin)); + + // if (this.selectedPin) return; + // document.body.dispatchEvent(new CustomEvent('unselect-annotation')); + }; public removeAnnotationPin(uuid: string): void {} } diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index 481eabb6..cd64472b 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -6,3 +6,14 @@ export interface SimpleParticipant { name?: string; avatar?: string; } + +export interface Simple2DPoint { + x: number; + y: number; +} + +export interface TemporaryPinData extends Simple2DPoint { + elementId: string; +} + +export type HorizontalSide = 'left' | 'right'; diff --git a/src/components/comments/types.ts b/src/components/comments/types.ts index f5f7110a..29f57382 100644 --- a/src/components/comments/types.ts +++ b/src/components/comments/types.ts @@ -33,6 +33,7 @@ export interface PinCoordinates { x: number; y: number; z?: number; + elementId?: string; type: 'canvas' | 'matterport' | 'threejs' | 'autodesk' | 'html'; } diff --git a/src/web-components/comments/css/comment-input.style.ts b/src/web-components/comments/css/comment-input.style.ts index 2fc61361..c13516ae 100644 --- a/src/web-components/comments/css/comment-input.style.ts +++ b/src/web-components/comments/css/comment-input.style.ts @@ -21,6 +21,7 @@ export const commentInputStyle = css` #comment-input--textarea { all: unset; border: 0px; + text-align: left; border-radius: 4px; outline: none; font-size: 14px; From 579ea564dd5dde4efc9680d6b5a4965e14528809 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 18:00:24 -0300 Subject: [PATCH 04/70] feat: add pin to element --- .../comments/canvas-pin-adapter/index.ts | 2 +- .../comments/html-pin-adapter/index.ts | 120 ++++++++++++++---- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index 8528af80..6a6528ff 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -303,7 +303,7 @@ export class CanvasPin implements PinAdapter { * @returns {void} */ private renderAnnotationsPins(): void { - if (!this.annotations || this.canvas.style.display === 'none') { + if (!this.annotations.length || this.canvas.style.display === 'none') { this.removeAnnotationsPins(); return; } diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 68925b95..9082a0f1 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -17,7 +17,7 @@ export class HTMLPin implements PinAdapter { private temporaryPinCoordinates: TemporaryPinData; private localParticipant: SimpleParticipant = {}; private elementsWithDataId: Record = {}; - private divWrappers: HTMLElement[]; + private divWrappers: Map; private pins: Map; private movedTemporaryPin: boolean; @@ -100,9 +100,37 @@ export class HTMLPin implements PinAdapter { }); } + private setDivWrapper(element: HTMLElement, id: string): HTMLElement { + const container = element; + const containerRect = container.getBoundingClientRect(); + + const divWrapper = document.createElement('div'); + const dataSupervizId = container.getAttribute('data-superviz-id'); + const wrapperId = `superviz-id-${dataSupervizId}`; + divWrapper.id = wrapperId; + + this.container.parentElement.style.position = 'relative'; + + divWrapper.style.position = 'fixed'; + divWrapper.style.top = `${containerRect.top}px`; + divWrapper.style.left = `${containerRect.left}px`; + divWrapper.style.width = `${containerRect.width}px`; + divWrapper.style.height = `${containerRect.height}px`; + divWrapper.style.pointerEvents = 'none'; + divWrapper.style.overflow = 'hidden'; + + if (!this.container.querySelector(`#${wrapperId}`)) { + container.parentElement.appendChild(divWrapper); + } + + return divWrapper; + } + private setElementReadyToPin(element: HTMLElement, id: string): void { if (this.elementsWithDataId[id]) return; + const divWrapper = this.setDivWrapper(element, id); + this.divWrappers.set(id, divWrapper); this.elementsWithDataId[id] = element; this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; this.addElementListeners(id); @@ -161,6 +189,22 @@ export class HTMLPin implements PinAdapter { this.removeListeners(); this.removeAddCursor(); + + delete this.divWrappers; + this.divWrappers = new Map(); + } + + /** + * @function removeAnnotationsPins + * @description clears all pins from the canvas. + * @returns {void} + */ + private removeAnnotationsPins(): void { + this.pins.forEach((pinElement) => { + pinElement.remove(); + }); + + this.pins.clear(); } /** @@ -176,6 +220,7 @@ export class HTMLPin implements PinAdapter { if (!this.isActive && !this.isPinsVisible) return; + this.removeAnnotationsPins(); this.renderAnnotationsPins(); } @@ -286,38 +331,61 @@ export class HTMLPin implements PinAdapter { * @function renderDivWrapper * @description Creates a div wrapper for the pins over each valid element. * */ - private renderDivWrapper(): HTMLElement[] { - const divWrappers: HTMLElement[] = Object.values(this.elementsWithDataId).map((el) => { - const container = el; - const containerRect = container.getBoundingClientRect(); - - const divWrapper = document.createElement('div'); - const dataSupervizId = container.getAttribute('data-superviz-id'); - const id = `superviz-id-${dataSupervizId}`; - divWrapper.id = id; - - this.container.parentElement.style.position = 'relative'; - - divWrapper.style.position = 'fixed'; - divWrapper.style.top = `${containerRect.top}px`; - divWrapper.style.left = `${containerRect.left}px`; - divWrapper.style.width = `${containerRect.width}px`; - divWrapper.style.height = `${containerRect.height}px`; - divWrapper.style.pointerEvents = 'none'; - divWrapper.style.overflow = 'hidden'; - - if (!this.container.querySelector(`#${id}`)) { - container.parentElement.appendChild(divWrapper); - } + private renderDivWrapper(): Map { + const divWrappers: Map = new Map(); - return divWrapper; + Object.entries(this.elementsWithDataId).forEach(([id, el]) => { + const divWrapper = this.setDivWrapper(el, id); + divWrappers.set(id, divWrapper); }); return divWrappers; } private renderAnnotationsPins(): void { - this.annotations.forEach((annotation) => {}); + if (!this.annotations.length) { + // this.removeAnnotationsPins(); + return; + } + + this.annotations.forEach((annotation) => { + if (annotation.resolved) { + return; + } + + const { x, y, elementId, type } = JSON.parse(annotation.position) as PinCoordinates; + if (type !== 'html') return; + + const element = this.elementsWithDataId[elementId]; + if (!element) return; + + const wrapper = this.divWrappers.get(elementId); + if (!wrapper) return; + + if (this.pins.has(annotation.uuid)) { + const pin = this.pins.get(annotation.uuid); + + const isVisible = wrapper.clientWidth > x && wrapper.clientHeight > y; + + if (isVisible) { + pin.setAttribute('style', 'opacity: 1'); + + this.pins.get(annotation.uuid).setAttribute('position', JSON.stringify({ x, y })); + return; + } + + pin.setAttribute('style', 'opacity: 0'); + } + + const pinElement = document.createElement('superviz-comments-annotation-pin'); + pinElement.setAttribute('type', PinMode.SHOW); + pinElement.setAttribute('annotation', JSON.stringify(annotation)); + pinElement.setAttribute('position', JSON.stringify({ x, y })); + pinElement.id = annotation.uuid; + + wrapper.appendChild(pinElement); + this.pins.set(annotation.uuid, pinElement); + }); } private onClick = (event: MouseEvent): void => { From 6eb16f05ff5ffe9f087ac47c418b601ab7793f15 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 18:06:27 -0300 Subject: [PATCH 05/70] feat: remove temporary pin when pressing Escape --- .../comments/html-pin-adapter/index.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 9082a0f1..f99d33ba 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -126,6 +126,21 @@ export class HTMLPin implements PinAdapter { return divWrapper; } + /** + * @function resetPins + * @description Unselects selected pin and removes temporary pin. + * @param {that} this - The canvas pin adapter instance. + * @param {KeyboardEvent} event - The keyboard event object. + * @returns {void} + * */ + private resetPins = (event?: KeyboardEvent): void => { + if (event && event?.key !== 'Escape') return; + + // this.resetSelectedPin(); + this.removeAnnotationPin('temporary-pin'); + this.temporaryPinCoordinates = null; + }; + private setElementReadyToPin(element: HTMLElement, id: string): void { if (this.elementsWithDataId[id]) return; const divWrapper = this.setDivWrapper(element, id); @@ -292,6 +307,7 @@ export class HTMLPin implements PinAdapter { */ private addListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); + document.body.addEventListener('keyup', this.resetPins); } public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { @@ -307,6 +323,7 @@ export class HTMLPin implements PinAdapter { * */ private removeListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); + document.body.removeEventListener('keyup', this.resetPins); } /** @@ -424,5 +441,19 @@ export class HTMLPin implements PinAdapter { // document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; - public removeAnnotationPin(uuid: string): void {} + /** + * @function removeAnnotationPin + * @description Removes an annotation pin from the canvas. + * @param {string} uuid - The uuid of the annotation to be removed. + * @returns {void} + * */ + public removeAnnotationPin(uuid: string): void { + const pinElement = this.pins.get(uuid); + + if (!pinElement) return; + + pinElement.remove(); + this.pins.delete(uuid); + this.annotations = this.annotations.filter((annotation) => annotation.uuid !== uuid); + } } From c40ab04d54c03374475b0928ca063ce955c28cc8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 18:11:58 -0300 Subject: [PATCH 06/70] feat: focus on pin when creating on clicking on pin/annotation --- .../comments/html-pin-adapter/index.ts | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index f99d33ba..ff5867e5 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -11,6 +11,7 @@ export class HTMLPin implements PinAdapter { private isPinsVisible: boolean = true; private annotations: Annotation[]; private animateFrame: number; + private selectedPin: HTMLElement | null = null; private mouseDownCoordinates: Simple2DPoint; public onPinFixedObserver: Observer; private commentsSide: HorizontalSide = 'left'; @@ -57,6 +58,28 @@ export class HTMLPin implements PinAdapter { cancelAnimationFrame(this.animateFrame); } + /** + * @function addListeners + * @description adds event listeners to the container element. + * @returns {void} + */ + private addListeners(): void { + Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); + document.body.addEventListener('keyup', this.resetPins); + document.body.addEventListener('select-annotation', this.annotationSelected); + } + + /** + * @function removeListeners + * @description removes event listeners from the container element. + * @returns {void} + * */ + private removeListeners(): void { + Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); + document.body.removeEventListener('keyup', this.resetPins); + document.body.removeEventListener('select-annotation', this.annotationSelected); + } + private clearElement(id: string) { const element = this.elementsWithDataId[id]; if (!element) return; @@ -126,6 +149,17 @@ export class HTMLPin implements PinAdapter { return divWrapper; } + /** + * @function resetSelectedPin + * @description Unselects a pin by removing its 'active' attribute + * @returns {void} + * */ + private resetSelectedPin(): void { + if (!this.selectedPin) return; + this.selectedPin.removeAttribute('active'); + this.selectedPin = null; + } + /** * @function resetPins * @description Unselects selected pin and removes temporary pin. @@ -136,7 +170,7 @@ export class HTMLPin implements PinAdapter { private resetPins = (event?: KeyboardEvent): void => { if (event && event?.key !== 'Escape') return; - // this.resetSelectedPin(); + this.resetSelectedPin(); this.removeAnnotationPin('temporary-pin'); this.temporaryPinCoordinates = null; }; @@ -249,6 +283,33 @@ export class HTMLPin implements PinAdapter { this.mouseDownCoordinates = { x, y }; }; + /** + * @function annotationSelected + * @description highlights the selected annotation and scrolls to it + * @param {CustomEvent} event + * @returns {void} + */ + private annotationSelected = ({ detail: { uuid } }: CustomEvent): void => { + if (!uuid) return; + + const annotation = JSON.parse(this.selectedPin?.getAttribute('annotation') ?? '{}'); + + this.resetPins(); + + if (annotation?.uuid === uuid) return; + + document.body.dispatchEvent(new CustomEvent('close-temporary-annotation')); + + const pinElement = this.pins.get(uuid); + + if (!pinElement) return; + + pinElement.setAttribute('active', ''); + + this.selectedPin = pinElement; + // this.goToPin(uuid); + }; + /** * @function renderTemporaryPin * @description @@ -300,32 +361,12 @@ export class HTMLPin implements PinAdapter { this.elementsWithDataId[id].removeEventListener('mousedown', this.setMouseDownCoordinates); } - /** - * @function addListeners - * @description adds event listeners to the container element. - * @returns {void} - */ - private addListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); - document.body.addEventListener('keyup', this.resetPins); - } - public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { this.commentsSide = side; this.localParticipant.avatar = avatar; this.localParticipant.name = name; }; - /** - * @function removeListeners - * @description removes event listeners from the container element. - * @returns {void} - * */ - private removeListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); - document.body.removeEventListener('keyup', this.resetPins); - } - /** * @function animate * @description animation frame @@ -427,6 +468,7 @@ export class HTMLPin implements PinAdapter { elementId, } as PinCoordinates); + this.resetSelectedPin(); this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; this.renderTemporaryPin(elementId); @@ -437,8 +479,8 @@ export class HTMLPin implements PinAdapter { this.movedTemporaryPin = !this.movedTemporaryPin; temporaryPin.setAttribute('movedPosition', String(this.movedTemporaryPin)); - // if (this.selectedPin) return; - // document.body.dispatchEvent(new CustomEvent('unselect-annotation')); + if (this.selectedPin) return; + document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; /** From 7a546dacfe80d15d5548f5cc997deddca4483c98 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 3 Jan 2024 19:59:44 -0300 Subject: [PATCH 07/70] feat: display or hide pins according to changes in data-superviz-id --- .../comments/html-pin-adapter/index.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index ff5867e5..aea90cae 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -37,6 +37,7 @@ export class HTMLPin implements PinAdapter { this.onPinFixedObserver = new Observer({ logger: this.logger }); this.divWrappers = this.renderDivWrapper(); this.annotations = []; + this.getElementsReady(); this.renderAnnotationsPins(); this.animateFrame = requestAnimationFrame(this.animate); @@ -50,6 +51,7 @@ export class HTMLPin implements PinAdapter { public destroy(): void { this.removeListeners(); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); + this.divWrappers.clear(); this.pins = new Map(); this.onPinFixedObserver.destroy(); this.onPinFixedObserver = null; @@ -84,14 +86,19 @@ export class HTMLPin implements PinAdapter { const element = this.elementsWithDataId[id]; if (!element) return; + const pins = this.divWrappers.get(id).children; + for (let i = 0; i < pins.length; ++i) { + const pin = pins.item(i); + this.pins.delete(pin.id); + pin.remove(); + } + element.style.cursor = 'default'; this.removeElementListeners(id); delete this.elementsWithDataId[id]; } private handleObserverChanges = (changes: MutationRecord[]): void => { - if (!this.isActive) return; - changes.forEach((change) => { const { target, oldValue } = change; const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); @@ -128,8 +135,7 @@ export class HTMLPin implements PinAdapter { const containerRect = container.getBoundingClientRect(); const divWrapper = document.createElement('div'); - const dataSupervizId = container.getAttribute('data-superviz-id'); - const wrapperId = `superviz-id-${dataSupervizId}`; + const wrapperId = `superviz-id-${id}`; divWrapper.id = wrapperId; this.container.parentElement.style.position = 'relative'; @@ -177,10 +183,14 @@ export class HTMLPin implements PinAdapter { private setElementReadyToPin(element: HTMLElement, id: string): void { if (this.elementsWithDataId[id]) return; - const divWrapper = this.setDivWrapper(element, id); + if (!this.divWrappers.get(id)) { + const divWrapper = this.setDivWrapper(element, id); + this.divWrappers.set(id, divWrapper); + } - this.divWrappers.set(id, divWrapper); this.elementsWithDataId[id] = element; + + if (!this.isActive || !this.isPinsVisible) return; this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; this.addElementListeners(id); } @@ -238,9 +248,6 @@ export class HTMLPin implements PinAdapter { this.removeListeners(); this.removeAddCursor(); - - delete this.divWrappers; - this.divWrappers = new Map(); } /** @@ -402,7 +409,7 @@ export class HTMLPin implements PinAdapter { private renderAnnotationsPins(): void { if (!this.annotations.length) { - // this.removeAnnotationsPins(); + this.removeAnnotationsPins(); return; } @@ -433,6 +440,7 @@ export class HTMLPin implements PinAdapter { } pin.setAttribute('style', 'opacity: 0'); + return; } const pinElement = document.createElement('superviz-comments-annotation-pin'); From bfe9c1520aa16d50d4b862370f43f38af947f14c Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 4 Jan 2024 08:21:01 -0300 Subject: [PATCH 08/70] feat: adapt wrapper size to element size --- .../comments/html-pin-adapter/index.ts | 44 ++++++++++++++++--- .../comments/css/annotation-pin.style.ts | 1 + .../comments/css/comments.style.ts | 1 + 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index aea90cae..3f534f02 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -21,6 +21,8 @@ export class HTMLPin implements PinAdapter { private divWrappers: Map; private pins: Map; private movedTemporaryPin: boolean; + private resizeObserver: ResizeObserver; + private mutationObserver: MutationObserver; constructor(containerId: string) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); @@ -32,12 +34,18 @@ export class HTMLPin implements PinAdapter { } this.isActive = false; + this.divWrappers = this.renderDivWrapper(); + this.getElementsReady(); + + this.mutationObserver = new MutationObserver(this.handleMutationObserverChanges); + this.resizeObserver = new ResizeObserver(this.handleResizeObserverChanges); + this.observeContainer(); + this.observeElements(); + this.pins = new Map(); this.onPinFixedObserver = new Observer({ logger: this.logger }); - this.divWrappers = this.renderDivWrapper(); this.annotations = []; - this.getElementsReady(); this.renderAnnotationsPins(); this.animateFrame = requestAnimationFrame(this.animate); @@ -50,6 +58,7 @@ export class HTMLPin implements PinAdapter { * */ public destroy(): void { this.removeListeners(); + this.removeObservers(); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); this.divWrappers.clear(); this.pins = new Map(); @@ -98,7 +107,7 @@ export class HTMLPin implements PinAdapter { delete this.elementsWithDataId[id]; } - private handleObserverChanges = (changes: MutationRecord[]): void => { + private handleMutationObserverChanges = (changes: MutationRecord[]): void => { changes.forEach((change) => { const { target, oldValue } = change; const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); @@ -119,10 +128,32 @@ export class HTMLPin implements PinAdapter { }); }; - private observeContainer() { - const mutationObserver = new MutationObserver(this.handleObserverChanges); + private removeObservers() { + this.mutationObserver.disconnect(); + this.resizeObserver.disconnect(); + } - mutationObserver.observe(this.container, { + private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { + changes.forEach((change) => { + const element = change.target; + const elementId = element.getAttribute('data-superviz-id'); + const elementRect = element.getBoundingClientRect(); + const wrapper = this.divWrappers.get(elementId); + wrapper.style.top = `${elementRect.top}px`; + wrapper.style.left = `${elementRect.left}px`; + wrapper.style.width = `${elementRect.width}px`; + wrapper.style.height = `${elementRect.height}px`; + }); + }; + + private observeElements() { + Object.values(this.elementsWithDataId).forEach((element) => { + this.resizeObserver.observe(element); + }); + } + + private observeContainer() { + this.mutationObserver.observe(this.container, { subtree: true, attributes: true, attributeFilter: ['data-superviz-id'], @@ -243,6 +274,7 @@ export class HTMLPin implements PinAdapter { this.addListeners(); this.setAddCursor(); this.getElementsReady(); + this.observeElements(); return; } diff --git a/src/web-components/comments/css/annotation-pin.style.ts b/src/web-components/comments/css/annotation-pin.style.ts index cfceb788..12b2e0ac 100644 --- a/src/web-components/comments/css/annotation-pin.style.ts +++ b/src/web-components/comments/css/annotation-pin.style.ts @@ -8,6 +8,7 @@ export const annotationPinStyles = css` justify-content: center; position: relative; pointer-events: auto; + z-index: 10; } .annotation-pin { diff --git a/src/web-components/comments/css/comments.style.ts b/src/web-components/comments/css/comments.style.ts index 6dd4f7b8..2ce4a71f 100644 --- a/src/web-components/comments/css/comments.style.ts +++ b/src/web-components/comments/css/comments.style.ts @@ -12,6 +12,7 @@ export const commentsStyle = css` bottom: 0; box-shadow: -2px 0 4px 0 rgba(0, 0, 0, 0.1); height: 100%; + z-index: 20; } .container-close { From 14967e001d9dfeef1202193b30f8d043c3c0a8cf Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 4 Jan 2024 09:43:07 -0300 Subject: [PATCH 09/70] fix: append temporary pin to wrapper instead of element --- src/components/comments/html-pin-adapter/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3f534f02..3b544d86 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -96,8 +96,9 @@ export class HTMLPin implements PinAdapter { if (!element) return; const pins = this.divWrappers.get(id).children; - for (let i = 0; i < pins.length; ++i) { - const pin = pins.item(i); + const { length } = pins; + for (let i = 0; i < length; ++i) { + const pin = pins.item(0); this.pins.delete(pin.id); pin.remove(); } @@ -178,6 +179,7 @@ export class HTMLPin implements PinAdapter { divWrapper.style.height = `${containerRect.height}px`; divWrapper.style.pointerEvents = 'none'; divWrapper.style.overflow = 'hidden'; + divWrapper.style.zIndex = '10'; if (!this.container.querySelector(`#${wrapperId}`)) { container.parentElement.appendChild(divWrapper); @@ -372,13 +374,13 @@ export class HTMLPin implements PinAdapter { temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); temporaryPin.setAttributeNode(document.createAttribute('active')); - this.elementsWithDataId[elementId].appendChild(temporaryPin); + this.divWrappers.get(elementId).appendChild(temporaryPin); } if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); - this.elementsWithDataId[this.temporaryPinCoordinates.elementId]?.removeChild(temporaryPin); - this.elementsWithDataId[elementId].appendChild(temporaryPin); + this.divWrappers.get(elementId)?.removeChild(temporaryPin); + this.divWrappers.get(elementId).appendChild(temporaryPin); this.temporaryPinCoordinates.elementId = elementId; temporaryPin.setAttribute('containerSides', JSON.stringify(elementSides)); } From 8203a5acf2ce670612df6df9d22e89e1e790061b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 4 Jan 2024 21:58:11 -0300 Subject: [PATCH 10/70] fix: render temporary pin in other wrapper so input that overflows is not hidden --- .../comments/html-pin-adapter/index.ts | 177 ++++++++++++------ .../comments/html-pin-adapter/types.ts | 8 +- 2 files changed, 124 insertions(+), 61 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3b544d86..f7a0d159 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -94,8 +94,9 @@ export class HTMLPin implements PinAdapter { private clearElement(id: string) { const element = this.elementsWithDataId[id]; if (!element) return; - - const pins = this.divWrappers.get(id).children; + const wrapper = this.divWrappers.get(id); + const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; + const pins = pinsWrapper.children; const { length } = pins; for (let i = 0; i < length; ++i) { const pin = pins.item(0); @@ -103,7 +104,7 @@ export class HTMLPin implements PinAdapter { pin.remove(); } - element.style.cursor = 'default'; + wrapper.style.cursor = 'default'; this.removeElementListeners(id); delete this.elementsWithDataId[id]; } @@ -144,6 +145,15 @@ export class HTMLPin implements PinAdapter { wrapper.style.left = `${elementRect.left}px`; wrapper.style.width = `${elementRect.width}px`; wrapper.style.height = `${elementRect.height}px`; + const subwrappers = wrapper.children; + + for (let i = 0; i < subwrappers.length; ++i) { + const subwrapper = subwrappers.item(i) as HTMLElement; + subwrapper.style.top = `0`; + subwrapper.style.left = `0`; + subwrapper.style.width = `${elementRect.width}px`; + subwrapper.style.height = `${elementRect.height}px`; + } }); }; @@ -162,30 +172,40 @@ export class HTMLPin implements PinAdapter { }); } - private setDivWrapper(element: HTMLElement, id: string): HTMLElement { + private setWrappers(element: HTMLElement, id: string): HTMLElement { const container = element; - const containerRect = container.getBoundingClientRect(); - - const divWrapper = document.createElement('div'); const wrapperId = `superviz-id-${id}`; - divWrapper.id = wrapperId; - - this.container.parentElement.style.position = 'relative'; - - divWrapper.style.position = 'fixed'; - divWrapper.style.top = `${containerRect.top}px`; - divWrapper.style.left = `${containerRect.left}px`; - divWrapper.style.width = `${containerRect.width}px`; - divWrapper.style.height = `${containerRect.height}px`; - divWrapper.style.pointerEvents = 'none'; - divWrapper.style.overflow = 'hidden'; - divWrapper.style.zIndex = '10'; - - if (!this.container.querySelector(`#${wrapperId}`)) { - container.parentElement.appendChild(divWrapper); + if (container.querySelector(`#${wrapperId}`)) { + return; } - return divWrapper; + const containerRect = container.getBoundingClientRect(); + + const containerWrapper = document.createElement('div'); + containerWrapper.setAttribute('data-wrapper-id', id); + containerWrapper.id = wrapperId; + this.container.style.position ||= 'relative'; + + containerWrapper.style.position = 'fixed'; + containerWrapper.style.top = `${containerRect.top}px`; + containerWrapper.style.left = `${containerRect.left}px`; + containerWrapper.style.width = `${containerRect.width}px`; + containerWrapper.style.height = `${containerRect.height}px`; + + // containerWrapper.style.zIndex = '10'; + + const pinsWrapper = document.createElement('div'); + pinsWrapper.setAttribute('data-pins-wrapper', ''); + pinsWrapper.style.position = 'absolute'; + pinsWrapper.style.overflow = 'hidden'; + pinsWrapper.style.top = '0'; + pinsWrapper.style.left = '0'; + pinsWrapper.style.width = '100%'; + pinsWrapper.style.height = '100%'; + containerWrapper.appendChild(pinsWrapper); + + this.container.appendChild(containerWrapper); + return containerWrapper; } /** @@ -202,7 +222,6 @@ export class HTMLPin implements PinAdapter { /** * @function resetPins * @description Unselects selected pin and removes temporary pin. - * @param {that} this - The canvas pin adapter instance. * @param {KeyboardEvent} event - The keyboard event object. * @returns {void} * */ @@ -210,21 +229,25 @@ export class HTMLPin implements PinAdapter { if (event && event?.key !== 'Escape') return; this.resetSelectedPin(); + + if (!this.temporaryPinCoordinates.elementId) return; + this.removeAnnotationPin('temporary-pin'); - this.temporaryPinCoordinates = null; + this.temporaryPinContainer.remove(); + this.temporaryPinCoordinates = {}; }; private setElementReadyToPin(element: HTMLElement, id: string): void { if (this.elementsWithDataId[id]) return; if (!this.divWrappers.get(id)) { - const divWrapper = this.setDivWrapper(element, id); + const divWrapper = this.setWrappers(element, id); this.divWrappers.set(id, divWrapper); } this.elementsWithDataId[id] = element; - + this.resizeObserver.observe(element); if (!this.isActive || !this.isPinsVisible) return; - this.elementsWithDataId[id].style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; this.addElementListeners(id); } @@ -238,16 +261,14 @@ export class HTMLPin implements PinAdapter { } private setAddCursor() { - Object.values(this.elementsWithDataId).forEach((el) => { - const element = el; - element.style.cursor = 'url("") 0 100, pointer'; + Object.keys(this.elementsWithDataId).forEach((id) => { + this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; }); } private removeAddCursor() { - Object.values(this.elementsWithDataId).forEach((el) => { - const element = el; - element.style.cursor = 'default'; + Object.keys(this.elementsWithDataId).forEach((id) => { + this.divWrappers.get(id).style.cursor = 'default'; }); } @@ -276,7 +297,6 @@ export class HTMLPin implements PinAdapter { this.addListeners(); this.setAddCursor(); this.getElementsReady(); - this.observeElements(); return; } @@ -351,6 +371,47 @@ export class HTMLPin implements PinAdapter { // this.goToPin(uuid); }; + private createTemporaryPinContainer(): HTMLDivElement { + const temporaryContainer = document.createElement('div'); + temporaryContainer.style.position = 'absolute'; + temporaryContainer.style.top = '0'; + temporaryContainer.style.left = '0'; + temporaryContainer.style.width = '100%'; + temporaryContainer.style.height = '100%'; + temporaryContainer.id = 'temporary-pin-container'; + return temporaryContainer; + } + + private get temporaryPinContainer(): HTMLDivElement { + return this.divWrappers + .get(this.temporaryPinCoordinates.elementId) + .querySelector('#temporary-pin-container'); + } + + private addTemporaryPinToElement(elementId: string, pin: HTMLElement): void { + const element = this.elementsWithDataId[elementId]; + if (!element) return; + + const wrapper = this.divWrappers.get(elementId); + if (!wrapper) return; + + const temporaryContainer = this.createTemporaryPinContainer(); + temporaryContainer.appendChild(pin); + + wrapper.appendChild(temporaryContainer); + } + + private addPinToElement(elementId: string, pin: HTMLElement): void { + const element = this.elementsWithDataId[elementId]; + if (!element) return; + + const wrapper = this.divWrappers.get(elementId); + if (!wrapper) return; + + wrapper.appendChild(pin); + this.temporaryPinCoordinates.elementId = elementId; + } + /** * @function renderTemporaryPin * @description @@ -358,7 +419,15 @@ export class HTMLPin implements PinAdapter { temporary-pin to mark where the annotation is being created */ public renderTemporaryPin(elementId?: string): void { - let temporaryPin = this.container.querySelector('#superviz-temporary-pin') as HTMLElement; + this.temporaryPinCoordinates.elementId ||= elementId; + let temporaryPin = this.pins.get('temporary-pin'); + + if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { + this.temporaryPinContainer.remove(); + this.pins.delete('temporary-pin'); + this.temporaryPinCoordinates.elementId = elementId; + temporaryPin = null; + } if (!temporaryPin) { const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); @@ -374,15 +443,8 @@ export class HTMLPin implements PinAdapter { temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); temporaryPin.setAttributeNode(document.createAttribute('active')); - this.divWrappers.get(elementId).appendChild(temporaryPin); - } - if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { - const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); - this.divWrappers.get(elementId)?.removeChild(temporaryPin); - this.divWrappers.get(elementId).appendChild(temporaryPin); - this.temporaryPinCoordinates.elementId = elementId; - temporaryPin.setAttribute('containerSides', JSON.stringify(elementSides)); + this.addTemporaryPinToElement(elementId, temporaryPin); } const { x, y } = this.temporaryPinCoordinates; @@ -393,13 +455,18 @@ export class HTMLPin implements PinAdapter { } private addElementListeners(id: string): void { - this.elementsWithDataId[id].addEventListener('click', this.onClick, true); - this.elementsWithDataId[id].addEventListener('mousedown', this.setMouseDownCoordinates); + this.divWrappers.get(id).addEventListener('click', this.onClick, true); + this.divWrappers.get(id).addEventListener('mousedown', this.setMouseDownCoordinates); + // this.elementsWithDataId[id].addEventListener('click', this.onClick, true); + // this.elementsWithDataId[id].addEventListener('mousedown', this.setMouseDownCoordinates); } private removeElementListeners(id: string): void { - this.elementsWithDataId[id].removeEventListener('click', this.onClick, true); - this.elementsWithDataId[id].removeEventListener('mousedown', this.setMouseDownCoordinates); + this.divWrappers.get(id).removeEventListener('click', this.onClick, true); + this.divWrappers.get(id).removeEventListener('mousedown', this.setMouseDownCoordinates); + this.resizeObserver.unobserve(this.elementsWithDataId[id]); + // this.elementsWithDataId[id].removeEventListener('click', this.onClick, true); + // this.elementsWithDataId[id].removeEventListener('mousedown', this.setMouseDownCoordinates); } public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { @@ -416,7 +483,7 @@ export class HTMLPin implements PinAdapter { private animate = (): void => { if (this.isActive || this.isPinsVisible) { this.renderAnnotationsPins(); - this.renderDivWrapper(); + // this.renderDivWrapper(); } // if (this.temporaryPinCoordinates && this.isActive) { @@ -434,7 +501,7 @@ export class HTMLPin implements PinAdapter { const divWrappers: Map = new Map(); Object.entries(this.elementsWithDataId).forEach(([id, el]) => { - const divWrapper = this.setDivWrapper(el, id); + const divWrapper = this.setWrappers(el, id); divWrappers.set(id, divWrapper); }); @@ -458,7 +525,7 @@ export class HTMLPin implements PinAdapter { const element = this.elementsWithDataId[elementId]; if (!element) return; - const wrapper = this.divWrappers.get(elementId); + const wrapper = this.divWrappers.get(elementId).querySelector('[data-pins-wrapper]'); if (!wrapper) return; if (this.pins.has(annotation.uuid)) { @@ -490,9 +557,9 @@ export class HTMLPin implements PinAdapter { private onClick = (event: MouseEvent): void => { if (!this.isActive || event.target === this.pins.get('temporary-pin')) return; - const clickedElement = event.currentTarget as HTMLElement; - const elementId = clickedElement.getAttribute('data-superviz-id'); - const rect = clickedElement.getBoundingClientRect(); + const wrapper = event.currentTarget as HTMLElement; + const elementId = wrapper.getAttribute('data-wrapper-id'); + const rect = wrapper.getBoundingClientRect(); const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; const x = event.clientX - rect.left; const y = event.clientY - rect.top; @@ -514,7 +581,7 @@ export class HTMLPin implements PinAdapter { this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; this.renderTemporaryPin(elementId); - const temporaryPin = this.container.querySelector('#superviz-temporary-pin'); + const temporaryPin = this.divWrappers.get(elementId).querySelector('#superviz-temporary-pin'); // we don't care about the actual movedTemporaryPin value // it only needs to trigger an update diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index cd64472b..d0c3f9a1 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -1,7 +1,3 @@ -export interface CanvasPinAdapterProps { - onGoToPin?: (position: { x: number; y: number }) => void; -} - export interface SimpleParticipant { name?: string; avatar?: string; @@ -12,8 +8,8 @@ export interface Simple2DPoint { y: number; } -export interface TemporaryPinData extends Simple2DPoint { - elementId: string; +export interface TemporaryPinData extends Partial { + elementId?: string; } export type HorizontalSide = 'left' | 'right'; From 6fb0f101e5715ea80416f29e75acb9836e875d93 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Jan 2024 09:58:04 -0300 Subject: [PATCH 11/70] refactor: reestructure, comment and organize code --- .../comments/html-pin-adapter/index.ts | 795 ++++++++++-------- 1 file changed, 450 insertions(+), 345 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index f7a0d159..5bbc9f7a 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -5,28 +5,45 @@ import { Annotation, PinAdapter, PinCoordinates } from '../types'; import { HorizontalSide, Simple2DPoint, SimpleParticipant, TemporaryPinData } from './types'; export class HTMLPin implements PinAdapter { + // Public properties + // Observers + public onPinFixedObserver: Observer; + + // Private properties + // Comments data + private annotations: Annotation[]; + private localParticipant: SimpleParticipant = {}; + + // Loggers private logger: Logger; - private container: HTMLElement; + + // Booleans private isActive: boolean; private isPinsVisible: boolean = true; - private annotations: Annotation[]; - private animateFrame: number; + private movedTemporaryPin: boolean; + + // Data about the current state of the application private selectedPin: HTMLElement | null = null; + + // Coordinates/Positions private mouseDownCoordinates: Simple2DPoint; - public onPinFixedObserver: Observer; private commentsSide: HorizontalSide = 'left'; - private temporaryPinCoordinates: TemporaryPinData; - private localParticipant: SimpleParticipant = {}; + private temporaryPinCoordinates: TemporaryPinData = {}; + + // Elements + private container: HTMLElement; private elementsWithDataId: Record = {}; private divWrappers: Map; private pins: Map; - private movedTemporaryPin: boolean; + + // Observers private resizeObserver: ResizeObserver; private mutationObserver: MutationObserver; constructor(containerId: string) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; + if (!this.container) { const message = `Element with id ${containerId} not found`; this.logger.log(message); @@ -34,12 +51,11 @@ export class HTMLPin implements PinAdapter { } this.isActive = false; - this.divWrappers = this.renderDivWrapper(); - this.getElementsReady(); + this.setDivWrappers(); + this.prepareElements(); this.mutationObserver = new MutationObserver(this.handleMutationObserverChanges); this.resizeObserver = new ResizeObserver(this.handleResizeObserverChanges); - this.observeContainer(); this.observeElements(); @@ -47,10 +63,9 @@ export class HTMLPin implements PinAdapter { this.onPinFixedObserver = new Observer({ logger: this.logger }); this.annotations = []; this.renderAnnotationsPins(); - - this.animateFrame = requestAnimationFrame(this.animate); } + // ------- setup ------- /** * @function destroy * @description destroys the container pin adapter. @@ -60,13 +75,44 @@ export class HTMLPin implements PinAdapter { this.removeListeners(); this.removeObservers(); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); - this.divWrappers.clear(); - this.pins = new Map(); + delete this.divWrappers; + delete this.pins; this.onPinFixedObserver.destroy(); this.onPinFixedObserver = null; this.annotations = []; + } + + /** + * @function addElementListeners + * @description adds event listeners to the element + * @param {string} id the id of the element to add the listeners to. + * @returns {void} + */ + private addElementListeners(id: string): void { + this.divWrappers.get(id).addEventListener('click', this.onClick, true); + this.divWrappers.get(id).addEventListener('mousedown', this.onMouseDown); + } + + /** + * @function removeElementListeners + * @description removes event listeners from the element + * @param {string} id the id of the element to remove the listeners from. + * @returns {void} + */ + private removeElementListeners(id: string, unobserveResize?: boolean): void { + this.divWrappers.get(id).removeEventListener('click', this.onClick, true); + this.divWrappers.get(id).removeEventListener('mousedown', this.onMouseDown); + if (unobserveResize) this.resizeObserver.unobserve(this.elementsWithDataId[id]); + } - cancelAnimationFrame(this.animateFrame); + /** + * @function removeObservers + * @description disconnects the mutation and resize observers. + * @returns {void} + */ + private removeObservers(): void { + this.mutationObserver.disconnect(); + this.resizeObserver.disconnect(); } /** @@ -78,6 +124,7 @@ export class HTMLPin implements PinAdapter { Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); document.body.addEventListener('keyup', this.resetPins); document.body.addEventListener('select-annotation', this.annotationSelected); + document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } /** @@ -86,84 +133,29 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ private removeListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); + Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id, true)); document.body.removeEventListener('keyup', this.resetPins); document.body.removeEventListener('select-annotation', this.annotationSelected); + document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } - private clearElement(id: string) { - const element = this.elementsWithDataId[id]; - if (!element) return; - const wrapper = this.divWrappers.get(id); - const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; - const pins = pinsWrapper.children; - const { length } = pins; - for (let i = 0; i < length; ++i) { - const pin = pins.item(0); - this.pins.delete(pin.id); - pin.remove(); - } - - wrapper.style.cursor = 'default'; - this.removeElementListeners(id); - delete this.elementsWithDataId[id]; - } - - private handleMutationObserverChanges = (changes: MutationRecord[]): void => { - changes.forEach((change) => { - const { target, oldValue } = change; - const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); - - if ((!dataId && !oldValue) || dataId === oldValue) return; - - const attributeRemoved = !dataId && oldValue; - if (attributeRemoved) { - this.clearElement(oldValue); - return; - } - - if (oldValue && this.elementsWithDataId[oldValue]) { - this.clearElement(oldValue); - } - - this.setElementReadyToPin(target as HTMLElement, dataId); - }); - }; - - private removeObservers() { - this.mutationObserver.disconnect(); - this.resizeObserver.disconnect(); - } - - private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { - changes.forEach((change) => { - const element = change.target; - const elementId = element.getAttribute('data-superviz-id'); - const elementRect = element.getBoundingClientRect(); - const wrapper = this.divWrappers.get(elementId); - wrapper.style.top = `${elementRect.top}px`; - wrapper.style.left = `${elementRect.left}px`; - wrapper.style.width = `${elementRect.width}px`; - wrapper.style.height = `${elementRect.height}px`; - const subwrappers = wrapper.children; - - for (let i = 0; i < subwrappers.length; ++i) { - const subwrapper = subwrappers.item(i) as HTMLElement; - subwrapper.style.top = `0`; - subwrapper.style.left = `0`; - subwrapper.style.width = `${elementRect.width}px`; - subwrapper.style.height = `${elementRect.height}px`; - } - }); - }; - - private observeElements() { + /** + * @function observeElements + * @description observes the elements with data-superviz-id attribute. + * @returns {void} + */ + private observeElements(): void { Object.values(this.elementsWithDataId).forEach((element) => { this.resizeObserver.observe(element); }); } - private observeContainer() { + /** + * @function observeContainer + * @description observes the container for changes in the data-superviz-id attribute. + * @returns {void} + */ + private observeContainer(): void { this.mutationObserver.observe(this.container, { subtree: true, attributes: true, @@ -172,86 +164,28 @@ export class HTMLPin implements PinAdapter { }); } - private setWrappers(element: HTMLElement, id: string): HTMLElement { - const container = element; - const wrapperId = `superviz-id-${id}`; - if (container.querySelector(`#${wrapperId}`)) { - return; - } - - const containerRect = container.getBoundingClientRect(); - - const containerWrapper = document.createElement('div'); - containerWrapper.setAttribute('data-wrapper-id', id); - containerWrapper.id = wrapperId; - this.container.style.position ||= 'relative'; - - containerWrapper.style.position = 'fixed'; - containerWrapper.style.top = `${containerRect.top}px`; - containerWrapper.style.left = `${containerRect.left}px`; - containerWrapper.style.width = `${containerRect.width}px`; - containerWrapper.style.height = `${containerRect.height}px`; - - // containerWrapper.style.zIndex = '10'; - - const pinsWrapper = document.createElement('div'); - pinsWrapper.setAttribute('data-pins-wrapper', ''); - pinsWrapper.style.position = 'absolute'; - pinsWrapper.style.overflow = 'hidden'; - pinsWrapper.style.top = '0'; - pinsWrapper.style.left = '0'; - pinsWrapper.style.width = '100%'; - pinsWrapper.style.height = '100%'; - containerWrapper.appendChild(pinsWrapper); - - this.container.appendChild(containerWrapper); - return containerWrapper; - } - /** - * @function resetSelectedPin - * @description Unselects a pin by removing its 'active' attribute - * @returns {void} - * */ - private resetSelectedPin(): void { - if (!this.selectedPin) return; - this.selectedPin.removeAttribute('active'); - this.selectedPin = null; - } - - /** - * @function resetPins - * @description Unselects selected pin and removes temporary pin. - * @param {KeyboardEvent} event - The keyboard event object. + * @function setDivWrappers + * @description sets the wrapper associated to each pinnable element * @returns {void} * */ - private resetPins = (event?: KeyboardEvent): void => { - if (event && event?.key !== 'Escape') return; - - this.resetSelectedPin(); - - if (!this.temporaryPinCoordinates.elementId) return; - - this.removeAnnotationPin('temporary-pin'); - this.temporaryPinContainer.remove(); - this.temporaryPinCoordinates = {}; - }; + private setDivWrappers(): void { + const divWrappers: Map = new Map(); - private setElementReadyToPin(element: HTMLElement, id: string): void { - if (this.elementsWithDataId[id]) return; - if (!this.divWrappers.get(id)) { - const divWrapper = this.setWrappers(element, id); - this.divWrappers.set(id, divWrapper); - } + Object.entries(this.elementsWithDataId).forEach(([id, el]) => { + const divWrapper = this.createWrapper(el, id); + divWrappers.set(id, divWrapper); + }); - this.elementsWithDataId[id] = element; - this.resizeObserver.observe(element); - if (!this.isActive || !this.isPinsVisible) return; - this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; - this.addElementListeners(id); + this.divWrappers = divWrappers; } - private getElementsReady(): void { + /** + * @function prepareElements + * @description set elements with data-superviz-id attribute as pinnable + * @returns {void} + */ + private prepareElements(): void { const elementsWithDataId = this.container.querySelectorAll('[data-superviz-id]'); elementsWithDataId.forEach((el: HTMLElement) => { @@ -260,18 +194,30 @@ export class HTMLPin implements PinAdapter { }); } - private setAddCursor() { + /** + * @function setAddCursor + * @description sets the mouse cursor to a special cursor + * @returns {void} + */ + private setAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { - this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = + 'url("https://i.ibb.co/GWY82b4/pin-modes.png") 0 100, pointer'; }); } - private removeAddCursor() { + /** + * @function removeAddCursor + * @description removes the special cursor + * @returns {void} + */ + private removeAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { this.divWrappers.get(id).style.cursor = 'default'; }); } + // ------- public methods ------- /** * @function setPinsVisibility * @param {boolean} isVisible - Controls the visibility of the pins. @@ -286,41 +232,38 @@ export class HTMLPin implements PinAdapter { } /** - * @function setActive - * @param {boolean} isOpen - Whether the container pin adapter is active or not. + * @function removeAnnotationPin + * @description Removes an annotation pin from the canvas. + * @param {string} uuid - The uuid of the annotation to be removed. * @returns {void} - */ - public setActive(isOpen: boolean): void { - this.isActive = isOpen; + * */ + public removeAnnotationPin(uuid: string): void { + const pinElement = this.pins.get(uuid); - if (this.isActive) { - this.addListeners(); - this.setAddCursor(); - this.getElementsReady(); - return; - } + if (!pinElement) return; - this.removeListeners(); - this.removeAddCursor(); + pinElement.remove(); + this.pins.delete(uuid); + this.annotations = this.annotations.filter((annotation) => annotation.uuid !== uuid); } /** - * @function removeAnnotationsPins - * @description clears all pins from the canvas. - * @returns {void} + * @function setCommentsMetadata + * @description stores data related to the local participant + * @param {HorizontalSide} side + * @param {string} avatar + * @param {string} name */ - private removeAnnotationsPins(): void { - this.pins.forEach((pinElement) => { - pinElement.remove(); - }); - - this.pins.clear(); - } + public setCommentsMetadata = (side: HorizontalSide, avatar: string, name: string): void => { + this.commentsSide = side; + this.localParticipant.avatar = avatar; + this.localParticipant.name = name; + }; /** * @function updateAnnotations * @description updates the annotations of the container. - * @param {Annotation[]} annotations - New annotation to be added to the container. + * @param {Annotation[]} annotations new annotation to be added to the container. * @returns {void} */ public updateAnnotations(annotations: Annotation[]): void { @@ -335,92 +278,34 @@ export class HTMLPin implements PinAdapter { } /** - * @function setMouseDownCoordinates - * @description stores the mouse down coordinates - * @param {MouseEvent} event - The mouse event object. + * @function setActive + * @param {boolean} isOpen whether the container pin adapter is active or not. * @returns {void} */ - private setMouseDownCoordinates = ({ x, y }: MouseEvent) => { - this.mouseDownCoordinates = { x, y }; - }; + public setActive(isOpen: boolean): void { + this.isActive = isOpen; + + if (this.isActive) { + this.addListeners(); + this.setAddCursor(); + this.prepareElements(); + return; + } + + this.removeListeners(); + this.removeAddCursor(); + } /** - * @function annotationSelected - * @description highlights the selected annotation and scrolls to it - * @param {CustomEvent} event - * @returns {void} + * @function renderTemporaryPin + * @description + creates a temporary pin with the id + temporary-pin to mark where the annotation is being created + * @param {string} elementId - The id of the element where the temporary pin will be rendered. */ - private annotationSelected = ({ detail: { uuid } }: CustomEvent): void => { - if (!uuid) return; - - const annotation = JSON.parse(this.selectedPin?.getAttribute('annotation') ?? '{}'); - - this.resetPins(); - - if (annotation?.uuid === uuid) return; - - document.body.dispatchEvent(new CustomEvent('close-temporary-annotation')); - - const pinElement = this.pins.get(uuid); - - if (!pinElement) return; - - pinElement.setAttribute('active', ''); - - this.selectedPin = pinElement; - // this.goToPin(uuid); - }; - - private createTemporaryPinContainer(): HTMLDivElement { - const temporaryContainer = document.createElement('div'); - temporaryContainer.style.position = 'absolute'; - temporaryContainer.style.top = '0'; - temporaryContainer.style.left = '0'; - temporaryContainer.style.width = '100%'; - temporaryContainer.style.height = '100%'; - temporaryContainer.id = 'temporary-pin-container'; - return temporaryContainer; - } - - private get temporaryPinContainer(): HTMLDivElement { - return this.divWrappers - .get(this.temporaryPinCoordinates.elementId) - .querySelector('#temporary-pin-container'); - } - - private addTemporaryPinToElement(elementId: string, pin: HTMLElement): void { - const element = this.elementsWithDataId[elementId]; - if (!element) return; - - const wrapper = this.divWrappers.get(elementId); - if (!wrapper) return; - - const temporaryContainer = this.createTemporaryPinContainer(); - temporaryContainer.appendChild(pin); - - wrapper.appendChild(temporaryContainer); - } - - private addPinToElement(elementId: string, pin: HTMLElement): void { - const element = this.elementsWithDataId[elementId]; - if (!element) return; - - const wrapper = this.divWrappers.get(elementId); - if (!wrapper) return; - - wrapper.appendChild(pin); - this.temporaryPinCoordinates.elementId = elementId; - } - - /** - * @function renderTemporaryPin - * @description - creates a temporary pin with the id - temporary-pin to mark where the annotation is being created - */ - public renderTemporaryPin(elementId?: string): void { - this.temporaryPinCoordinates.elementId ||= elementId; - let temporaryPin = this.pins.get('temporary-pin'); + public renderTemporaryPin(elementId?: string): void { + this.temporaryPinCoordinates.elementId ||= elementId; + let temporaryPin = this.pins.get('temporary-pin'); if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { this.temporaryPinContainer.remove(); @@ -454,60 +339,12 @@ export class HTMLPin implements PinAdapter { this.pins.set('temporary-pin', temporaryPin); } - private addElementListeners(id: string): void { - this.divWrappers.get(id).addEventListener('click', this.onClick, true); - this.divWrappers.get(id).addEventListener('mousedown', this.setMouseDownCoordinates); - // this.elementsWithDataId[id].addEventListener('click', this.onClick, true); - // this.elementsWithDataId[id].addEventListener('mousedown', this.setMouseDownCoordinates); - } - - private removeElementListeners(id: string): void { - this.divWrappers.get(id).removeEventListener('click', this.onClick, true); - this.divWrappers.get(id).removeEventListener('mousedown', this.setMouseDownCoordinates); - this.resizeObserver.unobserve(this.elementsWithDataId[id]); - // this.elementsWithDataId[id].removeEventListener('click', this.onClick, true); - // this.elementsWithDataId[id].removeEventListener('mousedown', this.setMouseDownCoordinates); - } - - public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { - this.commentsSide = side; - this.localParticipant.avatar = avatar; - this.localParticipant.name = name; - }; - + // ------- regular methods ------- /** - * @function animate - * @description animation frame + * @function renderAnnotationsPins + * @description appends the pins on the canvas. * @returns {void} */ - private animate = (): void => { - if (this.isActive || this.isPinsVisible) { - this.renderAnnotationsPins(); - // this.renderDivWrapper(); - } - - // if (this.temporaryPinCoordinates && this.isActive) { - // this.renderTemporaryPin(); - // } - - requestAnimationFrame(this.animate); - }; - - /** - * @function renderDivWrapper - * @description Creates a div wrapper for the pins over each valid element. - * */ - private renderDivWrapper(): Map { - const divWrappers: Map = new Map(); - - Object.entries(this.elementsWithDataId).forEach(([id, el]) => { - const divWrapper = this.setWrappers(el, id); - divWrappers.set(id, divWrapper); - }); - - return divWrappers; - } - private renderAnnotationsPins(): void { if (!this.annotations.length) { this.removeAnnotationsPins(); @@ -525,39 +362,250 @@ export class HTMLPin implements PinAdapter { const element = this.elementsWithDataId[elementId]; if (!element) return; - const wrapper = this.divWrappers.get(elementId).querySelector('[data-pins-wrapper]'); + const wrapper = this.divWrappers + .get(elementId) + .querySelector('[data-pins-wrapper]') as HTMLDivElement; if (!wrapper) return; if (this.pins.has(annotation.uuid)) { - const pin = this.pins.get(annotation.uuid); + return; + } - const isVisible = wrapper.clientWidth > x && wrapper.clientHeight > y; + const pinElement = this.createPin(annotation, x, y); + wrapper.appendChild(pinElement); + this.pins.set(annotation.uuid, pinElement); + }); + } - if (isVisible) { - pin.setAttribute('style', 'opacity: 1'); + /** + * @function clearElement + * @description clears an element that no longer has the data-superviz-id attribute + * @param {string} id the id of the element to be cleared + * @returns + */ + private clearElement(id: string): void { + const element = this.elementsWithDataId[id]; + if (!element) return; - this.pins.get(annotation.uuid).setAttribute('position', JSON.stringify({ x, y })); - return; - } + const wrapper = this.divWrappers.get(id); + const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; + const pins = pinsWrapper.children; + const { length } = pins; - pin.setAttribute('style', 'opacity: 0'); - return; - } + for (let i = 0; i < length; ++i) { + const pin = pins.item(0); + this.pins.delete(pin.id); + pin.remove(); + } - const pinElement = document.createElement('superviz-comments-annotation-pin'); - pinElement.setAttribute('type', PinMode.SHOW); - pinElement.setAttribute('annotation', JSON.stringify(annotation)); - pinElement.setAttribute('position', JSON.stringify({ x, y })); - pinElement.id = annotation.uuid; + wrapper.style.cursor = 'default'; + this.removeElementListeners(id); + delete this.elementsWithDataId[id]; + } - wrapper.appendChild(pinElement); - this.pins.set(annotation.uuid, pinElement); + /** + * @function resetSelectedPin + * @description Unselects a pin by removing its 'active' attribute + * @returns {void} + * */ + private resetSelectedPin(): void { + if (!this.selectedPin) return; + this.selectedPin.removeAttribute('active'); + this.selectedPin = null; + } + + /** + * @function removeAnnotationsPins + * @description clears all pins from the canvas. + * @returns {void} + */ + private removeAnnotationsPins(): void { + this.pins.forEach((pinElement) => { + pinElement.remove(); }); + + this.pins.clear(); + } + + /** + * @function setElementReadyToPin + * @description prepare an element with all necessary to add pins over it + * @param {HTMLElement} element + * @param {string} id + * @returns {void} + */ + private setElementReadyToPin(element: HTMLElement, id: string): void { + if (this.elementsWithDataId[id]) return; + + if (!this.divWrappers.get(id)) { + const divWrapper = this.createWrapper(element, id); + this.divWrappers.set(id, divWrapper); + } + this.elementsWithDataId[id] = element; + + if (!this.isActive || !this.isPinsVisible) return; + + this.resizeObserver.observe(element); + this.divWrappers.get(id).style.cursor = + 'url("https://i.ibb.co/GWY82b4/pin-modes.png") 0 100, pointer'; + this.addElementListeners(id); + } + + /** + * @function resetPins + * @description Unselects selected pin and removes temporary pin. + * @param {KeyboardEvent} event - The keyboard event object. + * @returns {void} + * */ + private resetPins = (event?: KeyboardEvent): void => { + if (event && event?.key !== 'Escape') return; + + this.resetSelectedPin(); + + if (!this.temporaryPinCoordinates.elementId) return; + + this.removeAnnotationPin('temporary-pin'); + this.temporaryPinContainer.remove(); + this.temporaryPinCoordinates = {}; + }; + + /** + * @function annotationSelected + * @description highlights the selected annotation and scrolls to it + * @param {CustomEvent} event + * @returns {void} + */ + private annotationSelected = ({ detail: { uuid } }: CustomEvent): void => { + if (!uuid) return; + + const annotation = this.annotations.find( + (annotation) => annotation.uuid === this.selectedPin?.id, + ); + + this.resetPins(); + + if (annotation?.uuid === uuid) return; + + document.body.dispatchEvent(new CustomEvent('close-temporary-annotation')); + + const pinElement = this.pins.get(uuid); + + if (!pinElement) return; + + pinElement.setAttribute('active', ''); + + this.selectedPin = pinElement; + }; + + /** + * @function addTemporaryPinToElement + * @param {string} elementId + * @param {HTMLElement} pin + * @returns {void} + */ + private addTemporaryPinToElement(elementId: string, pin: HTMLElement): void { + const element = this.elementsWithDataId[elementId]; + if (!element) return; + + const wrapper = this.divWrappers.get(elementId); + if (!wrapper) return; + + const temporaryContainer = this.createTemporaryPinContainer(); + temporaryContainer.appendChild(pin); + + wrapper.appendChild(temporaryContainer); + } + + // ------- helper functions ------- + /** + * @function createTemporaryPinContainer + * @description return a temporary pin container + * @returns {HTMLDivElement} + */ + private createTemporaryPinContainer(): HTMLDivElement { + const temporaryContainer = document.createElement('div'); + temporaryContainer.style.position = 'absolute'; + temporaryContainer.style.top = '0'; + temporaryContainer.style.left = '0'; + temporaryContainer.style.width = '100%'; + temporaryContainer.style.height = '100%'; + temporaryContainer.id = 'temporary-pin-container'; + return temporaryContainer; + } + + /** + * @function createPin + * @param {Annotation} annotation the annotation associated to the pin to be rendered + * @param {number} x the x coordinate of the pin + * @param {number} y the y coordinate of the pin + * @returns + */ + private createPin(annotation: Annotation, x: number, y: number) { + const pinElement = document.createElement('superviz-comments-annotation-pin'); + pinElement.setAttribute('type', PinMode.SHOW); + pinElement.setAttribute('annotation', JSON.stringify(annotation)); + pinElement.setAttribute('position', JSON.stringify({ x, y })); + pinElement.id = annotation.uuid; + + return pinElement; + } + + private createWrapper(element: HTMLElement, id: string): HTMLElement { + const container = element; + const wrapperId = `superviz-id-${id}`; + if (container.querySelector(`#${wrapperId}`)) { + return; + } + + const containerRect = container.getBoundingClientRect(); + + const containerWrapper = document.createElement('div'); + containerWrapper.setAttribute('data-wrapper-id', id); + containerWrapper.id = wrapperId; + this.container.style.position ||= 'relative'; + + containerWrapper.style.position = 'fixed'; + containerWrapper.style.top = `${containerRect.top}px`; + containerWrapper.style.left = `${containerRect.left}px`; + containerWrapper.style.width = `${containerRect.width}px`; + containerWrapper.style.height = `${containerRect.height}px`; + + const pinsWrapper = document.createElement('div'); + pinsWrapper.setAttribute('data-pins-wrapper', ''); + pinsWrapper.style.position = 'absolute'; + pinsWrapper.style.overflow = 'hidden'; + pinsWrapper.style.top = '0'; + pinsWrapper.style.left = '0'; + pinsWrapper.style.width = '100%'; + pinsWrapper.style.height = '100%'; + containerWrapper.appendChild(pinsWrapper); + + this.container.appendChild(containerWrapper); + return containerWrapper; + } + + /** + * @function temporaryPinContainer + * @description returns the temporary pin container + * @returns {HTMLDivElement} + */ + private get temporaryPinContainer(): HTMLDivElement { + return this.divWrappers + .get(this.temporaryPinCoordinates.elementId) + .querySelector('#temporary-pin-container'); } + // ------- callbacks ------- private onClick = (event: MouseEvent): void => { if (!this.isActive || event.target === this.pins.get('temporary-pin')) return; + + const target = event.target as HTMLElement; const wrapper = event.currentTarget as HTMLElement; + + if (target !== wrapper && this.pins.has(target.id)) { + return; + } + const elementId = wrapper.getAttribute('data-wrapper-id'); const rect = wrapper.getBoundingClientRect(); const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; @@ -593,18 +641,75 @@ export class HTMLPin implements PinAdapter { }; /** - * @function removeAnnotationPin - * @description Removes an annotation pin from the canvas. - * @param {string} uuid - The uuid of the annotation to be removed. + * @function onMouseDown + * @description stores the mouse down coordinates + * @param {MouseEvent} event - The mouse event object. * @returns {void} - * */ - public removeAnnotationPin(uuid: string): void { - const pinElement = this.pins.get(uuid); + */ + private onMouseDown = ({ x, y }: MouseEvent) => { + this.mouseDownCoordinates = { x, y }; + }; - if (!pinElement) return; + private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { + changes.forEach((change) => { + const element = change.target; + const elementId = element.getAttribute('data-superviz-id'); + const elementRect = element.getBoundingClientRect(); + const wrapper = this.divWrappers.get(elementId); + wrapper.style.top = `${elementRect.top}px`; + wrapper.style.left = `${elementRect.left}px`; + wrapper.style.width = `${elementRect.width}px`; + wrapper.style.height = `${elementRect.height}px`; + const subwrappers = wrapper.children; - pinElement.remove(); - this.pins.delete(uuid); - this.annotations = this.annotations.filter((annotation) => annotation.uuid !== uuid); - } + for (let i = 0; i < subwrappers.length; ++i) { + const subwrapper = subwrappers.item(i) as HTMLElement; + subwrapper.style.top = `0`; + subwrapper.style.left = `0`; + subwrapper.style.width = `${elementRect.width}px`; + subwrapper.style.height = `${elementRect.height}px`; + } + }); + }; + + private handleMutationObserverChanges = (changes: MutationRecord[]): void => { + changes.forEach((change) => { + const { target, oldValue } = change; + const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); + + if ((!dataId && !oldValue) || dataId === oldValue) return; + const attributeRemoved = !dataId && oldValue; + if (attributeRemoved) { + this.clearElement(oldValue); + return; + } + + if (oldValue && this.elementsWithDataId[oldValue]) { + this.clearElement(oldValue); + } + + this.setElementReadyToPin(target as HTMLElement, dataId); + this.renderAnnotationsPins(); + }); + }; + + /** + * @function onToggleAnnotationSidebar + * @description Removes temporary pin and unselects selected pin + * @param {CustomEvent} event + * @returns {void} + */ + private onToggleAnnotationSidebar = ({ detail }: CustomEvent): void => { + const { open } = detail; + + if (open) return; + + this.pins.forEach((pinElement) => { + pinElement.removeAttribute('active'); + }); + + if (this.pins.has('temporary-pin')) { + this.removeAnnotationPin('temporary-pin'); + } + }; } From 5d3319f031dd447deef541b1dbbb21d59ce1ca91 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Jan 2024 10:15:38 -0300 Subject: [PATCH 12/70] fix: position pin exactly at mouse pointer --- src/components/comments/html-pin-adapter/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 5bbc9f7a..c9932fc8 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -620,13 +620,13 @@ export class HTMLPin implements PinAdapter { this.onPinFixedObserver.publish({ x, - y, + y: y - 30, type: 'html', elementId, } as PinCoordinates); this.resetSelectedPin(); - this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; + this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y: y - 30 }; this.renderTemporaryPin(elementId); const temporaryPin = this.divWrappers.get(elementId).querySelector('#superviz-temporary-pin'); From d4726870c9be074ac1c0f50c3b05a7742af7d68a Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Jan 2024 10:28:45 -0300 Subject: [PATCH 13/70] fix: remove pin from element if element loses data-attribute --- src/components/comments/html-pin-adapter/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index c9932fc8..8e715bf8 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -201,8 +201,7 @@ export class HTMLPin implements PinAdapter { */ private setAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { - this.divWrappers.get(id).style.cursor = - 'url("https://i.ibb.co/GWY82b4/pin-modes.png") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; }); } @@ -233,7 +232,7 @@ export class HTMLPin implements PinAdapter { /** * @function removeAnnotationPin - * @description Removes an annotation pin from the canvas. + * @description Removes an annotation pin from the container. * @param {string} uuid - The uuid of the annotation to be removed. * @returns {void} * */ @@ -342,7 +341,7 @@ export class HTMLPin implements PinAdapter { // ------- regular methods ------- /** * @function renderAnnotationsPins - * @description appends the pins on the canvas. + * @description appends the pins on the container. * @returns {void} */ private renderAnnotationsPins(): void { @@ -416,7 +415,7 @@ export class HTMLPin implements PinAdapter { /** * @function removeAnnotationsPins - * @description clears all pins from the canvas. + * @description clears all pins from the container. * @returns {void} */ private removeAnnotationsPins(): void { @@ -446,8 +445,7 @@ export class HTMLPin implements PinAdapter { if (!this.isActive || !this.isPinsVisible) return; this.resizeObserver.observe(element); - this.divWrappers.get(id).style.cursor = - 'url("https://i.ibb.co/GWY82b4/pin-modes.png") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; this.addElementListeners(id); } @@ -680,6 +678,7 @@ export class HTMLPin implements PinAdapter { if ((!dataId && !oldValue) || dataId === oldValue) return; const attributeRemoved = !dataId && oldValue; if (attributeRemoved) { + this.removeAnnotationPin('temporary-pin'); this.clearElement(oldValue); return; } From a4c4cc9a7b82db6a8883b64644192f7ce9932f8b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Jan 2024 11:46:20 -0300 Subject: [PATCH 14/70] fix: toggle sidebar if clicking on pin when sidebar is closed --- src/components/comments/html-pin-adapter/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 8e715bf8..008ae7e7 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -63,6 +63,8 @@ export class HTMLPin implements PinAdapter { this.onPinFixedObserver = new Observer({ logger: this.logger }); this.annotations = []; this.renderAnnotationsPins(); + + document.body.addEventListener('select-annotation', this.annotationSelected); } // ------- setup ------- @@ -74,6 +76,7 @@ export class HTMLPin implements PinAdapter { public destroy(): void { this.removeListeners(); this.removeObservers(); + document.body.removeEventListener('select-annotation', this.annotationSelected); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); delete this.divWrappers; delete this.pins; @@ -634,7 +637,6 @@ export class HTMLPin implements PinAdapter { this.movedTemporaryPin = !this.movedTemporaryPin; temporaryPin.setAttribute('movedPosition', String(this.movedTemporaryPin)); - if (this.selectedPin) return; document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; From a4222c421e06cf56c804d892f73dbea93c0d9460 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Jan 2024 12:17:02 -0300 Subject: [PATCH 15/70] fix: stop observing resize only if element loses data-superviz-id attribute --- .../comments/html-pin-adapter/index.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 008ae7e7..e05587cb 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -76,13 +76,14 @@ export class HTMLPin implements PinAdapter { public destroy(): void { this.removeListeners(); this.removeObservers(); - document.body.removeEventListener('select-annotation', this.annotationSelected); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); delete this.divWrappers; delete this.pins; this.onPinFixedObserver.destroy(); this.onPinFixedObserver = null; this.annotations = []; + + document.body.removeEventListener('select-annotation', this.annotationSelected); } /** @@ -102,10 +103,13 @@ export class HTMLPin implements PinAdapter { * @param {string} id the id of the element to remove the listeners from. * @returns {void} */ - private removeElementListeners(id: string, unobserveResize?: boolean): void { + private removeElementListeners(id: string, keepObserver?: boolean): void { this.divWrappers.get(id).removeEventListener('click', this.onClick, true); this.divWrappers.get(id).removeEventListener('mousedown', this.onMouseDown); - if (unobserveResize) this.resizeObserver.unobserve(this.elementsWithDataId[id]); + + if (keepObserver) return; + + this.resizeObserver.unobserve(this.elementsWithDataId[id]); } /** @@ -126,7 +130,6 @@ export class HTMLPin implements PinAdapter { private addListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); document.body.addEventListener('keyup', this.resetPins); - document.body.addEventListener('select-annotation', this.annotationSelected); document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } @@ -138,7 +141,6 @@ export class HTMLPin implements PinAdapter { private removeListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id, true)); document.body.removeEventListener('keyup', this.resetPins); - document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } @@ -383,9 +385,10 @@ export class HTMLPin implements PinAdapter { * @function clearElement * @description clears an element that no longer has the data-superviz-id attribute * @param {string} id the id of the element to be cleared + * @param {boolean} keepObserver whether to keep he resize observer or not * @returns */ - private clearElement(id: string): void { + private clearElement(id: string, keepObserver?: boolean): void { const element = this.elementsWithDataId[id]; if (!element) return; @@ -401,7 +404,7 @@ export class HTMLPin implements PinAdapter { } wrapper.style.cursor = 'default'; - this.removeElementListeners(id); + this.removeElementListeners(id, keepObserver); delete this.elementsWithDataId[id]; } @@ -443,11 +446,12 @@ export class HTMLPin implements PinAdapter { const divWrapper = this.createWrapper(element, id); this.divWrappers.set(id, divWrapper); } + this.elementsWithDataId[id] = element; + this.resizeObserver?.observe(element); if (!this.isActive || !this.isPinsVisible) return; - this.resizeObserver.observe(element); this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; this.addElementListeners(id); } @@ -679,6 +683,7 @@ export class HTMLPin implements PinAdapter { if ((!dataId && !oldValue) || dataId === oldValue) return; const attributeRemoved = !dataId && oldValue; + if (attributeRemoved) { this.removeAnnotationPin('temporary-pin'); this.clearElement(oldValue); @@ -686,7 +691,7 @@ export class HTMLPin implements PinAdapter { } if (oldValue && this.elementsWithDataId[oldValue]) { - this.clearElement(oldValue); + this.clearElement(oldValue, true); } this.setElementReadyToPin(target as HTMLElement, dataId); From a6e11e325870641ed0aa0d83587a9d661bd64c33 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sat, 6 Jan 2024 15:17:21 -0300 Subject: [PATCH 16/70] feat: create public method to translate the pins over an element --- .../comments/html-pin-adapter/index.ts | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index e05587cb..1ff61490 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -24,6 +24,7 @@ export class HTMLPin implements PinAdapter { // Data about the current state of the application private selectedPin: HTMLElement | null = null; + private dataAttribute: string = 'data-superviz-id'; // Coordinates/Positions private mouseDownCoordinates: Simple2DPoint; @@ -33,14 +34,14 @@ export class HTMLPin implements PinAdapter { // Elements private container: HTMLElement; private elementsWithDataId: Record = {}; - private divWrappers: Map; + private divWrappers: Map = new Map(); private pins: Map; // Observers private resizeObserver: ResizeObserver; private mutationObserver: MutationObserver; - constructor(containerId: string) { + constructor(containerId: string, dataAttributeName?: string) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; @@ -50,8 +51,8 @@ export class HTMLPin implements PinAdapter { throw new Error(message); } + this.dataAttribute &&= dataAttributeName; this.isActive = false; - this.setDivWrappers(); this.prepareElements(); this.mutationObserver = new MutationObserver(this.handleMutationObserverChanges); @@ -74,13 +75,20 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ public destroy(): void { + this.logger.log('Destroying HTML Pin Adapter for Comments'); this.removeListeners(); this.removeObservers(); this.divWrappers.forEach((divWrapper) => divWrapper.remove()); + this.divWrappers.clear(); + this.pins.forEach((pin) => pin.remove()); + this.pins.clear(); delete this.divWrappers; delete this.pins; + delete this.elementsWithDataId; + delete this.logger; this.onPinFixedObserver.destroy(); - this.onPinFixedObserver = null; + delete this.onPinFixedObserver; + this.annotations = []; document.body.removeEventListener('select-annotation', this.annotationSelected); @@ -120,6 +128,8 @@ export class HTMLPin implements PinAdapter { private removeObservers(): void { this.mutationObserver.disconnect(); this.resizeObserver.disconnect(); + delete this.mutationObserver; + delete this.resizeObserver; } /** @@ -146,7 +156,7 @@ export class HTMLPin implements PinAdapter { /** * @function observeElements - * @description observes the elements with data-superviz-id attribute. + * @description observes the elements with the specified data attribute. * @returns {void} */ private observeElements(): void { @@ -157,44 +167,28 @@ export class HTMLPin implements PinAdapter { /** * @function observeContainer - * @description observes the container for changes in the data-superviz-id attribute. + * @description observes the container for changes in the specified data attribute. * @returns {void} */ private observeContainer(): void { this.mutationObserver.observe(this.container, { subtree: true, attributes: true, - attributeFilter: ['data-superviz-id'], + attributeFilter: [this.dataAttribute], attributeOldValue: true, }); } - /** - * @function setDivWrappers - * @description sets the wrapper associated to each pinnable element - * @returns {void} - * */ - private setDivWrappers(): void { - const divWrappers: Map = new Map(); - - Object.entries(this.elementsWithDataId).forEach(([id, el]) => { - const divWrapper = this.createWrapper(el, id); - divWrappers.set(id, divWrapper); - }); - - this.divWrappers = divWrappers; - } - /** * @function prepareElements - * @description set elements with data-superviz-id attribute as pinnable + * @description set elements with the specified data attribute as pinnable * @returns {void} */ private prepareElements(): void { - const elementsWithDataId = this.container.querySelectorAll('[data-superviz-id]'); + const elementsWithDataId = this.container.querySelectorAll(`[${this.dataAttribute}]`); elementsWithDataId.forEach((el: HTMLElement) => { - const id = el.getAttribute('data-superviz-id'); + const id = el.getAttribute(this.dataAttribute); this.setElementReadyToPin(el, id); }); } @@ -233,8 +227,25 @@ export class HTMLPin implements PinAdapter { if (this.isPinsVisible) { this.renderAnnotationsPins(); } + + this.removeAnnotationsPins(); } + /** + * @function translatePins + * @description translates the wrapper containing all the pins of an element + * @param {string} elementId + * @param {number} x + * @param {number} y + * @returns {void} + */ + public translatePins = (elementId: string, x: number, y: number): void => { + const wrapper = this.divWrappers.get(elementId); + if (!wrapper) return; + + wrapper.style.transform = `translate(${x}px, ${y}px)`; + }; + /** * @function removeAnnotationPin * @description Removes an annotation pin from the container. @@ -383,7 +394,7 @@ export class HTMLPin implements PinAdapter { /** * @function clearElement - * @description clears an element that no longer has the data-superviz-id attribute + * @description clears an element that no longer has the specified data attribute * @param {string} id the id of the element to be cleared * @param {boolean} keepObserver whether to keep he resize observer or not * @returns @@ -574,6 +585,7 @@ export class HTMLPin implements PinAdapter { containerWrapper.style.left = `${containerRect.left}px`; containerWrapper.style.width = `${containerRect.width}px`; containerWrapper.style.height = `${containerRect.height}px`; + containerWrapper.style.pointerEvents = 'none'; const pinsWrapper = document.createElement('div'); pinsWrapper.setAttribute('data-pins-wrapper', ''); @@ -657,7 +669,7 @@ export class HTMLPin implements PinAdapter { private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { changes.forEach((change) => { const element = change.target; - const elementId = element.getAttribute('data-superviz-id'); + const elementId = element.getAttribute(this.dataAttribute); const elementRect = element.getBoundingClientRect(); const wrapper = this.divWrappers.get(elementId); wrapper.style.top = `${elementRect.top}px`; @@ -679,7 +691,7 @@ export class HTMLPin implements PinAdapter { private handleMutationObserverChanges = (changes: MutationRecord[]): void => { changes.forEach((change) => { const { target, oldValue } = change; - const dataId = (target as HTMLElement).getAttribute('data-superviz-id'); + const dataId = (target as HTMLElement).getAttribute(this.dataAttribute); if ((!dataId && !oldValue) || dataId === oldValue) return; const attributeRemoved = !dataId && oldValue; From 8d06252f54ea466e8631d855a7146053523f399d Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sat, 6 Jan 2024 15:50:01 -0300 Subject: [PATCH 17/70] feat: append all wrappers to a single div inside the specified container --- .../comments/html-pin-adapter/index.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 1ff61490..ce43cfc8 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -33,6 +33,7 @@ export class HTMLPin implements PinAdapter { // Elements private container: HTMLElement; + private pinsContainer: HTMLDivElement; private elementsWithDataId: Record = {}; private divWrappers: Map = new Map(); private pins: Map; @@ -51,6 +52,7 @@ export class HTMLPin implements PinAdapter { throw new Error(message); } + this.createPinsContainer(); this.dataAttribute &&= dataAttributeName; this.isActive = false; this.prepareElements(); @@ -88,6 +90,9 @@ export class HTMLPin implements PinAdapter { delete this.logger; this.onPinFixedObserver.destroy(); delete this.onPinFixedObserver; + this.pinsContainer.remove(); + delete this.pinsContainer; + delete this.container; this.annotations = []; @@ -201,6 +206,7 @@ export class HTMLPin implements PinAdapter { private setAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.pointerEvents = 'auto'; }); } @@ -212,6 +218,7 @@ export class HTMLPin implements PinAdapter { private removeAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { this.divWrappers.get(id).style.cursor = 'default'; + this.divWrappers.get(id).style.pointerEvents = 'none'; }); } @@ -313,9 +320,7 @@ export class HTMLPin implements PinAdapter { /** * @function renderTemporaryPin - * @description - creates a temporary pin with the id - temporary-pin to mark where the annotation is being created + * @description creates a temporary pin with the id temporary-pin to mark where the annotation is being created * @param {string} elementId - The id of the element where the temporary pin will be rendered. */ public renderTemporaryPin(elementId?: string): void { @@ -549,6 +554,22 @@ export class HTMLPin implements PinAdapter { return temporaryContainer; } + /** + * @function createPinsContainer + * @description creates the container where pins will be appended and appends it to the DOM (either the parent of the element with the specified id or the body) + * @returns {void} + */ + private createPinsContainer(): void { + const pinsContainer = document.createElement('div'); + pinsContainer.style.position = 'absolute'; + pinsContainer.style.top = '0'; + pinsContainer.style.left = '0'; + pinsContainer.style.width = '100%'; + pinsContainer.style.height = '100%'; + this.pinsContainer = pinsContainer; + this.container.appendChild(pinsContainer); + } + /** * @function createPin * @param {Annotation} annotation the annotation associated to the pin to be rendered @@ -578,7 +599,6 @@ export class HTMLPin implements PinAdapter { const containerWrapper = document.createElement('div'); containerWrapper.setAttribute('data-wrapper-id', id); containerWrapper.id = wrapperId; - this.container.style.position ||= 'relative'; containerWrapper.style.position = 'fixed'; containerWrapper.style.top = `${containerRect.top}px`; @@ -597,7 +617,7 @@ export class HTMLPin implements PinAdapter { pinsWrapper.style.height = '100%'; containerWrapper.appendChild(pinsWrapper); - this.container.appendChild(containerWrapper); + this.pinsContainer.appendChild(containerWrapper); return containerWrapper; } From 29aa1c6cc8ad72141d04597f74d01a0ef66f7293 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 08:22:31 -0300 Subject: [PATCH 18/70] feat: create tests for html-pin-adapter --- .../comments/html-pin-adapter/index.test.ts | 1157 +++++++++++++++++ 1 file changed, 1157 insertions(+) create mode 100644 src/components/comments/html-pin-adapter/index.test.ts diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts new file mode 100644 index 00000000..4cfd37e7 --- /dev/null +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -0,0 +1,1157 @@ +import { MOCK_ANNOTATION } from '../../../../__mocks__/comments.mock'; + +import { HTMLPin } from '.'; + +window.ResizeObserver = jest.fn().mockReturnValue({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +}); + +const MOCK_ANNOTATION_HTML = { + ...MOCK_ANNOTATION, + position: JSON.stringify({ + x: 100, + y: 100, + z: null, + type: 'html', + elementId: '1', + }), +}; + +describe('HTMLPinAdapter', () => { + let instance: HTMLPin; + let target: HTMLElement; + let currentTarget: HTMLElement; + + beforeEach(() => { + document.body.innerHTML = ` +
+
+
+
+
+ `; + + instance = new HTMLPin('container'); + instance.setActive(true); + instance['mouseDownCoordinates'] = { x: 100, y: 100 }; + target = instance['divWrappers'].get('1') as HTMLElement; + currentTarget = target; + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + describe('constructor', () => { + test('should create a new instance of HTMLPinAdapter', () => { + const canvasPinAdapter = new HTMLPin('container'); + canvasPinAdapter.setActive(true); + expect(canvasPinAdapter).toBeInstanceOf(HTMLPin); + }); + + test('should throw an error if no html element is found', () => { + expect(() => new HTMLPin('not-found-html')).toThrowError( + 'Element with id not-found-html not found', + ); + }); + }); + + describe('destroy', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should destroy the HTML pin adapter', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + const removeListenersSpy = jest.spyOn(instance as any, 'removeListeners'); + const removeObserversSpy = jest.spyOn(instance as any, 'removeObservers'); + const onPinFixedObserverSpy = jest.spyOn(instance['onPinFixedObserver'], 'destroy'); + const removeElementListenersSpy = jest.spyOn(document.body as any, 'removeEventListener'); + const removeSpy = jest.fn(); + const removeEventListenerSpy = jest.fn(); + const disconnectSpy = jest.spyOn(instance['resizeObserver'], 'disconnect'); + + const wrappers = [...instance['divWrappers']].map(([entry, value]) => { + return [ + entry, + { ...value, remove: removeSpy, removeEventListener: removeEventListenerSpy }, + ]; + }); + instance['divWrappers'] = new Map(wrappers as [key: any, value: any][]); + + instance.destroy(); + + expect(disconnectSpy).toHaveBeenCalled(); + expect(removeListenersSpy).toHaveBeenCalled(); + expect(removeObserversSpy).toHaveBeenCalled(); + expect(onPinFixedObserverSpy).toHaveBeenCalled(); + expect(removeElementListenersSpy).toHaveBeenCalled(); + + expect(removeSpy).toHaveBeenCalledTimes(3); + + expect(instance['annotations']).toEqual([]); + expect(instance['elementsWithDataId']).toEqual(undefined); + expect(instance['divWrappers']).toEqual(undefined); + expect(instance['pins']).toEqual(undefined); + expect(instance['onPinFixedObserver']).toEqual(undefined); + expect(instance['divWrappers']).toEqual(undefined); + expect(instance['resizeObserver']).toEqual(undefined); + expect(instance['mutationObserver']).toEqual(undefined); + }); + }); + + describe('listeners', () => { + afterEach(() => { + jest.restoreAllMocks(); + instance['divWrappers'].clear(); + instance['prepareElements'](); + }); + + test('should add event listeners to the HTML container', () => { + const bodyAddEventListenerSpy = jest.spyOn(document.body, 'addEventListener'); + const wrapperAddEventListenerSpy = jest.fn(); + + const wrappers = [...instance['divWrappers']].map(([entry, value]) => { + return [entry, { ...value, addEventListener: wrapperAddEventListenerSpy }]; + }); + + instance['divWrappers'] = new Map(wrappers as [key: any, value: any][]); + + instance['addListeners'](); + + expect(bodyAddEventListenerSpy).toHaveBeenCalledTimes(2); + expect(wrapperAddEventListenerSpy).toHaveBeenCalledTimes(6); + }); + + test('should remove event listeners from the HTML container', () => { + const bodyRemoveEventListenerSpy = jest.spyOn(document.body, 'removeEventListener'); + const wrapperRemoveEventListenerSpy = jest.fn(); + + const wrappers = [...instance['divWrappers']].map(([entry, value]) => { + return [entry, { ...value, removeEventListener: wrapperRemoveEventListenerSpy }]; + }); + + instance['divWrappers'] = new Map(wrappers as [key: any, value: any][]); + + instance['removeListeners'](); + + expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(2); + expect(wrapperRemoveEventListenerSpy).toHaveBeenCalledTimes(6); + }); + + test('should not call resizeObserver.unobserve if flag is true', () => { + instance['removeElementListeners']('1', true); + expect(instance['resizeObserver'].unobserve).not.toHaveBeenCalled(); + }); + + test('should call resizeObserver.unobserve if flag is false', () => { + instance['removeElementListeners']('1', false); + expect(instance['resizeObserver'].unobserve).toHaveBeenCalled(); + }); + }); + + describe('annotationSelected', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('should select annotation pin', async () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['selectedPin']).toBeNull(); + + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: MOCK_ANNOTATION_HTML.uuid, + }, + }), + ); + + expect([...instance['pins'].values()].some((pin) => pin.hasAttribute('active'))).toBeTruthy(); + }); + + test('should not select annotation pin if uuid is not defined', async () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['selectedPin']).toBeNull(); + + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: undefined, + }, + }), + ); + + expect([...instance['pins'].values()].some((pin) => pin.hasAttribute('active'))).toBeFalsy(); + }); + }); + + describe('renderAnnotationsPins', () => { + afterAll(() => { + jest.restoreAllMocks(); + instance['pins'].clear(); + }); + + test('should not render anything if annotations list is empty', () => { + instance['annotations'] = []; + instance['pins'].clear(); + const spy = jest.spyOn(instance as any, 'removeAnnotationsPins'); + + instance['renderAnnotationsPins'](); + + expect(spy).toHaveBeenCalled(); + expect(instance['pins'].size).toEqual(0); + }); + + test('should render annotations pins', () => { + instance['annotations'] = [MOCK_ANNOTATION_HTML]; + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(1); + }); + + test('should not render annotation pin if annotation is resolved', () => { + instance['annotations'] = [ + { + ...MOCK_ANNOTATION_HTML, + resolved: true, + }, + ]; + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not render annotation pin if pin was not set using html adapter', () => { + instance['annotations'] = [MOCK_ANNOTATION]; + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not render annotation pin if element with the elementId of the annotation is not found', () => { + instance['annotations'] = [ + { + ...MOCK_ANNOTATION_HTML, + position: JSON.stringify({ + x: 100, + y: 100, + z: null, + type: 'html', + elementId: 'not-found', + }), + }, + ]; + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not render annotation pin if wrapper associated with the elementId of the annotation is not found', () => { + instance['annotations'] = [ + { + ...MOCK_ANNOTATION_HTML, + position: JSON.stringify({ + x: 100, + y: 100, + z: null, + type: 'html', + elementId: '1', + }), + }, + ]; + + instance['divWrappers'] + .get('1') + ?.querySelector('[data-pins-wrapper]')! + .removeAttribute('data-pins-wrapper'); + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + // // delete this avoids an error being throw when the pin is destroyed + // delete instance['elementsWithDataId']['1']; + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not render new annotation pin if it already exists', () => { + instance['annotations'] = [MOCK_ANNOTATION_HTML]; + instance['pins'].clear(); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(1); + + instance['renderAnnotationsPins'](); + + expect(instance['pins'].size).toEqual(1); + }); + + test('should not create pin if annotation element id is not found', () => { + document.body.innerHTML = ``; + }); + }); + + describe('onClick', () => { + let element: HTMLElement; + + beforeEach(() => { + element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + instance['annotations'] = [MOCK_ANNOTATION_HTML]; + instance['pins'].clear(); + instance['setElementReadyToPin'](element, '1'); + instance['renderAnnotationsPins'](); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should create temporary pin when mouse clicks canvas', () => { + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + }); + + test('should not create a temporary pin if the adapter is not active', () => { + instance.setActive(false); + + instance['onClick']({ + x: 100, + y: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + instance.setActive(true); + expect(instance['pins'].has('temporary-pin')).toBeFalsy(); + }); + + test('should remove temporary pin when selecting another pin', () => { + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: MOCK_ANNOTATION_HTML.uuid, + }, + }), + ); + + expect(instance['pins'].has('temporary-pin')).toBeFalsy(); + }); + + test('should not create a temporary pin if clicking over another pin', () => { + const pin = instance['pins'].get(MOCK_ANNOTATION_HTML.uuid); + instance['onClick']({ + clientX: 100, + clientY: 100, + target: pin, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeFalsy(); + }); + + test('should not create a temporary pin if distance between mouse down and mouse up is more than 10px', () => { + instance['onMouseDown']({ x: 100, y: 100 } as unknown as MouseEvent); + + instance['onClick']({ + clientX: 100, + clientY: 111, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeFalsy(); + }); + }); + + describe('clearElement', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should remove pins and listeners and set cursor to default on element being cleared', () => { + instance['prepareElements'](); + instance['annotations'] = [MOCK_ANNOTATION_HTML]; + instance['renderAnnotationsPins'](); + + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + + expect(wrapper.style.cursor).not.toEqual('default'); + expect(instance['pins'].size).toEqual(1); + expect(Object.keys(instance['elementsWithDataId']).length).toEqual(3); + + const spy = jest.spyOn(instance as any, 'removeElementListeners'); + instance['clearElement']('1'); + + expect(spy).toHaveBeenCalled(); + expect(instance['pins'].size).toEqual(0); + expect(Object.keys(instance['elementsWithDataId']).length).toEqual(2); + expect(wrapper.style.cursor).toEqual('default'); + }); + + test('should not clear element if it is not stored in elementsWithDataId', () => { + instance['prepareElements'](); + instance['annotations'] = [MOCK_ANNOTATION_HTML]; + instance['renderAnnotationsPins'](); + + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + + expect(wrapper.style.cursor).not.toEqual('default'); + expect(instance['pins'].size).toEqual(1); + expect(Object.keys(instance['elementsWithDataId']).length).toEqual(3); + + const spy = jest.spyOn(instance as any, 'removeElementListeners'); + instance['clearElement']('not-found'); + + expect(spy).not.toHaveBeenCalled(); + expect(instance['pins'].size).toEqual(1); + expect(Object.keys(instance['elementsWithDataId']).length).toEqual(3); + expect(wrapper.style.cursor).not.toEqual('default'); + }); + }); + + describe('setCommentsMetadata', () => { + test('should store updated data about comments and local participant', () => { + instance.setCommentsMetadata('right', 'user-avatar', 'user name'); + + expect(instance['commentsSide']).toEqual('right'); + expect(instance['localParticipant']).toEqual({ + avatar: 'user-avatar', + name: 'user name', + }); + }); + }); + + describe('resetPins', () => { + test('should remove active on Escape key', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + const detail = { + uuid: MOCK_ANNOTATION_HTML.uuid, + }; + + instance['annotationSelected']({ detail } as unknown as CustomEvent); + + expect(instance['selectedPin']).not.toBeNull(); + + instance['resetPins']({ key: 'Escape' } as unknown as KeyboardEvent); + + expect(instance['selectedPin']).toBeNull(); + }); + + test('should reset on KeyBoardEvent if the key is Escape', () => { + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + + instance['resetPins']({ key: 'Escape' } as unknown as KeyboardEvent); + + expect(instance['pins'].has('temporary-pin')).toBeFalsy(); + }); + + test('should not reset on KeyboardEvent if the key is not Escape', () => { + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + + instance['resetPins']({ key: 'Enter' } as unknown as KeyboardEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + }); + }); + + describe('annotationSelected', () => { + test('should toggle active attribute when click same annotation twice', () => { + const detail = { + uuid: MOCK_ANNOTATION_HTML.uuid, + }; + + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + instance['annotationSelected']({ detail } as unknown as CustomEvent); + + expect(instance['selectedPin']).not.toBeNull(); + expect(instance['selectedPin']?.hasAttribute('active')).toBeTruthy(); + + instance['annotationSelected']({ detail } as unknown as CustomEvent); + + expect(instance['selectedPin']).toBeNull(); + }); + + test('should not select annotation pin if it does not exist', async () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['selectedPin']).toBeNull(); + + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: 'not-found', + }, + }), + ); + + expect([...instance['pins'].values()].some((pin) => pin.hasAttribute('active'))).toBeFalsy(); + }); + + test('should remove highlight from annotation pin when sidebar is closed', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: MOCK_ANNOTATION_HTML.uuid, + }, + }), + ); + + let pin = instance['pins'].get(MOCK_ANNOTATION_HTML.uuid); + + expect(pin?.hasAttribute('active')).toBeTruthy(); + + instance['onToggleAnnotationSidebar']( + new CustomEvent('toggle-annotation-sidebar', { + detail: { + open: false, + }, + }), + ); + + pin = instance['pins'].get(MOCK_ANNOTATION_HTML.uuid); + + expect(pin?.hasAttribute('active')).toBeFalsy(); + }); + + test('should not remove highlight from annotation pin when sibar is opened', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + instance['annotationSelected']( + new CustomEvent('select-annotation', { + detail: { + uuid: MOCK_ANNOTATION_HTML.uuid, + }, + }), + ); + + let pin = instance['pins'].get(MOCK_ANNOTATION_HTML.uuid); + + expect(pin?.hasAttribute('active')).toBeTruthy(); + + instance['onToggleAnnotationSidebar']( + new CustomEvent('toggle-annotation-sidebar', { + detail: { + open: true, + }, + }), + ); + + pin = instance['pins'].get(MOCK_ANNOTATION_HTML.uuid); + + expect(pin?.hasAttribute('active')).toBeTruthy(); + }); + }); + + describe('removeAnnotationPin', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should remove annotation pin', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['pins'].size).toEqual(1); + + instance.removeAnnotationPin(MOCK_ANNOTATION_HTML.uuid); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not remove annotation pin if it does not exist', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['pins'].size).toEqual(1); + + instance.removeAnnotationPin('not_found_uuid'); + + expect(instance['pins'].size).toEqual(1); + }); + }); + + describe('updateAnnotations', () => { + test('should not render annotations if the adapter is not active and visibility is false', async () => { + instance.setActive(false); + instance.setPinsVisibility(false); + + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should remove pins when visibility is false', () => { + instance.setPinsVisibility(true); + + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['pins'].size).toEqual(1); + + instance.setPinsVisibility(false); + + expect(instance['pins'].size).toEqual(0); + }); + + test('should not render annotation if the coordinate type is not canvas', () => { + instance.updateAnnotations([ + { + ...MOCK_ANNOTATION_HTML, + uuid: 'not-canvas', + position: JSON.stringify({ + x: 100, + y: 100, + type: 'not-canvas', + }), + }, + ]); + + expect(instance['pins'].has('not-canvas')).toBeFalsy(); + }); + + test('should remove annotation pin when it is resolved', () => { + const annotation = { + ...MOCK_ANNOTATION_HTML, + resolved: false, + }; + + instance.updateAnnotations([ + { ...annotation, uuid: '000 ' }, + { ...annotation, uuid: '123' }, + { ...annotation, uuid: '321' }, + ]); + + expect(instance['pins'].size).toEqual(3); + + instance.updateAnnotations([ + { ...annotation, uuid: '000 ' }, + { ...annotation, uuid: '123', resolved: true }, + { ...annotation, uuid: '321', resolved: true }, + ]); + + expect(instance['pins'].size).toEqual(1); + }); + + test('should not render annotations if the canvas is hidden', () => { + instance.updateAnnotations([MOCK_ANNOTATION_HTML]); + + expect(instance['pins'].size).toEqual(1); + + instance['container'].style.display = 'none'; + + instance.updateAnnotations([]); + + expect(instance['pins'].size).toEqual(0); + }); + }); + + describe('onMouseDown', () => { + test('should update mouse coordinates on mousedown event', () => { + instance['onMouseDown']({ x: 351, y: 153 } as unknown as MouseEvent); + expect(instance['mouseDownCoordinates']).toEqual({ x: 351, y: 153 }); + }); + }); + + describe('renderTemporaryPin', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('should remove previous temporary pin container when rendering temporary pin over another element', () => { + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + expect(instance['temporaryPinCoordinates'].elementId).toBe( + currentTarget.getAttribute('data-wrapper-id'), + ); + + const removeSpy = jest.spyOn(instance['temporaryPinContainer'] as any, 'remove'); + const deleteSpy = jest.spyOn(instance['pins'], 'delete'); + + instance['onClick']({ + clientX: 100, + clientY: 100, + target: document.body.querySelector('[data-wrapper-id="2"]') as HTMLElement, + currentTarget: document.body.querySelector('[data-wrapper-id="2"]') as HTMLElement, + } as unknown as MouseEvent); + + expect(removeSpy).toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalled(); + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + expect(instance['temporaryPinCoordinates'].elementId).toBe('2'); + }); + }); + + describe('translateAndScalePins', () => { + afterEach(() => { + instance['divWrappers'].clear(); + }); + + test('should apply the correct translate and scale to the specified pins wrapper', () => { + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const transformObject = { + scale: Math.random() * 10, + translateX: Math.random() * 1000, + translateY: Math.random() * 1000, + }; + + const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; + + expect(wrapper.style.transform).not.toEqual(transformString); + + instance['translateAndScalePins']('1', transformObject); + + expect(wrapper.style.transform).toEqual(transformString); + }); + + test('should not apply the transform if the wrapper does not exist', () => { + const transformObject = { + scale: Math.random() * 10, + translateX: Math.random() * 1000, + translateY: Math.random() * 1000, + }; + + const spyParse = jest.spyOn(instance as any, 'parseTransform'); + + instance['translateAndScalePins']('not-found', transformObject); + + expect(instance['divWrappers'].has('not-found')).toBeFalsy(); + expect(spyParse).not.toHaveBeenCalled(); + }); + + test('should keep previous transform if the new transform is not defined', () => { + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const transformObject = { + scale: Math.random() * 10, + translateX: Math.random() * 1000, + translateY: Math.random() * 1000, + }; + + const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; + + expect(wrapper.style.transform).not.toEqual(transformString); + + instance['translateAndScalePins']('1', transformObject); + + expect(wrapper.style.transform).toEqual(transformString); + + instance['translateAndScalePins']('1', {}); + + expect(wrapper.style.transform).toEqual(transformString); + }); + }); + + describe('translateAndScaleContainer', () => { + beforeEach(() => { + instance['pinsContainer'].style.transform = 'scale(1) translateX(0px) translateY(0px)'; + }); + + test('should apply the correct translate and scale to the pins container', () => { + const transformObject = { + scale: Math.random() * 10, + translateX: Math.random() * 1000, + translateY: Math.random() * 1000, + }; + + const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; + + expect(instance['pinsContainer'].style.transform).not.toEqual(transformString); + + instance['translateAndScaleContainer'](transformObject); + + expect(instance['pinsContainer'].style.transform).toEqual(transformString); + }); + + test('should keep previous transform if the new transform is not defined', () => { + const transformObject = { + scale: Math.random() * 10, + translateX: Math.random() * 1000, + translateY: Math.random() * 1000, + }; + + const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; + + expect(instance['pinsContainer'].style.transform).not.toEqual(transformString); + + instance['translateAndScaleContainer'](transformObject); + + expect(instance['pinsContainer'].style.transform).toEqual(transformString); + + instance['translateAndScaleContainer']({}); + + expect(instance['pinsContainer'].style.transform).toEqual(transformString); + }); + }); + + describe('setElementReadyToPin', () => { + beforeEach(() => { + instance['divWrappers'].get('1')!.style.cursor = 'default'; + }); + + afterEach(() => { + jest.restoreAllMocks(); + instance['divWrappers'].clear(); + }); + + test('should change cursor and add event listeners', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const spy = jest.spyOn(instance as any, 'addElementListeners'); + delete instance['elementsWithDataId']['1']; + instance['setElementReadyToPin'](element, '1'); + + expect(wrapper.style.cursor).not.toEqual('default'); + expect(spy).toHaveBeenCalled(); + }); + + test('should not change cursor and add event listeners if comments are not active', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const spy = jest.spyOn(instance as any, 'addElementListeners'); + instance.setActive(false); + delete instance['elementsWithDataId']['1']; + instance['setElementReadyToPin'](element, '1'); + + expect(wrapper.style.cursor).toEqual('default'); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should not change cursor and add event listeners if pins are not visible', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const spy = jest.spyOn(instance as any, 'addElementListeners'); + + instance.setPinsVisibility(false); + delete instance['elementsWithDataId']['1']; + instance['setElementReadyToPin'](element, '1'); + + expect(wrapper.style.cursor).toEqual('default'); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should create new divWrapper if divWrapper not found', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const spySet = jest.spyOn(instance['divWrappers'], 'set'); + const spyCreate = jest.spyOn(instance as any, 'createWrapper'); + + instance['pinsContainer'].innerHTML = ''; + instance['divWrappers'].clear(); + + delete instance['elementsWithDataId']['1']; + + instance['setElementReadyToPin'](element, '1'); + + expect(spyCreate).toHaveBeenCalled(); + expect(spySet).toHaveBeenCalled(); + }); + }); + + describe('addTemporaryPinToElement', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should add temporary pin to element', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const temporaryPinContainer = document.createElement('div'); + temporaryPinContainer.id = 'temp-container'; + const pin = document.createElement('div'); + pin.id = 'temp-pin'; + const spy = jest + .spyOn(instance as any, 'createTemporaryPinContainer') + .mockReturnValue(temporaryPinContainer); + + instance['addTemporaryPinToElement']('1', pin); + + expect(spy).toHaveBeenCalled(); + expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(pin); + expect(instance['divWrappers'].get('1')?.querySelector('#temp-container')).toBe( + temporaryPinContainer, + ); + }); + + test('should not add temporary pin to element if element is not found', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const temporaryPinContainer = document.createElement('div'); + temporaryPinContainer.id = 'temp-container'; + const pin = document.createElement('div'); + pin.id = 'temp-pin'; + const spy = jest + .spyOn(instance as any, 'createTemporaryPinContainer') + .mockReturnValue(temporaryPinContainer); + + instance['addTemporaryPinToElement']('not-found', pin); + + expect(spy).not.toHaveBeenCalled(); + expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(null); + expect(instance['divWrappers'].get('1')?.querySelector('#temp-container')).toBe(null); + }); + + test('should not add temporary pin to element if wrapper is not found', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const temporaryPinContainer = document.createElement('div'); + temporaryPinContainer.id = 'temp-container'; + const pin = document.createElement('div'); + pin.id = 'temp-pin'; + const spy = jest + .spyOn(instance as any, 'createTemporaryPinContainer') + .mockReturnValue(temporaryPinContainer); + instance['divWrappers'].delete('1'); + instance['addTemporaryPinToElement']('1', pin); + + expect(spy).not.toHaveBeenCalled(); + expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(null); + }); + }); + + describe('createWrapper', () => { + test('should create a new wrapper', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + instance['pinsContainer'].innerHTML = ''; + + const wrapper = instance['createWrapper'](element, '1'); + + const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLElement; + const containerRect = element.getBoundingClientRect(); + + expect(wrapper).toBeInstanceOf(HTMLDivElement); + expect(wrapper.style.position).toEqual('fixed'); + expect(wrapper.style.top).toEqual(`${containerRect.top}px`); + expect(wrapper.style.left).toEqual(`${containerRect.left}px`); + expect(wrapper.style.width).toEqual(`${containerRect.width}px`); + expect(wrapper.style.height).toEqual(`${containerRect.height}px`); + expect(wrapper.style.pointerEvents).toEqual('none'); + expect(wrapper.style.transform).toEqual('translateX(0) translateY(0) scale(1)'); + expect(wrapper.style.cursor).toEqual('default'); + expect(wrapper.getAttribute('data-wrapper-id')).toEqual('1'); + expect(wrapper.id).toEqual('superviz-id-1'); + + expect(pinsWrapper).toBeInstanceOf(HTMLDivElement); + expect(pinsWrapper.style.position).toEqual('absolute'); + expect(pinsWrapper.style.overflow).toEqual('hidden'); + expect(pinsWrapper.style.top).toEqual('0px'); + expect(pinsWrapper.style.left).toEqual('0px'); + expect(pinsWrapper.style.width).toEqual('100%'); + expect(pinsWrapper.style.height).toEqual('100%'); + }); + + test('should not create a new wrapper if wrapper already exists', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + instance['pinsContainer'].innerHTML = ''; + + const wrapper1 = instance['createWrapper'](element, '1'); + const wrapper2 = instance['createWrapper'](element, '1'); + + expect(wrapper1).toBeInstanceOf(HTMLDivElement); + expect(wrapper2).toBe(undefined); + }); + }); + + describe('handleResizeObserverChanges', () => { + test('should update the wrapper position when the element is resized', () => { + const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLElement; + + const width = Math.random() * 1000; + const height = Math.random() * 1000; + const top = Math.random() * 1000; + const left = Math.random() * 1000; + + element.style.width = `${width}px`; + element.style.height = `${height}px`; + element.style.top = `${top}px`; + element.style.left = `${left}px`; + + const changes = { + target: element, + } as unknown as ResizeObserverEntry; + + element.getBoundingClientRect = jest.fn().mockReturnValue({ + width, + height, + top, + left, + }); + + instance['handleResizeObserverChanges']([changes]); + + expect(wrapper.style.width).toEqual(`${width}px`); + expect(wrapper.style.height).toEqual(`${height}px`); + expect(wrapper.style.top).toEqual(`${top}px`); + expect(wrapper.style.left).toEqual(`${left}px`); + expect(pinsWrapper.style.width).toEqual(`${width}px`); + expect(pinsWrapper.style.height).toEqual(`${height}px`); + expect(pinsWrapper.style.top).toEqual('0px'); + expect(pinsWrapper.style.left).toEqual('0px'); + }); + }); + + describe('handleMutationObserverChanges', () => { + let setElementsSpy: jest.SpyInstance; + let renderAnnotationsSpy: jest.SpyInstance; + let clearElementSpy: jest.SpyInstance; + let removeAnnotationSpy: jest.SpyInstance; + + beforeEach(() => { + setElementsSpy = jest.spyOn(instance as any, 'setElementReadyToPin'); + renderAnnotationsSpy = jest.spyOn(instance as any, 'renderAnnotationsPins'); + clearElementSpy = jest.spyOn(instance as any, 'clearElement'); + removeAnnotationSpy = jest.spyOn(instance as any, 'removeAnnotationPin'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should set elements and update pins when a new element with the specified attribute appears', () => { + const change = { + target: document.body.querySelector('[data-superviz-id="1"]') as HTMLElement, + oldValue: null, + } as unknown as MutationRecord; + + instance['handleMutationObserverChanges']([change]); + + expect(setElementsSpy).toHaveBeenCalled(); + expect(renderAnnotationsSpy).toHaveBeenCalled(); + expect(clearElementSpy).not.toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + }); + + test('should clear elements and remove pins if the attribute is removed from the element', () => { + const change = { + target: document.createElement('div') as HTMLElement, + oldValue: '1', + } as unknown as MutationRecord; + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).toHaveBeenCalled(); + expect(removeAnnotationSpy).toHaveBeenCalled(); + expect(renderAnnotationsSpy).not.toHaveBeenCalled(); + expect(setElementsSpy).not.toHaveBeenCalled(); + }); + + test('should clear element if the attribute changes, but still exists', () => { + const change = { + target: document.body.querySelector('[data-superviz-id="1"]') as HTMLElement, + oldValue: '2', + } as unknown as MutationRecord; + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).toHaveBeenCalled(); + expect(renderAnnotationsSpy).toHaveBeenCalled(); + expect(setElementsSpy).toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + }); + + test('should do nothing if there is not new nor old value to the attribute', () => { + const target = document.createElement('div') as HTMLElement; + target.setAttribute('data-superviz-id', ''); + const change = { + target, + oldValue: null, + } as unknown as MutationRecord; + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).not.toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + expect(renderAnnotationsSpy).not.toHaveBeenCalled(); + expect(setElementsSpy).not.toHaveBeenCalled(); + }); + + test('should do nothing if the new value is the same as the old attribute value', () => { + const change = { + target: document.body.querySelector('[data-superviz-id="1"]') as HTMLElement, + oldValue: '1', + } as unknown as MutationRecord; + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).not.toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + expect(renderAnnotationsSpy).not.toHaveBeenCalled(); + expect(setElementsSpy).not.toHaveBeenCalled(); + }); + }); + + describe('onToggleAnnotationSidebar', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should remove active attribute from selected pin if sidebar is closed', () => { + const spy = jest.spyOn(instance as any, 'resetSelectedPin'); + instance['onToggleAnnotationSidebar']({ detail: { open: false } } as unknown as CustomEvent); + expect(spy).toHaveBeenCalled(); + }); + + test('should not remove active attribute from selected pin if sidebar is opened', () => { + const spy = jest.spyOn(instance as any, 'resetSelectedPin'); + instance['onToggleAnnotationSidebar']({ detail: { open: true } } as unknown as CustomEvent); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should remove temporary pin if sidebar is closed', () => { + const spy = jest.spyOn(instance as any, 'removeAnnotationPin'); + instance['onClick']({ + clientX: 100, + clientY: 100, + target, + currentTarget, + } as unknown as MouseEvent); + + expect(instance['pins'].has('temporary-pin')).toBeTruthy(); + + instance['onToggleAnnotationSidebar']({ detail: { open: false } } as unknown as CustomEvent); + + expect(spy).toHaveBeenCalledWith('temporary-pin'); + }); + }); +}); From 8ab06ef643c4e7454b73a75d0ce69cb3d9f02073 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 08:23:13 -0300 Subject: [PATCH 19/70] feat: add new type to html pin adapter --- src/components/comments/html-pin-adapter/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index d0c3f9a1..b197a781 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -13,3 +13,9 @@ export interface TemporaryPinData extends Partial { } export type HorizontalSide = 'left' | 'right'; + +export interface TranslateAndScale { + scale: number; + translateX: number; + translateY: number; +} From ca7208ef410d8ebbde18924875aee7df4ae3abc8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 08:23:42 -0300 Subject: [PATCH 20/70] feat: create public method to scale and translate the entire pins container --- .../comments/html-pin-adapter/index.ts | 196 +++++++++++++----- 1 file changed, 144 insertions(+), 52 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index ce43cfc8..22d0fe6e 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -2,7 +2,13 @@ import { Logger, Observer } from '../../../common/utils'; import { PinMode } from '../../../web-components/comments/components/types'; import { Annotation, PinAdapter, PinCoordinates } from '../types'; -import { HorizontalSide, Simple2DPoint, SimpleParticipant, TemporaryPinData } from './types'; +import { + HorizontalSide, + TranslateAndScale, + Simple2DPoint, + SimpleParticipant, + TemporaryPinData, +} from './types'; export class HTMLPin implements PinAdapter { // Public properties @@ -53,7 +59,7 @@ export class HTMLPin implements PinAdapter { } this.createPinsContainer(); - this.dataAttribute &&= dataAttributeName; + this.dataAttribute = dataAttributeName || this.dataAttribute; this.isActive = false; this.prepareElements(); @@ -65,7 +71,6 @@ export class HTMLPin implements PinAdapter { this.pins = new Map(); this.onPinFixedObserver = new Observer({ logger: this.logger }); this.annotations = []; - this.renderAnnotationsPins(); document.body.addEventListener('select-annotation', this.annotationSelected); } @@ -73,7 +78,7 @@ export class HTMLPin implements PinAdapter { // ------- setup ------- /** * @function destroy - * @description destroys the container pin adapter. + * @description destroys the pin adapter. * @returns {void} * */ public destroy(): void { @@ -101,7 +106,7 @@ export class HTMLPin implements PinAdapter { /** * @function addElementListeners - * @description adds event listeners to the element + * @description adds event listeners to the element. * @param {string} id the id of the element to add the listeners to. * @returns {void} */ @@ -114,6 +119,7 @@ export class HTMLPin implements PinAdapter { * @function removeElementListeners * @description removes event listeners from the element * @param {string} id the id of the element to remove the listeners from. + * @param {boolean} keepObserver whether to keep he resize observer or not. * @returns {void} */ private removeElementListeners(id: string, keepObserver?: boolean): void { @@ -186,7 +192,7 @@ export class HTMLPin implements PinAdapter { /** * @function prepareElements - * @description set elements with the specified data attribute as pinnable + * @description sets elements with the specified data attribute as pinnable. * @returns {void} */ private prepareElements(): void { @@ -200,7 +206,7 @@ export class HTMLPin implements PinAdapter { /** * @function setAddCursor - * @description sets the mouse cursor to a special cursor + * @description sets the mouse cursor to a special cursor when hovering over all the elements with the specified data-attribute. * @returns {void} */ private setAddCursor(): void { @@ -212,7 +218,7 @@ export class HTMLPin implements PinAdapter { /** * @function removeAddCursor - * @description removes the special cursor + * @description removes the special cursor. * @returns {void} */ private removeAddCursor(): void { @@ -225,7 +231,8 @@ export class HTMLPin implements PinAdapter { // ------- public methods ------- /** * @function setPinsVisibility - * @param {boolean} isVisible - Controls the visibility of the pins. + * @description sets the visibility of the pins, hides them if it is not visible. + * @param {boolean} isVisible controls the visibility of the pins. * @returns {void} */ public setPinsVisibility(isVisible: boolean): void { @@ -242,15 +249,43 @@ export class HTMLPin implements PinAdapter { * @function translatePins * @description translates the wrapper containing all the pins of an element * @param {string} elementId - * @param {number} x - * @param {number} y + * @param {Partial} options the values to be applied to the transform property of the specified wrapper * @returns {void} */ - public translatePins = (elementId: string, x: number, y: number): void => { + public translateAndScalePins = (elementId: string, options: Partial): void => { const wrapper = this.divWrappers.get(elementId); if (!wrapper) return; - wrapper.style.transform = `translate(${x}px, ${y}px)`; + const { scale, translateX, translateY } = options; + const { + scale: currentScale, + translateX: currentTranslateX, + translateY: currentTranslateY, + } = this.parseTransform(wrapper.style.transform); + + wrapper.style.transform = `scale(${scale ?? currentScale}) translateX(${ + translateX ?? currentTranslateX + }px) translateY(${translateY ?? currentTranslateY}px)`; + }; + + /** + * @function translateAndScaleContainer + * @description apply a translation and scales the container containing all the pins + * @param {Partial} options the values to be applied to the transform property of the general pins container + * @returns {void} + */ + public translateAndScaleContainer = (options: Partial): void => { + const { scale, translateX, translateY } = options; + const { + scale: currentScale, + translateX: currentTranslateX, + translateY: currentTranslateY, + } = this.parseTransform(this.pinsContainer.style.transform); + + const transform = `scale(${scale ?? currentScale}) translateX(${ + translateX ?? currentTranslateX + }px) translateY(${translateY ?? currentTranslateY}px)`; + this.pinsContainer.style.transform = transform; }; /** @@ -272,9 +307,10 @@ export class HTMLPin implements PinAdapter { /** * @function setCommentsMetadata * @description stores data related to the local participant - * @param {HorizontalSide} side - * @param {string} avatar - * @param {string} name + * @param {HorizontalSide} side whether the comments sidebar is on the left or right side of the screen + * @param {string} avatar the avatar of the local participant + * @param {string} name the name of the local participant + * @returns {void} */ public setCommentsMetadata = (side: HorizontalSide, avatar: string, name: string): void => { this.commentsSide = side; @@ -292,8 +328,9 @@ export class HTMLPin implements PinAdapter { this.logger.log('updateAnnotations', annotations); this.annotations = annotations; + this.renderAnnotationsPins(); - if (!this.isActive && !this.isPinsVisible) return; + if (!this.isActive || !this.isPinsVisible) return; this.removeAnnotationsPins(); this.renderAnnotationsPins(); @@ -301,6 +338,7 @@ export class HTMLPin implements PinAdapter { /** * @function setActive + * @description sets the container pin adapter as active or not * @param {boolean} isOpen whether the container pin adapter is active or not. * @returns {void} */ @@ -321,7 +359,8 @@ export class HTMLPin implements PinAdapter { /** * @function renderTemporaryPin * @description creates a temporary pin with the id temporary-pin to mark where the annotation is being created - * @param {string} elementId - The id of the element where the temporary pin will be rendered. + * @param {string} elementId the id of the element where the temporary pin will be rendered. + * @returns {void} */ public renderTemporaryPin(elementId?: string): void { this.temporaryPinCoordinates.elementId ||= elementId; @@ -384,7 +423,7 @@ export class HTMLPin implements PinAdapter { const wrapper = this.divWrappers .get(elementId) - .querySelector('[data-pins-wrapper]') as HTMLDivElement; + ?.querySelector('[data-pins-wrapper]') as HTMLDivElement; if (!wrapper) return; if (this.pins.has(annotation.uuid)) { @@ -402,25 +441,30 @@ export class HTMLPin implements PinAdapter { * @description clears an element that no longer has the specified data attribute * @param {string} id the id of the element to be cleared * @param {boolean} keepObserver whether to keep he resize observer or not - * @returns + * @returns {void} */ private clearElement(id: string, keepObserver?: boolean): void { const element = this.elementsWithDataId[id]; if (!element) return; const wrapper = this.divWrappers.get(id); - const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; - const pins = pinsWrapper.children; - const { length } = pins; - - for (let i = 0; i < length; ++i) { - const pin = pins.item(0); - this.pins.delete(pin.id); - pin.remove(); + if (wrapper) { + const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; + const pins = pinsWrapper.children; + const { length } = pins; + + for (let i = 0; i < length; ++i) { + const pin = pins.item(0); + this.pins.delete(pin.id); + pin.remove(); + } + + wrapper.style.cursor = 'default'; + wrapper.remove(); } - wrapper.style.cursor = 'default'; this.removeElementListeners(id, keepObserver); + this.divWrappers.delete(id); delete this.elementsWithDataId[id]; } @@ -469,13 +513,14 @@ export class HTMLPin implements PinAdapter { if (!this.isActive || !this.isPinsVisible) return; this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.pointerEvents = 'auto'; this.addElementListeners(id); } /** * @function resetPins * @description Unselects selected pin and removes temporary pin. - * @param {KeyboardEvent} event - The keyboard event object. + * @param {KeyboardEvent} event the keyboard event object, this should be 'Escape'. * @returns {void} * */ private resetPins = (event?: KeyboardEvent): void => { @@ -493,7 +538,7 @@ export class HTMLPin implements PinAdapter { /** * @function annotationSelected * @description highlights the selected annotation and scrolls to it - * @param {CustomEvent} event + * @param {CustomEvent} event the emitted event object with the uuid of the selected annotation * @returns {void} */ private annotationSelected = ({ detail: { uuid } }: CustomEvent): void => { @@ -520,8 +565,9 @@ export class HTMLPin implements PinAdapter { /** * @function addTemporaryPinToElement - * @param {string} elementId - * @param {HTMLElement} pin + * @description adds the temporary pin and the temporary pin container to the element with the specified id. + * @param {string} elementId the id of the element where the temporary pin will be rendered. + * @param {HTMLElement} pin the temporary pin to be rendered. * @returns {void} */ private addTemporaryPinToElement(elementId: string, pin: HTMLElement): void { @@ -541,7 +587,7 @@ export class HTMLPin implements PinAdapter { /** * @function createTemporaryPinContainer * @description return a temporary pin container - * @returns {HTMLDivElement} + * @returns {HTMLDivElement} the temporary pin container, separated from the main pins container to avoid overflow issues */ private createTemporaryPinContainer(): HTMLDivElement { const temporaryContainer = document.createElement('div'); @@ -565,17 +611,21 @@ export class HTMLPin implements PinAdapter { pinsContainer.style.top = '0'; pinsContainer.style.left = '0'; pinsContainer.style.width = '100%'; + pinsContainer.style.pointerEvents = 'none'; pinsContainer.style.height = '100%'; + pinsContainer.style.transformOrigin = '0 0'; + this.pinsContainer = pinsContainer; this.container.appendChild(pinsContainer); } /** * @function createPin + * @description creates a pin element and sets its properties * @param {Annotation} annotation the annotation associated to the pin to be rendered * @param {number} x the x coordinate of the pin * @param {number} y the y coordinate of the pin - * @returns + * @returns {HTMLElement} the pin element */ private createPin(annotation: Annotation, x: number, y: number) { const pinElement = document.createElement('superviz-comments-annotation-pin'); @@ -587,12 +637,18 @@ export class HTMLPin implements PinAdapter { return pinElement; } + /** + * @function createWrapper + * @description creates a wrapper for the element with the specified id + * @param {HTMLElement} element the element to be wrapped + * @param {string} id the id of the element to be wrapped + * @returns {HTMLElement} the new wrapper element + */ private createWrapper(element: HTMLElement, id: string): HTMLElement { const container = element; const wrapperId = `superviz-id-${id}`; - if (container.querySelector(`#${wrapperId}`)) { - return; - } + + if (this.pinsContainer.querySelector(`#${wrapperId}`)) return; const containerRect = container.getBoundingClientRect(); @@ -600,12 +656,14 @@ export class HTMLPin implements PinAdapter { containerWrapper.setAttribute('data-wrapper-id', id); containerWrapper.id = wrapperId; - containerWrapper.style.position = 'fixed'; + containerWrapper.style.position = 'absolute'; containerWrapper.style.top = `${containerRect.top}px`; containerWrapper.style.left = `${containerRect.left}px`; containerWrapper.style.width = `${containerRect.width}px`; containerWrapper.style.height = `${containerRect.height}px`; containerWrapper.style.pointerEvents = 'none'; + containerWrapper.style.transform = 'translateX(0) translateY(0) scale(1)'; + containerWrapper.style.cursor = 'default'; const pinsWrapper = document.createElement('div'); pinsWrapper.setAttribute('data-pins-wrapper', ''); @@ -624,7 +682,7 @@ export class HTMLPin implements PinAdapter { /** * @function temporaryPinContainer * @description returns the temporary pin container - * @returns {HTMLDivElement} + * @returns {HTMLDivElement} the temporary pin container */ private get temporaryPinContainer(): HTMLDivElement { return this.divWrappers @@ -633,6 +691,12 @@ export class HTMLPin implements PinAdapter { } // ------- callbacks ------- + /** + * @function onClick + * @description handles the click event on the container; mainly, creates or moves the temporary pin + * @param {MouseEvent} event the mouse event object + * @returns {void} + */ private onClick = (event: MouseEvent): void => { if (!this.isActive || event.target === this.pins.get('temporary-pin')) return; @@ -686,28 +750,42 @@ export class HTMLPin implements PinAdapter { this.mouseDownCoordinates = { x, y }; }; + /** + * @function handleResizeObserverChanges + * @description handles the resize changes in the elements with the specified data attribute + * @param {ResizeObserverEntry[]} changes the elements with the specified data attribute that have changed + * @returns {void} + */ private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { changes.forEach((change) => { - const element = change.target; + const element = change.target as HTMLElement; const elementId = element.getAttribute(this.dataAttribute); const elementRect = element.getBoundingClientRect(); const wrapper = this.divWrappers.get(elementId); - wrapper.style.top = `${elementRect.top}px`; - wrapper.style.left = `${elementRect.left}px`; - wrapper.style.width = `${elementRect.width}px`; - wrapper.style.height = `${elementRect.height}px`; + + wrapper.style.top = `${elementRect.top + window.scrollY}`; + wrapper.style.left = `${elementRect.left + window.scrollX}`; + wrapper.style.width = `${elementRect.width}`; + wrapper.style.height = `${elementRect.height}`; + const subwrappers = wrapper.children; for (let i = 0; i < subwrappers.length; ++i) { const subwrapper = subwrappers.item(i) as HTMLElement; - subwrapper.style.top = `0`; - subwrapper.style.left = `0`; + subwrapper.style.top = '0'; + subwrapper.style.left = '0'; subwrapper.style.width = `${elementRect.width}px`; - subwrapper.style.height = `${elementRect.height}px`; + subwrapper.style.height = `${elementRect.height}`; } }); }; + /** + * @function handleMutationObserverChanges + * @description handles the changes in the value of the specified data attribute of the elements inside the container + * @param {MutationRecord[]} changes the changes in the value of the specified data attribute of the elements inside the container + * @returns {void} + */ private handleMutationObserverChanges = (changes: MutationRecord[]): void => { changes.forEach((change) => { const { target, oldValue } = change; @@ -734,7 +812,7 @@ export class HTMLPin implements PinAdapter { /** * @function onToggleAnnotationSidebar * @description Removes temporary pin and unselects selected pin - * @param {CustomEvent} event + * @param {CustomEvent} event the emitted event object with the info about if the annotation sidebar is open or not * @returns {void} */ private onToggleAnnotationSidebar = ({ detail }: CustomEvent): void => { @@ -742,12 +820,26 @@ export class HTMLPin implements PinAdapter { if (open) return; - this.pins.forEach((pinElement) => { - pinElement.removeAttribute('active'); - }); + this.resetSelectedPin(); if (this.pins.has('temporary-pin')) { this.removeAnnotationPin('temporary-pin'); } }; + + /** + * @function parseTransform + * @description parses the transform property of an element + * @param {string} transform the transform property of an element + * @returns {TranslateAndScale} an object with the key-value pairs of the transform property + */ + private parseTransform(transform: string): TranslateAndScale { + return Array.from(transform.matchAll(/(\w+)\((.+?)\)/gm)).reduce( + (agg: any, [, property, value]: any) => ({ + ...agg, + [property]: (value as string).replace('px', ''), + }), + {}, + ) as TranslateAndScale; + } } From 8365d11ecb9209a04845b39a4361d5f0e60018c2 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 11:05:19 -0300 Subject: [PATCH 21/70] feat: update pins positions on scroll --- .../comments/html-pin-adapter/index.ts | 66 ++++++++++++++----- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 22d0fe6e..89e9e988 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -43,6 +43,7 @@ export class HTMLPin implements PinAdapter { private elementsWithDataId: Record = {}; private divWrappers: Map = new Map(); private pins: Map; + private traversedDom: Set = new Set(); // Observers private resizeObserver: ResizeObserver; @@ -73,6 +74,9 @@ export class HTMLPin implements PinAdapter { this.annotations = []; document.body.addEventListener('select-annotation', this.annotationSelected); + Object.values(this.elementsWithDataId).forEach((element) => { + this.addScrollListeners(element); + }); } // ------- setup ------- @@ -102,6 +106,10 @@ export class HTMLPin implements PinAdapter { this.annotations = []; document.body.removeEventListener('select-annotation', this.annotationSelected); + document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); + this.traversedDom.forEach((element) => { + element.removeEventListener('scroll', this.onScroll); + }); } /** @@ -154,6 +162,21 @@ export class HTMLPin implements PinAdapter { document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } + /** + * @function addScrollListeners + */ + private addScrollListeners(element: HTMLElement): void { + let el = element; + while (el.parentElement) { + el = el.parentElement; + + if (this.traversedDom.has(el)) break; + + this.traversedDom.add(el); + el.addEventListener('scroll', this.onScroll); + } + } + /** * @function removeListeners * @description removes event listeners from the container element. @@ -162,7 +185,6 @@ export class HTMLPin implements PinAdapter { private removeListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id, true)); document.body.removeEventListener('keyup', this.resetPins); - document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } /** @@ -211,7 +233,8 @@ export class HTMLPin implements PinAdapter { */ private setAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { - this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = + 'url("https://production.cdn.superviz.com/static/pin-add.png") 0 100, pointer'; this.divWrappers.get(id).style.pointerEvents = 'auto'; }); } @@ -509,10 +532,12 @@ export class HTMLPin implements PinAdapter { this.elementsWithDataId[id] = element; this.resizeObserver?.observe(element); + this.addScrollListeners(element); if (!this.isActive || !this.isPinsVisible) return; - this.divWrappers.get(id).style.cursor = 'url("") 0 100, pointer'; + this.divWrappers.get(id).style.cursor = + 'url("https://production.cdn.superviz.com/static/pin-add.png") 0 100, pointer'; this.divWrappers.get(id).style.pointerEvents = 'auto'; this.addElementListeners(id); } @@ -656,7 +681,7 @@ export class HTMLPin implements PinAdapter { containerWrapper.setAttribute('data-wrapper-id', id); containerWrapper.id = wrapperId; - containerWrapper.style.position = 'absolute'; + containerWrapper.style.position = 'fixed'; containerWrapper.style.top = `${containerRect.top}px`; containerWrapper.style.left = `${containerRect.left}px`; containerWrapper.style.width = `${containerRect.width}px`; @@ -740,6 +765,21 @@ export class HTMLPin implements PinAdapter { document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; + /** + * @function onScroll + */ + private onScroll = (): void => { + Object.entries(this.elementsWithDataId).forEach(([key, value]) => { + const wrapper = this.divWrappers.get(key); + const elementRect = value.getBoundingClientRect(); + + wrapper.style.top = `${elementRect.top}px`; + wrapper.style.left = `${elementRect.left}px`; + wrapper.style.width = `${elementRect.width}px`; + wrapper.style.height = `${elementRect.height}px`; + }); + }; + /** * @function onMouseDown * @description stores the mouse down coordinates @@ -763,20 +803,10 @@ export class HTMLPin implements PinAdapter { const elementRect = element.getBoundingClientRect(); const wrapper = this.divWrappers.get(elementId); - wrapper.style.top = `${elementRect.top + window.scrollY}`; - wrapper.style.left = `${elementRect.left + window.scrollX}`; - wrapper.style.width = `${elementRect.width}`; - wrapper.style.height = `${elementRect.height}`; - - const subwrappers = wrapper.children; - - for (let i = 0; i < subwrappers.length; ++i) { - const subwrapper = subwrappers.item(i) as HTMLElement; - subwrapper.style.top = '0'; - subwrapper.style.left = '0'; - subwrapper.style.width = `${elementRect.width}px`; - subwrapper.style.height = `${elementRect.height}`; - } + wrapper.style.top = `${elementRect.top + window.scrollY}px`; + wrapper.style.left = `${elementRect.left + window.scrollX}px`; + wrapper.style.width = `${elementRect.width}px`; + wrapper.style.height = `${elementRect.height}px`; }); }; From 3513baf0c7a84969a7d3683109e9f58a9b0567d4 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 11:05:43 -0300 Subject: [PATCH 22/70] fix: stop showing comments over delete modal --- src/web-components/comments/css/comments.style.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web-components/comments/css/comments.style.ts b/src/web-components/comments/css/comments.style.ts index 2ce4a71f..6dd4f7b8 100644 --- a/src/web-components/comments/css/comments.style.ts +++ b/src/web-components/comments/css/comments.style.ts @@ -12,7 +12,6 @@ export const commentsStyle = css` bottom: 0; box-shadow: -2px 0 4px 0 rgba(0, 0, 0, 0.1); height: 100%; - z-index: 20; } .container-close { From d6b59223f56b27079c645a54df22cee3406abdbb Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 11:20:28 -0300 Subject: [PATCH 23/70] feat: create test for onScroll callback --- .../comments/html-pin-adapter/index.test.ts | 41 +++++++++++++++---- .../comments/html-pin-adapter/index.ts | 8 +++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 4cfd37e7..cb28ca7c 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -137,7 +137,7 @@ describe('HTMLPinAdapter', () => { instance['removeListeners'](); - expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(2); + expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(1); expect(wrapperRemoveEventListenerSpy).toHaveBeenCalledTimes(6); }); @@ -157,7 +157,7 @@ describe('HTMLPinAdapter', () => { jest.restoreAllMocks(); }); - test('should select annotation pin', async () => { + test('should select annotation pin', () => { instance.updateAnnotations([MOCK_ANNOTATION_HTML]); expect(instance['selectedPin']).toBeNull(); @@ -173,7 +173,7 @@ describe('HTMLPinAdapter', () => { expect([...instance['pins'].values()].some((pin) => pin.hasAttribute('active'))).toBeTruthy(); }); - test('should not select annotation pin if uuid is not defined', async () => { + test('should not select annotation pin if uuid is not defined', () => { instance.updateAnnotations([MOCK_ANNOTATION_HTML]); expect(instance['selectedPin']).toBeNull(); @@ -513,7 +513,7 @@ describe('HTMLPinAdapter', () => { expect(instance['selectedPin']).toBeNull(); }); - test('should not select annotation pin if it does not exist', async () => { + test('should not select annotation pin if it does not exist', () => { instance.updateAnnotations([MOCK_ANNOTATION_HTML]); expect(instance['selectedPin']).toBeNull(); @@ -611,8 +611,7 @@ describe('HTMLPinAdapter', () => { }); describe('updateAnnotations', () => { - test('should not render annotations if the adapter is not active and visibility is false', async () => { - instance.setActive(false); + test('should not render annotations if visibility is false', () => { instance.setPinsVisibility(false); instance.updateAnnotations([MOCK_ANNOTATION_HTML]); @@ -1024,8 +1023,8 @@ describe('HTMLPinAdapter', () => { expect(wrapper.style.height).toEqual(`${height}px`); expect(wrapper.style.top).toEqual(`${top}px`); expect(wrapper.style.left).toEqual(`${left}px`); - expect(pinsWrapper.style.width).toEqual(`${width}px`); - expect(pinsWrapper.style.height).toEqual(`${height}px`); + expect(pinsWrapper.style.width).toEqual(`100%`); + expect(pinsWrapper.style.height).toEqual(`100%`); expect(pinsWrapper.style.top).toEqual('0px'); expect(pinsWrapper.style.left).toEqual('0px'); }); @@ -1154,4 +1153,30 @@ describe('HTMLPinAdapter', () => { expect(spy).toHaveBeenCalledWith('temporary-pin'); }); }); + + describe('onScroll', () => { + test('should update the wrappers positions to keep them in the same position relative to the elements with the specified data-attribute', () => { + const element1 = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; + const element2 = document.body.querySelector('[data-superviz-id="2"]') as HTMLElement; + const wrapper1 = instance['divWrappers'].get('1') as HTMLElement; + const wrapper2 = instance['divWrappers'].get('2') as HTMLElement; + + const element1Rect = element1.getBoundingClientRect(); + const element2Rect = element2.getBoundingClientRect(); + + wrapper1.style.top = `${Math.random()}px`; + wrapper1.style.left = `${Math.random()}px`; + + wrapper2.style.top = `${Math.random()}px`; + wrapper2.style.left = `${Math.random()}px`; + + instance['onScroll'](); + + expect(wrapper1.style.top).toEqual(`${element1Rect.top}px`); + expect(wrapper1.style.left).toEqual(`${element1Rect.left}px`); + + expect(wrapper2.style.top).toEqual(`${element2Rect.top}px`); + expect(wrapper2.style.left).toEqual(`${element2Rect.left}px`); + }); + }); }); diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 89e9e988..cc06d72e 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -164,6 +164,9 @@ export class HTMLPin implements PinAdapter { /** * @function addScrollListeners + * @description adds scroll event listeners to the element and its parents, until a parent that already has the scroll listener is found. + * @param {HTMLElement} element the element to add the scroll listener to. + * @returns {void} */ private addScrollListeners(element: HTMLElement): void { let el = element; @@ -351,9 +354,8 @@ export class HTMLPin implements PinAdapter { this.logger.log('updateAnnotations', annotations); this.annotations = annotations; - this.renderAnnotationsPins(); - if (!this.isActive || !this.isPinsVisible) return; + if (!this.isPinsVisible) return; this.removeAnnotationsPins(); this.renderAnnotationsPins(); @@ -767,6 +769,8 @@ export class HTMLPin implements PinAdapter { /** * @function onScroll + * @description moves the wrappers to the correct position when the user scrolls + * @returns {void} */ private onScroll = (): void => { Object.entries(this.elementsWithDataId).forEach(([key, value]) => { From 2ed4924ec16b1dd776a4daf55e82ecc94f866d8b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 12:13:40 -0300 Subject: [PATCH 24/70] fix: remove pin from element even if element does not have the specified data-attribute --- src/components/comments/html-pin-adapter/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index cc06d72e..721df10d 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -323,11 +323,16 @@ export class HTMLPin implements PinAdapter { public removeAnnotationPin(uuid: string): void { const pinElement = this.pins.get(uuid); - if (!pinElement) return; + if (!pinElement && uuid === 'temporary-pin') return; - pinElement.remove(); - this.pins.delete(uuid); - this.annotations = this.annotations.filter((annotation) => annotation.uuid !== uuid); + if (pinElement) { + pinElement.remove(); + this.pins.delete(uuid); + } + + this.annotations = this.annotations.filter((annotation) => { + return annotation.uuid !== uuid; + }); } /** From 27b66affdc30ad2ffc640fb4e2337fdc7e8de3a1 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:09:33 -0300 Subject: [PATCH 25/70] fix: toggle annotation active attribute when clicking on it multiple times --- .../comments/html-pin-adapter/index.test.ts | 16 ++++++++++++++++ .../comments/html-pin-adapter/index.ts | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index cb28ca7c..6cb395fa 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -1075,6 +1075,22 @@ describe('HTMLPinAdapter', () => { expect(setElementsSpy).not.toHaveBeenCalled(); }); + test('should unselect pin if the attribute is removed from the element', () => { + const change = { + target: document.createElement('div') as HTMLElement, + oldValue: '1', + } as unknown as MutationRecord; + + const selectedPin = document.createElement('div'); + selectedPin.setAttribute('elementId', '1'); + instance['selectedPin'] = selectedPin; + + const spy = jest.spyOn(document.body, 'dispatchEvent'); + instance['handleMutationObserverChanges']([change]); + + expect(spy).toHaveBeenCalledWith(new CustomEvent('select-annotation')); + }); + test('should clear element if the attribute changes, but still exists', () => { const change = { target: document.body.querySelector('[data-superviz-id="1"]') as HTMLElement, diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 721df10d..1190892c 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -563,7 +563,7 @@ export class HTMLPin implements PinAdapter { if (!this.temporaryPinCoordinates.elementId) return; this.removeAnnotationPin('temporary-pin'); - this.temporaryPinContainer.remove(); + this.temporaryPinContainer?.remove(); this.temporaryPinCoordinates = {}; }; @@ -593,6 +593,12 @@ export class HTMLPin implements PinAdapter { pinElement.setAttribute('active', ''); this.selectedPin = pinElement; + + const newSelectedAnnotation = this.annotations.find((annotation) => annotation.uuid === uuid); + this.selectedPin.setAttribute( + 'elementId', + JSON.parse(newSelectedAnnotation.position).elementId, + ); }; /** @@ -717,9 +723,7 @@ export class HTMLPin implements PinAdapter { * @returns {HTMLDivElement} the temporary pin container */ private get temporaryPinContainer(): HTMLDivElement { - return this.divWrappers - .get(this.temporaryPinCoordinates.elementId) - .querySelector('#temporary-pin-container'); + return document.getElementById('temporary-pin-container') as HTMLDivElement; } // ------- callbacks ------- @@ -836,6 +840,11 @@ export class HTMLPin implements PinAdapter { if (attributeRemoved) { this.removeAnnotationPin('temporary-pin'); this.clearElement(oldValue); + + if (this.selectedPin?.getAttribute('elementId') === oldValue) { + document.body.dispatchEvent(new CustomEvent('unselect-annotation')); + } + return; } From adeea4ceec00c9b0023c1f7f598650f8b97b9587 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:16:58 -0300 Subject: [PATCH 26/70] feat: set high z-indexes to comments button and sidebar and to modals --- src/web-components/comments/css/comments.style.ts | 2 ++ src/web-components/comments/css/float-button.style.ts | 2 ++ src/web-components/modal/styles/index.style.ts | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/web-components/comments/css/comments.style.ts b/src/web-components/comments/css/comments.style.ts index 6dd4f7b8..b1299121 100644 --- a/src/web-components/comments/css/comments.style.ts +++ b/src/web-components/comments/css/comments.style.ts @@ -12,6 +12,8 @@ export const commentsStyle = css` bottom: 0; box-shadow: -2px 0 4px 0 rgba(0, 0, 0, 0.1); height: 100%; + + z-index: 99; } .container-close { diff --git a/src/web-components/comments/css/float-button.style.ts b/src/web-components/comments/css/float-button.style.ts index c99834f9..2a344848 100644 --- a/src/web-components/comments/css/float-button.style.ts +++ b/src/web-components/comments/css/float-button.style.ts @@ -18,6 +18,8 @@ export const floatButtonStyle = css` cursor: pointer; overflow: hidden; padding-left: 10px; + + z-index: 99; } button.float-button p { diff --git a/src/web-components/modal/styles/index.style.ts b/src/web-components/modal/styles/index.style.ts index 710aa162..06558f8c 100644 --- a/src/web-components/modal/styles/index.style.ts +++ b/src/web-components/modal/styles/index.style.ts @@ -1,6 +1,6 @@ import { css } from 'lit'; -export const modalStyle = css` +export const modalStyle = css` .modal--overlay { position: absolute; top: 0; @@ -9,6 +9,8 @@ export const modalStyle = css` width: 100%; height: 100%; background: rgba(var(--sv-gray-400), 0.8); + + z-index: 99; } .modal--container { @@ -22,6 +24,8 @@ export const modalStyle = css` width: 100%; height: 100%; background: transparent; + + z-index: 99; } .modal--container > .modal { From 9e22f30b6b0f91ea98425ac508fb2269f5decdd1 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:24:27 -0300 Subject: [PATCH 27/70] feat: set z-index to pins container --- src/components/comments/html-pin-adapter/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 1190892c..630c15f1 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -652,6 +652,7 @@ export class HTMLPin implements PinAdapter { pinsContainer.style.pointerEvents = 'none'; pinsContainer.style.height = '100%'; pinsContainer.style.transformOrigin = '0 0'; + pinsContainer.style.zIndex = '99'; this.pinsContainer = pinsContainer; this.container.appendChild(pinsContainer); From 56d86cdf888b1b2ad08379ef3934d33f8633a727 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:41:43 -0300 Subject: [PATCH 28/70] fix: set selectPin to null when removing data-attribute --- src/components/comments/html-pin-adapter/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 630c15f1..2688006e 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -844,6 +844,7 @@ export class HTMLPin implements PinAdapter { if (this.selectedPin?.getAttribute('elementId') === oldValue) { document.body.dispatchEvent(new CustomEvent('unselect-annotation')); + this.selectedPin = null; } return; From 853a4aacb2368ce8de17eb97186d5bf5d309dcb5 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:46:48 -0300 Subject: [PATCH 29/70] fix: pass options to html pin constructor inside an object --- src/components/comments/html-pin-adapter/index.ts | 5 ++++- src/components/comments/html-pin-adapter/types.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 2688006e..790a4109 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -8,6 +8,7 @@ import { Simple2DPoint, SimpleParticipant, TemporaryPinData, + HTMLPinOptions, } from './types'; export class HTMLPin implements PinAdapter { @@ -49,7 +50,7 @@ export class HTMLPin implements PinAdapter { private resizeObserver: ResizeObserver; private mutationObserver: MutationObserver; - constructor(containerId: string, dataAttributeName?: string) { + constructor(containerId: string, options?: HTMLPinOptions) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; @@ -59,6 +60,8 @@ export class HTMLPin implements PinAdapter { throw new Error(message); } + const { dataAttributeName } = options; + this.createPinsContainer(); this.dataAttribute = dataAttributeName || this.dataAttribute; this.isActive = false; diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index b197a781..bdc97419 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -19,3 +19,7 @@ export interface TranslateAndScale { translateX: number; translateY: number; } + +export interface HTMLPinOptions { + dataAttributeName?: string; +} From 456f1034b33fee675d3666f9544dc28059d39e3a Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 13:58:25 -0300 Subject: [PATCH 30/70] fix: set to undefined instead of using keyword delete --- src/components/comments/html-pin-adapter/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 790a4109..3e2cba7d 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -96,15 +96,15 @@ export class HTMLPin implements PinAdapter { this.divWrappers.clear(); this.pins.forEach((pin) => pin.remove()); this.pins.clear(); - delete this.divWrappers; - delete this.pins; - delete this.elementsWithDataId; - delete this.logger; + this.divWrappers = undefined; + this.pins = undefined; + this.elementsWithDataId = undefined; + this.logger = undefined; this.onPinFixedObserver.destroy(); - delete this.onPinFixedObserver; + this.onPinFixedObserver = undefined; this.pinsContainer.remove(); - delete this.pinsContainer; - delete this.container; + this.pinsContainer = undefined; + this.container = undefined; this.annotations = []; From 88c059ff52a167ec4890536fe41bcf1335d25082 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 14:12:06 -0300 Subject: [PATCH 31/70] feat: remove elements from scroll watch list if they're not useful anymore --- src/components/comments/html-pin-adapter/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3e2cba7d..40089c13 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -173,7 +173,7 @@ export class HTMLPin implements PinAdapter { */ private addScrollListeners(element: HTMLElement): void { let el = element; - while (el.parentElement) { + while (el.parentElement && el.parentElement !== document.body) { el = el.parentElement; if (this.traversedDom.has(el)) break; @@ -641,6 +641,18 @@ export class HTMLPin implements PinAdapter { return temporaryContainer; } + private updateTraversedDomList(id: string) { + let element: HTMLElement = this.elementsWithDataId[id].parentElement; + while (element) { + if (element.querySelector(`[${this.dataAttribute}]`)) { + break; + } + + this.traversedDom.delete(element); + element = element.parentElement; + } + } + /** * @function createPinsContainer * @description creates the container where pins will be appended and appends it to the DOM (either the parent of the element with the specified id or the body) @@ -842,6 +854,7 @@ export class HTMLPin implements PinAdapter { const attributeRemoved = !dataId && oldValue; if (attributeRemoved) { + this.updateTraversedDomList(oldValue); this.removeAnnotationPin('temporary-pin'); this.clearElement(oldValue); From 5622bcb65f1b60361bfb6081af70d99639c3650d Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 14:27:30 -0300 Subject: [PATCH 32/70] feat: throw error in case of invalid dataAttributeName --- src/components/comments/html-pin-adapter/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 40089c13..196004f1 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -63,6 +63,12 @@ export class HTMLPin implements PinAdapter { const { dataAttributeName } = options; this.createPinsContainer(); + + if (this.dataAttribute === '') throw new Error('dataAttributeName cannot be an empty string'); + if (this.dataAttribute === null) throw new Error('dataAttributeName cannot be null'); + if (this.dataAttribute && typeof this.dataAttribute !== 'string') + throw new Error('dataAttributeName must be a string'); + this.dataAttribute = dataAttributeName || this.dataAttribute; this.isActive = false; this.prepareElements(); From 157f9599b2e17bfd797c709f42ce278b4bba48d4 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Jan 2024 14:36:06 -0300 Subject: [PATCH 33/70] feat: give initializer to html pin constructor second parameter --- src/components/comments/html-pin-adapter/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 196004f1..a2292e69 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -50,7 +50,7 @@ export class HTMLPin implements PinAdapter { private resizeObserver: ResizeObserver; private mutationObserver: MutationObserver; - constructor(containerId: string, options?: HTMLPinOptions) { + constructor(containerId: string, options: HTMLPinOptions = {}) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; @@ -60,6 +60,9 @@ export class HTMLPin implements PinAdapter { throw new Error(message); } + if (typeof options !== 'object') + throw new Error('Second argument of the HTMLPin constructor must be an object'); + const { dataAttributeName } = options; this.createPinsContainer(); From 236fd8f28487d3261a21fb8733e1190c53344165 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 9 Jan 2024 13:23:59 -0300 Subject: [PATCH 34/70] feat: create pins wrappers as children or siblings of elements --- .../comments/html-pin-adapter/index.test.ts | 349 +++++++++--------- .../comments/html-pin-adapter/index.ts | 336 +++++++---------- .../comments/html-pin-adapter/types.ts | 6 - 3 files changed, 291 insertions(+), 400 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 6cb395fa..05a23c4e 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -2,12 +2,6 @@ import { MOCK_ANNOTATION } from '../../../../__mocks__/comments.mock'; import { HTMLPin } from '.'; -window.ResizeObserver = jest.fn().mockReturnValue({ - observe: jest.fn(), - disconnect: jest.fn(), - unobserve: jest.fn(), -}); - const MOCK_ANNOTATION_HTML = { ...MOCK_ANNOTATION, position: JSON.stringify({ @@ -56,6 +50,36 @@ describe('HTMLPinAdapter', () => { 'Element with id not-found-html not found', ); }); + + test('should throw error if second argument is not of type object', () => { + expect(() => new HTMLPin('container', 'not-object' as any)).toThrowError( + 'Second argument of the HTMLPin constructor must be an object', + ); + }); + + test('should throw error if dataAttributeName is an empty string', () => { + expect(() => new HTMLPin('container', { dataAttributeName: '' })).toThrowError( + 'dataAttributeName must be a non-empty string', + ); + }); + + test('should throw error if dataAttributeName is null', () => { + expect(() => new HTMLPin('container', { dataAttributeName: null as any })).toThrowError( + 'dataAttributeName cannot be null', + ); + }); + + test('should throw error if dataAttributeName is not a string', () => { + expect(() => new HTMLPin('container', { dataAttributeName: 123 as any })).toThrowError( + 'dataAttributeName must be a non-empty string', + ); + }); + + test('should call requestAnimationFrame if there is a void element', () => { + document.body.innerHTML = '
'; + const pin = new HTMLPin('container'); + expect(pin['animateFrame']).toBeTruthy(); + }); }); describe('destroy', () => { @@ -71,7 +95,6 @@ describe('HTMLPinAdapter', () => { const removeElementListenersSpy = jest.spyOn(document.body as any, 'removeEventListener'); const removeSpy = jest.fn(); const removeEventListenerSpy = jest.fn(); - const disconnectSpy = jest.spyOn(instance['resizeObserver'], 'disconnect'); const wrappers = [...instance['divWrappers']].map(([entry, value]) => { return [ @@ -83,7 +106,6 @@ describe('HTMLPinAdapter', () => { instance.destroy(); - expect(disconnectSpy).toHaveBeenCalled(); expect(removeListenersSpy).toHaveBeenCalled(); expect(removeObserversSpy).toHaveBeenCalled(); expect(onPinFixedObserverSpy).toHaveBeenCalled(); @@ -97,7 +119,6 @@ describe('HTMLPinAdapter', () => { expect(instance['pins']).toEqual(undefined); expect(instance['onPinFixedObserver']).toEqual(undefined); expect(instance['divWrappers']).toEqual(undefined); - expect(instance['resizeObserver']).toEqual(undefined); expect(instance['mutationObserver']).toEqual(undefined); }); }); @@ -140,16 +161,6 @@ describe('HTMLPinAdapter', () => { expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(1); expect(wrapperRemoveEventListenerSpy).toHaveBeenCalledTimes(6); }); - - test('should not call resizeObserver.unobserve if flag is true', () => { - instance['removeElementListeners']('1', true); - expect(instance['resizeObserver'].unobserve).not.toHaveBeenCalled(); - }); - - test('should call resizeObserver.unobserve if flag is false', () => { - instance['removeElementListeners']('1', false); - expect(instance['resizeObserver'].unobserve).toHaveBeenCalled(); - }); }); describe('annotationSelected', () => { @@ -401,6 +412,8 @@ describe('HTMLPinAdapter', () => { instance['annotations'] = [MOCK_ANNOTATION_HTML]; instance['renderAnnotationsPins'](); + expect(instance['divWrappers'].get('1')).not.toEqual(undefined); + const wrapper = instance['divWrappers'].get('1') as HTMLElement; expect(wrapper.style.cursor).not.toEqual('default'); @@ -412,8 +425,8 @@ describe('HTMLPinAdapter', () => { expect(spy).toHaveBeenCalled(); expect(instance['pins'].size).toEqual(0); - expect(Object.keys(instance['elementsWithDataId']).length).toEqual(2); - expect(wrapper.style.cursor).toEqual('default'); + expect(instance['elementsWithDataId']['1']).toEqual(undefined); + expect(instance['divWrappers'].get('1')).toEqual(undefined); }); test('should not clear element if it is not stored in elementsWithDataId', () => { @@ -725,107 +738,6 @@ describe('HTMLPinAdapter', () => { }); }); - describe('translateAndScalePins', () => { - afterEach(() => { - instance['divWrappers'].clear(); - }); - - test('should apply the correct translate and scale to the specified pins wrapper', () => { - const wrapper = instance['divWrappers'].get('1') as HTMLElement; - const transformObject = { - scale: Math.random() * 10, - translateX: Math.random() * 1000, - translateY: Math.random() * 1000, - }; - - const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; - - expect(wrapper.style.transform).not.toEqual(transformString); - - instance['translateAndScalePins']('1', transformObject); - - expect(wrapper.style.transform).toEqual(transformString); - }); - - test('should not apply the transform if the wrapper does not exist', () => { - const transformObject = { - scale: Math.random() * 10, - translateX: Math.random() * 1000, - translateY: Math.random() * 1000, - }; - - const spyParse = jest.spyOn(instance as any, 'parseTransform'); - - instance['translateAndScalePins']('not-found', transformObject); - - expect(instance['divWrappers'].has('not-found')).toBeFalsy(); - expect(spyParse).not.toHaveBeenCalled(); - }); - - test('should keep previous transform if the new transform is not defined', () => { - const wrapper = instance['divWrappers'].get('1') as HTMLElement; - const transformObject = { - scale: Math.random() * 10, - translateX: Math.random() * 1000, - translateY: Math.random() * 1000, - }; - - const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; - - expect(wrapper.style.transform).not.toEqual(transformString); - - instance['translateAndScalePins']('1', transformObject); - - expect(wrapper.style.transform).toEqual(transformString); - - instance['translateAndScalePins']('1', {}); - - expect(wrapper.style.transform).toEqual(transformString); - }); - }); - - describe('translateAndScaleContainer', () => { - beforeEach(() => { - instance['pinsContainer'].style.transform = 'scale(1) translateX(0px) translateY(0px)'; - }); - - test('should apply the correct translate and scale to the pins container', () => { - const transformObject = { - scale: Math.random() * 10, - translateX: Math.random() * 1000, - translateY: Math.random() * 1000, - }; - - const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; - - expect(instance['pinsContainer'].style.transform).not.toEqual(transformString); - - instance['translateAndScaleContainer'](transformObject); - - expect(instance['pinsContainer'].style.transform).toEqual(transformString); - }); - - test('should keep previous transform if the new transform is not defined', () => { - const transformObject = { - scale: Math.random() * 10, - translateX: Math.random() * 1000, - translateY: Math.random() * 1000, - }; - - const transformString = `scale(${transformObject.scale}) translateX(${transformObject.translateX}px) translateY(${transformObject.translateY}px)`; - - expect(instance['pinsContainer'].style.transform).not.toEqual(transformString); - - instance['translateAndScaleContainer'](transformObject); - - expect(instance['pinsContainer'].style.transform).toEqual(transformString); - - instance['translateAndScaleContainer']({}); - - expect(instance['pinsContainer'].style.transform).toEqual(transformString); - }); - }); - describe('setElementReadyToPin', () => { beforeEach(() => { instance['divWrappers'].get('1')!.style.cursor = 'default'; @@ -877,7 +789,6 @@ describe('HTMLPinAdapter', () => { const spySet = jest.spyOn(instance['divWrappers'], 'set'); const spyCreate = jest.spyOn(instance as any, 'createWrapper'); - instance['pinsContainer'].innerHTML = ''; instance['divWrappers'].clear(); delete instance['elementsWithDataId']['1']; @@ -950,21 +861,20 @@ describe('HTMLPinAdapter', () => { describe('createWrapper', () => { test('should create a new wrapper', () => { const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - instance['pinsContainer'].innerHTML = ''; + instance['divWrappers'].clear(); const wrapper = instance['createWrapper'](element, '1'); const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLElement; const containerRect = element.getBoundingClientRect(); expect(wrapper).toBeInstanceOf(HTMLDivElement); - expect(wrapper.style.position).toEqual('fixed'); - expect(wrapper.style.top).toEqual(`${containerRect.top}px`); - expect(wrapper.style.left).toEqual(`${containerRect.left}px`); - expect(wrapper.style.width).toEqual(`${containerRect.width}px`); - expect(wrapper.style.height).toEqual(`${containerRect.height}px`); + expect(wrapper.style.position).toEqual('absolute'); + expect(wrapper.style.top).toEqual('0px'); + expect(wrapper.style.left).toEqual('0px'); + expect(wrapper.style.width).toEqual('100%'); + expect(wrapper.style.height).toEqual('100%'); expect(wrapper.style.pointerEvents).toEqual('none'); - expect(wrapper.style.transform).toEqual('translateX(0) translateY(0) scale(1)'); expect(wrapper.style.cursor).toEqual('default'); expect(wrapper.getAttribute('data-wrapper-id')).toEqual('1'); expect(wrapper.id).toEqual('superviz-id-1'); @@ -980,53 +890,52 @@ describe('HTMLPinAdapter', () => { test('should not create a new wrapper if wrapper already exists', () => { const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - instance['pinsContainer'].innerHTML = ''; + + instance['divWrappers'].clear(); const wrapper1 = instance['createWrapper'](element, '1'); + instance['divWrappers'].set('1', wrapper1); + const wrapper2 = instance['createWrapper'](element, '1'); expect(wrapper1).toBeInstanceOf(HTMLDivElement); expect(wrapper2).toBe(undefined); }); - }); - describe('handleResizeObserverChanges', () => { - test('should update the wrapper position when the element is resized', () => { - const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - const wrapper = instance['divWrappers'].get('1') as HTMLElement; - const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLElement; + test('should create wrapper as sibling of the element if element is a void element', () => { + document.body.innerHTML = ''; + const element = document.querySelector('img') as HTMLElement; - const width = Math.random() * 1000; - const height = Math.random() * 1000; - const top = Math.random() * 1000; - const left = Math.random() * 1000; - - element.style.width = `${width}px`; - element.style.height = `${height}px`; - element.style.top = `${top}px`; - element.style.left = `${left}px`; - - const changes = { - target: element, - } as unknown as ResizeObserverEntry; - - element.getBoundingClientRect = jest.fn().mockReturnValue({ - width, - height, - top, - left, - }); + instance['divWrappers'].clear(); + instance['elementsWithDataId']['1'] = element; - instance['handleResizeObserverChanges']([changes]); + const wrapper = instance['createWrapper'](element, '1'); - expect(wrapper.style.width).toEqual(`${width}px`); - expect(wrapper.style.height).toEqual(`${height}px`); - expect(wrapper.style.top).toEqual(`${top}px`); - expect(wrapper.style.left).toEqual(`${left}px`); - expect(pinsWrapper.style.width).toEqual(`100%`); - expect(pinsWrapper.style.height).toEqual(`100%`); - expect(pinsWrapper.style.top).toEqual('0px'); - expect(pinsWrapper.style.left).toEqual('0px'); + const containerRect = element.getBoundingClientRect(); + + expect(wrapper.parentElement).toEqual(element.parentElement); + expect(wrapper.style.position).toEqual('fixed'); + expect(wrapper.style.top).toEqual(`${containerRect.top}px`); + expect(wrapper.style.left).toEqual(`${containerRect.left}px`); + expect(wrapper.style.width).toEqual(`${containerRect.width}px`); + expect(wrapper.style.height).toEqual(`${containerRect.height}px`); + + expect(instance['voidElementsWrappers'].get('1')).toEqual(wrapper); + }); + + test('should append wrapper of void element to body if element parent is not found', () => { + document.body.innerHTML = ''; + const element = document.createElement('img') as HTMLElement; + element.setAttribute('data-superviz-id', '1'); + + jest.spyOn(instance as any, 'setPositionNotStatic').mockImplementation(() => {}); + + instance['divWrappers'].clear(); + instance['elementsWithDataId']['1'] = element; + + const wrapper = instance['createWrapper'](element, '1'); + + expect(wrapper.parentElement).toEqual(document.body); }); }); @@ -1170,29 +1079,103 @@ describe('HTMLPinAdapter', () => { }); }); - describe('onScroll', () => { - test('should update the wrappers positions to keep them in the same position relative to the elements with the specified data-attribute', () => { - const element1 = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - const element2 = document.body.querySelector('[data-superviz-id="2"]') as HTMLElement; - const wrapper1 = instance['divWrappers'].get('1') as HTMLElement; - const wrapper2 = instance['divWrappers'].get('2') as HTMLElement; + describe('animate', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + test('should update pins positions', () => { + const spy = jest.spyOn(instance as any, 'updatePinsPositions'); + window.requestAnimationFrame = jest.fn(); + instance['animate'](); + expect(spy).toHaveBeenCalled(); + expect(window.requestAnimationFrame).toHaveBeenCalledWith(instance['animate']); + }); + }); + + describe('updatePinsPositions', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should update position of all wrappers of void elements', () => { + const div = document.body.querySelector('div') as HTMLDivElement; + const image = document.body.querySelector('img') as HTMLImageElement; + + instance['container'] = div as HTMLElement; + instance['prepareElements'](); + + image.getBoundingClientRect = jest.fn().mockReturnValue({ + left: 40, + top: 30, + width: 50, + height: 60, + }); + + const wrapper = instance['divWrappers'].get('image-id') as HTMLElement; + const spy = jest.spyOn(wrapper.style, 'setProperty'); + + instance['updatePinsPositions'](); + + expect(spy).toHaveBeenNthCalledWith(1, 'top', '30px'); + expect(spy).toHaveBeenNthCalledWith(2, 'left', '40px'); + expect(spy).toHaveBeenNthCalledWith(3, 'width', '50px'); + expect(spy).toHaveBeenNthCalledWith(4, 'height', '60px'); + }); - const element1Rect = element1.getBoundingClientRect(); - const element2Rect = element2.getBoundingClientRect(); + test('should not update if positions are the same', () => { + const div = document.body.querySelector('div') as HTMLDivElement; + const image = document.body.querySelector('img') as HTMLImageElement; - wrapper1.style.top = `${Math.random()}px`; - wrapper1.style.left = `${Math.random()}px`; + instance['container'] = div as HTMLElement; + instance['prepareElements'](); + + image.getBoundingClientRect = jest.fn().mockReturnValue({ + left: 40, + top: 30, + width: 50, + height: 60, + }); + + const wrapper = instance['divWrappers'].get('image-id') as HTMLElement; + + wrapper.getBoundingClientRect = jest.fn().mockReturnValue({ + left: 40, + top: 30, + width: 50, + height: 60, + }); + + const spy = jest.spyOn(wrapper.style, 'setProperty'); + + instance['updatePinsPositions'](); + + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('setPositionNotStatic', () => { + test('should set element position to relative if it is static', () => { + const element = document.createElement('div'); + + element.style.position = 'static'; + + instance['setPositionNotStatic'](element); + + expect(element.style.position).toEqual('relative'); + }); - wrapper2.style.top = `${Math.random()}px`; - wrapper2.style.left = `${Math.random()}px`; + test('should do nothing if element position is not static', () => { + const element = document.createElement('div'); - instance['onScroll'](); + element.style.position = 'absolute'; - expect(wrapper1.style.top).toEqual(`${element1Rect.top}px`); - expect(wrapper1.style.left).toEqual(`${element1Rect.left}px`); + instance['setPositionNotStatic'](element); - expect(wrapper2.style.top).toEqual(`${element2Rect.top}px`); - expect(wrapper2.style.left).toEqual(`${element2Rect.left}px`); + expect(element.style.position).toEqual('absolute'); }); }); }); diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index a2292e69..f28a02f9 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -1,10 +1,11 @@ +import { isEqual } from 'lodash'; + import { Logger, Observer } from '../../../common/utils'; import { PinMode } from '../../../web-components/comments/components/types'; import { Annotation, PinAdapter, PinCoordinates } from '../types'; import { HorizontalSide, - TranslateAndScale, Simple2DPoint, SimpleParticipant, TemporaryPinData, @@ -32,6 +33,7 @@ export class HTMLPin implements PinAdapter { // Data about the current state of the application private selectedPin: HTMLElement | null = null; private dataAttribute: string = 'data-superviz-id'; + private animateFrame: number; // Coordinates/Positions private mouseDownCoordinates: Simple2DPoint; @@ -40,16 +42,32 @@ export class HTMLPin implements PinAdapter { // Elements private container: HTMLElement; - private pinsContainer: HTMLDivElement; private elementsWithDataId: Record = {}; private divWrappers: Map = new Map(); private pins: Map; - private traversedDom: Set = new Set(); + private voidElementsWrappers: Map = new Map(); // Observers - private resizeObserver: ResizeObserver; private mutationObserver: MutationObserver; + // Consts + private readonly VOID_ELEMENTS = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ]; + constructor(containerId: string, options: HTMLPinOptions = {}) { this.logger = new Logger('@superviz/sdk/comments-component/container-pin-adapter'); this.container = document.getElementById(containerId) as HTMLElement; @@ -65,30 +83,27 @@ export class HTMLPin implements PinAdapter { const { dataAttributeName } = options; - this.createPinsContainer(); - - if (this.dataAttribute === '') throw new Error('dataAttributeName cannot be an empty string'); - if (this.dataAttribute === null) throw new Error('dataAttributeName cannot be null'); - if (this.dataAttribute && typeof this.dataAttribute !== 'string') - throw new Error('dataAttributeName must be a string'); + if (dataAttributeName === '') throw new Error('dataAttributeName must be a non-empty string'); + if (dataAttributeName === null) throw new Error('dataAttributeName cannot be null'); + if (dataAttributeName !== undefined && typeof dataAttributeName !== 'string') + throw new Error('dataAttributeName must be a non-empty string'); this.dataAttribute = dataAttributeName || this.dataAttribute; this.isActive = false; this.prepareElements(); this.mutationObserver = new MutationObserver(this.handleMutationObserverChanges); - this.resizeObserver = new ResizeObserver(this.handleResizeObserverChanges); this.observeContainer(); - this.observeElements(); this.pins = new Map(); this.onPinFixedObserver = new Observer({ logger: this.logger }); this.annotations = []; document.body.addEventListener('select-annotation', this.annotationSelected); - Object.values(this.elementsWithDataId).forEach((element) => { - this.addScrollListeners(element); - }); + + if (!this.voidElementsWrappers.size) return; + + this.animateFrame = requestAnimationFrame(this.animate); } // ------- setup ------- @@ -111,17 +126,13 @@ export class HTMLPin implements PinAdapter { this.logger = undefined; this.onPinFixedObserver.destroy(); this.onPinFixedObserver = undefined; - this.pinsContainer.remove(); - this.pinsContainer = undefined; this.container = undefined; - + this.voidElementsWrappers.clear(); + this.voidElementsWrappers = undefined; this.annotations = []; - + cancelAnimationFrame(this.animateFrame); document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); - this.traversedDom.forEach((element) => { - element.removeEventListener('scroll', this.onScroll); - }); } /** @@ -139,28 +150,21 @@ export class HTMLPin implements PinAdapter { * @function removeElementListeners * @description removes event listeners from the element * @param {string} id the id of the element to remove the listeners from. - * @param {boolean} keepObserver whether to keep he resize observer or not. * @returns {void} */ - private removeElementListeners(id: string, keepObserver?: boolean): void { + private removeElementListeners(id: string): void { this.divWrappers.get(id).removeEventListener('click', this.onClick, true); this.divWrappers.get(id).removeEventListener('mousedown', this.onMouseDown); - - if (keepObserver) return; - - this.resizeObserver.unobserve(this.elementsWithDataId[id]); } /** * @function removeObservers - * @description disconnects the mutation and resize observers. + * @description disconnects the observers. * @returns {void} */ private removeObservers(): void { this.mutationObserver.disconnect(); - this.resizeObserver.disconnect(); - delete this.mutationObserver; - delete this.resizeObserver; + this.mutationObserver = undefined; } /** @@ -175,22 +179,14 @@ export class HTMLPin implements PinAdapter { } /** - * @function addScrollListeners - * @description adds scroll event listeners to the element and its parents, until a parent that already has the scroll listener is found. - * @param {HTMLElement} element the element to add the scroll listener to. + * @function animate + * @description updates the position of the wrappers of the void elements. * @returns {void} */ - private addScrollListeners(element: HTMLElement): void { - let el = element; - while (el.parentElement && el.parentElement !== document.body) { - el = el.parentElement; - - if (this.traversedDom.has(el)) break; - - this.traversedDom.add(el); - el.addEventListener('scroll', this.onScroll); - } - } + private animate = (): void => { + this.updatePinsPositions(); + requestAnimationFrame(this.animate); + }; /** * @function removeListeners @@ -198,21 +194,10 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ private removeListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id, true)); + Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); document.body.removeEventListener('keyup', this.resetPins); } - /** - * @function observeElements - * @description observes the elements with the specified data attribute. - * @returns {void} - */ - private observeElements(): void { - Object.values(this.elementsWithDataId).forEach((element) => { - this.resizeObserver.observe(element); - }); - } - /** * @function observeContainer * @description observes the container for changes in the specified data attribute. @@ -283,49 +268,6 @@ export class HTMLPin implements PinAdapter { this.removeAnnotationsPins(); } - /** - * @function translatePins - * @description translates the wrapper containing all the pins of an element - * @param {string} elementId - * @param {Partial} options the values to be applied to the transform property of the specified wrapper - * @returns {void} - */ - public translateAndScalePins = (elementId: string, options: Partial): void => { - const wrapper = this.divWrappers.get(elementId); - if (!wrapper) return; - - const { scale, translateX, translateY } = options; - const { - scale: currentScale, - translateX: currentTranslateX, - translateY: currentTranslateY, - } = this.parseTransform(wrapper.style.transform); - - wrapper.style.transform = `scale(${scale ?? currentScale}) translateX(${ - translateX ?? currentTranslateX - }px) translateY(${translateY ?? currentTranslateY}px)`; - }; - - /** - * @function translateAndScaleContainer - * @description apply a translation and scales the container containing all the pins - * @param {Partial} options the values to be applied to the transform property of the general pins container - * @returns {void} - */ - public translateAndScaleContainer = (options: Partial): void => { - const { scale, translateX, translateY } = options; - const { - scale: currentScale, - translateX: currentTranslateX, - translateY: currentTranslateY, - } = this.parseTransform(this.pinsContainer.style.transform); - - const transform = `scale(${scale ?? currentScale}) translateX(${ - translateX ?? currentTranslateX - }px) translateY(${translateY ?? currentTranslateY}px)`; - this.pinsContainer.style.transform = transform; - }; - /** * @function removeAnnotationPin * @description Removes an annotation pin from the container. @@ -478,14 +420,32 @@ export class HTMLPin implements PinAdapter { }); } + /** + * @function updatePinsPositions + * @description updates the position of the wrappers of the void elements. + * @returns {void} + */ + private updatePinsPositions() { + this.voidElementsWrappers.forEach((wrapper, id) => { + const wrapperRect = JSON.stringify(wrapper.getBoundingClientRect()); + const elementRect = this.elementsWithDataId[id].getBoundingClientRect(); + + if (isEqual(JSON.stringify(elementRect), wrapperRect)) return; + + wrapper.style.setProperty('top', `${elementRect.top}px`); + wrapper.style.setProperty('left', `${elementRect.left}px`); + wrapper.style.setProperty('width', `${elementRect.width}px`); + wrapper.style.setProperty('height', `${elementRect.height}px`); + }); + } + /** * @function clearElement * @description clears an element that no longer has the specified data attribute * @param {string} id the id of the element to be cleared - * @param {boolean} keepObserver whether to keep he resize observer or not * @returns {void} */ - private clearElement(id: string, keepObserver?: boolean): void { + private clearElement(id: string): void { const element = this.elementsWithDataId[id]; if (!element) return; @@ -496,18 +456,22 @@ export class HTMLPin implements PinAdapter { const { length } = pins; for (let i = 0; i < length; ++i) { - const pin = pins.item(0); + const pin = pins.item(i); this.pins.delete(pin.id); - pin.remove(); } - wrapper.style.cursor = 'default'; wrapper.remove(); } - this.removeElementListeners(id, keepObserver); + this.voidElementsWrappers.delete(id); + this.removeElementListeners(id); this.divWrappers.delete(id); - delete this.elementsWithDataId[id]; + this.elementsWithDataId[id] = undefined; + + if (!this.voidElementsWrappers.size) { + cancelAnimationFrame(this.animateFrame); + this.animateFrame = undefined; + } } /** @@ -543,16 +507,13 @@ export class HTMLPin implements PinAdapter { */ private setElementReadyToPin(element: HTMLElement, id: string): void { if (this.elementsWithDataId[id]) return; + this.elementsWithDataId[id] = element; if (!this.divWrappers.get(id)) { const divWrapper = this.createWrapper(element, id); this.divWrappers.set(id, divWrapper); } - this.elementsWithDataId[id] = element; - this.resizeObserver?.observe(element); - this.addScrollListeners(element); - if (!this.isActive || !this.isPinsVisible) return; this.divWrappers.get(id).style.cursor = @@ -581,7 +542,7 @@ export class HTMLPin implements PinAdapter { /** * @function annotationSelected - * @description highlights the selected annotation and scrolls to it + * @description highlights the selected annotation * @param {CustomEvent} event the emitted event object with the uuid of the selected annotation * @returns {void} */ @@ -650,38 +611,6 @@ export class HTMLPin implements PinAdapter { return temporaryContainer; } - private updateTraversedDomList(id: string) { - let element: HTMLElement = this.elementsWithDataId[id].parentElement; - while (element) { - if (element.querySelector(`[${this.dataAttribute}]`)) { - break; - } - - this.traversedDom.delete(element); - element = element.parentElement; - } - } - - /** - * @function createPinsContainer - * @description creates the container where pins will be appended and appends it to the DOM (either the parent of the element with the specified id or the body) - * @returns {void} - */ - private createPinsContainer(): void { - const pinsContainer = document.createElement('div'); - pinsContainer.style.position = 'absolute'; - pinsContainer.style.top = '0'; - pinsContainer.style.left = '0'; - pinsContainer.style.width = '100%'; - pinsContainer.style.pointerEvents = 'none'; - pinsContainer.style.height = '100%'; - pinsContainer.style.transformOrigin = '0 0'; - pinsContainer.style.zIndex = '99'; - - this.pinsContainer = pinsContainer; - this.container.appendChild(pinsContainer); - } - /** * @function createPin * @description creates a pin element and sets its properties @@ -711,7 +640,7 @@ export class HTMLPin implements PinAdapter { const container = element; const wrapperId = `superviz-id-${id}`; - if (this.pinsContainer.querySelector(`#${wrapperId}`)) return; + if (this.divWrappers.get(id)) return; const containerRect = container.getBoundingClientRect(); @@ -719,14 +648,14 @@ export class HTMLPin implements PinAdapter { containerWrapper.setAttribute('data-wrapper-id', id); containerWrapper.id = wrapperId; - containerWrapper.style.position = 'fixed'; - containerWrapper.style.top = `${containerRect.top}px`; - containerWrapper.style.left = `${containerRect.left}px`; - containerWrapper.style.width = `${containerRect.width}px`; - containerWrapper.style.height = `${containerRect.height}px`; + containerWrapper.style.position = 'absolute'; containerWrapper.style.pointerEvents = 'none'; containerWrapper.style.transform = 'translateX(0) translateY(0) scale(1)'; containerWrapper.style.cursor = 'default'; + containerWrapper.style.top = `0`; + containerWrapper.style.left = `0`; + containerWrapper.style.width = `100%`; + containerWrapper.style.height = `100%`; const pinsWrapper = document.createElement('div'); pinsWrapper.setAttribute('data-pins-wrapper', ''); @@ -738,10 +667,47 @@ export class HTMLPin implements PinAdapter { pinsWrapper.style.height = '100%'; containerWrapper.appendChild(pinsWrapper); - this.pinsContainer.appendChild(containerWrapper); + if (!this.VOID_ELEMENTS.includes(this.elementsWithDataId[id].tagName.toLowerCase())) { + this.elementsWithDataId[id].appendChild(containerWrapper); + this.setPositionNotStatic(this.elementsWithDataId[id]); + return containerWrapper; + } + + containerWrapper.style.position = 'fixed'; + containerWrapper.style.top = `${containerRect.top}px`; + containerWrapper.style.left = `${containerRect.left}px`; + containerWrapper.style.width = `${containerRect.width}px`; + containerWrapper.style.height = `${containerRect.height}px`; + + let parent = this.elementsWithDataId[id].parentElement; + + if (!parent) parent = document.body; + + this.setPositionNotStatic(parent); + parent.appendChild(containerWrapper); + + this.voidElementsWrappers.set(id, containerWrapper); + + if (!this.animateFrame) { + this.animateFrame = requestAnimationFrame(this.animate); + } + return containerWrapper; } + /** + * @function setPositionNotStatic + * @description sets the position of the element to relative if it is static + * @param {HTMLElement} element the element to be checked + * @returns {void} + */ + private setPositionNotStatic(element: HTMLElement): void { + const { position } = window.getComputedStyle(element); + if (position !== 'static') return; + + element.style.setProperty('position', 'relative'); + } + /** * @function temporaryPinContainer * @description returns the temporary pin container @@ -771,11 +737,13 @@ export class HTMLPin implements PinAdapter { const elementId = wrapper.getAttribute('data-wrapper-id'); const rect = wrapper.getBoundingClientRect(); const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; + const scale = wrapper.getBoundingClientRect().width / wrapper.offsetWidth || 1; + + const x = (event.clientX - rect.left) / scale; + const y = (event.clientY - rect.top) / scale; - const originalX = mouseDownX - rect.x; - const originalY = mouseDownY - rect.y; + const originalX = (mouseDownX - rect.x) / scale; + const originalY = (mouseDownY - rect.y) / scale; const distance = Math.hypot(x - originalX, y - originalY); if (distance > 10) return; @@ -801,23 +769,6 @@ export class HTMLPin implements PinAdapter { document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; - /** - * @function onScroll - * @description moves the wrappers to the correct position when the user scrolls - * @returns {void} - */ - private onScroll = (): void => { - Object.entries(this.elementsWithDataId).forEach(([key, value]) => { - const wrapper = this.divWrappers.get(key); - const elementRect = value.getBoundingClientRect(); - - wrapper.style.top = `${elementRect.top}px`; - wrapper.style.left = `${elementRect.left}px`; - wrapper.style.width = `${elementRect.width}px`; - wrapper.style.height = `${elementRect.height}px`; - }); - }; - /** * @function onMouseDown * @description stores the mouse down coordinates @@ -828,26 +779,6 @@ export class HTMLPin implements PinAdapter { this.mouseDownCoordinates = { x, y }; }; - /** - * @function handleResizeObserverChanges - * @description handles the resize changes in the elements with the specified data attribute - * @param {ResizeObserverEntry[]} changes the elements with the specified data attribute that have changed - * @returns {void} - */ - private handleResizeObserverChanges = (changes: ResizeObserverEntry[]): void => { - changes.forEach((change) => { - const element = change.target as HTMLElement; - const elementId = element.getAttribute(this.dataAttribute); - const elementRect = element.getBoundingClientRect(); - const wrapper = this.divWrappers.get(elementId); - - wrapper.style.top = `${elementRect.top + window.scrollY}px`; - wrapper.style.left = `${elementRect.left + window.scrollX}px`; - wrapper.style.width = `${elementRect.width}px`; - wrapper.style.height = `${elementRect.height}px`; - }); - }; - /** * @function handleMutationObserverChanges * @description handles the changes in the value of the specified data attribute of the elements inside the container @@ -863,7 +794,6 @@ export class HTMLPin implements PinAdapter { const attributeRemoved = !dataId && oldValue; if (attributeRemoved) { - this.updateTraversedDomList(oldValue); this.removeAnnotationPin('temporary-pin'); this.clearElement(oldValue); @@ -876,7 +806,7 @@ export class HTMLPin implements PinAdapter { } if (oldValue && this.elementsWithDataId[oldValue]) { - this.clearElement(oldValue, true); + this.clearElement(oldValue); } this.setElementReadyToPin(target as HTMLElement, dataId); @@ -901,20 +831,4 @@ export class HTMLPin implements PinAdapter { this.removeAnnotationPin('temporary-pin'); } }; - - /** - * @function parseTransform - * @description parses the transform property of an element - * @param {string} transform the transform property of an element - * @returns {TranslateAndScale} an object with the key-value pairs of the transform property - */ - private parseTransform(transform: string): TranslateAndScale { - return Array.from(transform.matchAll(/(\w+)\((.+?)\)/gm)).reduce( - (agg: any, [, property, value]: any) => ({ - ...agg, - [property]: (value as string).replace('px', ''), - }), - {}, - ) as TranslateAndScale; - } } diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index bdc97419..4fc2ac7a 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -14,12 +14,6 @@ export interface TemporaryPinData extends Partial { export type HorizontalSide = 'left' | 'right'; -export interface TranslateAndScale { - scale: number; - translateX: number; - translateY: number; -} - export interface HTMLPinOptions { dataAttributeName?: string; } From c9ff9ef7e875068ce1c60fc057822229abc9cef0 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 9 Jan 2024 14:45:24 -0300 Subject: [PATCH 35/70] feat: allow user to specify a pattern of the specified data attribute value that should be ignored --- .../comments/html-pin-adapter/index.test.ts | 60 +++++++++++++++++++ .../comments/html-pin-adapter/index.ts | 25 ++++++-- .../comments/html-pin-adapter/types.ts | 1 + 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 05a23c4e..7862f213 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -1043,6 +1043,50 @@ describe('HTMLPinAdapter', () => { expect(renderAnnotationsSpy).not.toHaveBeenCalled(); expect(setElementsSpy).not.toHaveBeenCalled(); }); + + test('should clear element then do nothing if new value is filtered', () => { + document.body.innerHTML = + '
'; + instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + const change = { + target: document.body.querySelector('[data-superviz-id="1-matches"]') as HTMLElement, + oldValue: '2', + } as unknown as MutationRecord; + + setElementsSpy = jest.spyOn(instance as any, 'setElementReadyToPin'); + renderAnnotationsSpy = jest.spyOn(instance as any, 'renderAnnotationsPins'); + clearElementSpy = jest.spyOn(instance as any, 'clearElement'); + removeAnnotationSpy = jest.spyOn(instance as any, 'removeAnnotationPin'); + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).toHaveBeenCalled(); + expect(renderAnnotationsSpy).not.toHaveBeenCalled(); + expect(setElementsSpy).not.toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + }); + + test('should not clear element if old value was skipped', () => { + document.body.innerHTML = + '
'; + instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + const change = { + target: document.body.querySelector('[data-superviz-id="does-not-match"]') as HTMLElement, + oldValue: '1-matches', + } as unknown as MutationRecord; + + setElementsSpy = jest.spyOn(instance as any, 'setElementReadyToPin'); + renderAnnotationsSpy = jest.spyOn(instance as any, 'renderAnnotationsPins'); + clearElementSpy = jest.spyOn(instance as any, 'clearElement'); + removeAnnotationSpy = jest.spyOn(instance as any, 'removeAnnotationPin'); + + instance['handleMutationObserverChanges']([change]); + + expect(clearElementSpy).not.toHaveBeenCalled(); + expect(renderAnnotationsSpy).toHaveBeenCalled(); + expect(setElementsSpy).toHaveBeenCalled(); + expect(removeAnnotationSpy).not.toHaveBeenCalled(); + }); }); describe('onToggleAnnotationSidebar', () => { @@ -1178,4 +1222,20 @@ describe('HTMLPinAdapter', () => { expect(element.style.position).toEqual('absolute'); }); }); + + describe('prepareElements', () => { + test('should not prepare element if data attribute value matches filter', () => { + document.body.innerHTML = + '
'; + const container = document.getElementById('container') as HTMLElement; + + instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + const spy = jest.spyOn(instance as any, 'setElementReadyToPin'); + + instance['container'] = container; + instance['prepareElements'](); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index f28a02f9..3bf7f98a 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -34,6 +34,7 @@ export class HTMLPin implements PinAdapter { private selectedPin: HTMLElement | null = null; private dataAttribute: string = 'data-superviz-id'; private animateFrame: number; + private dataAttributeNameFilters: RegExp[]; // Coordinates/Positions private mouseDownCoordinates: Simple2DPoint; @@ -81,7 +82,7 @@ export class HTMLPin implements PinAdapter { if (typeof options !== 'object') throw new Error('Second argument of the HTMLPin constructor must be an object'); - const { dataAttributeName } = options; + const { dataAttributeName, dataAttributeNameFilters } = options; if (dataAttributeName === '') throw new Error('dataAttributeName must be a non-empty string'); if (dataAttributeName === null) throw new Error('dataAttributeName cannot be null'); @@ -89,6 +90,8 @@ export class HTMLPin implements PinAdapter { throw new Error('dataAttributeName must be a non-empty string'); this.dataAttribute = dataAttributeName || this.dataAttribute; + this.dataAttributeNameFilters = dataAttributeNameFilters || []; + this.isActive = false; this.prepareElements(); @@ -222,6 +225,12 @@ export class HTMLPin implements PinAdapter { elementsWithDataId.forEach((el: HTMLElement) => { const id = el.getAttribute(this.dataAttribute); + const skip = this.dataAttributeNameFilters.some((filter, index) => { + return id.match(filter); + }); + + if (skip) return; + this.setElementReadyToPin(el, id); }); } @@ -789,9 +798,13 @@ export class HTMLPin implements PinAdapter { changes.forEach((change) => { const { target, oldValue } = change; const dataId = (target as HTMLElement).getAttribute(this.dataAttribute); - if ((!dataId && !oldValue) || dataId === oldValue) return; - const attributeRemoved = !dataId && oldValue; + + const oldValueSkipped = this.dataAttributeNameFilters.some((filter) => + oldValue.match(filter), + ); + + const attributeRemoved = !dataId && oldValue && !oldValueSkipped; if (attributeRemoved) { this.removeAnnotationPin('temporary-pin'); @@ -805,10 +818,14 @@ export class HTMLPin implements PinAdapter { return; } - if (oldValue && this.elementsWithDataId[oldValue]) { + const skip = this.dataAttributeNameFilters.some((filter) => dataId.match(filter)); + + if ((oldValue && this.elementsWithDataId[oldValue]) || skip) { this.clearElement(oldValue); } + if (skip) return; + this.setElementReadyToPin(target as HTMLElement, dataId); this.renderAnnotationsPins(); }); diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index 4fc2ac7a..c2712194 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -16,4 +16,5 @@ export type HorizontalSide = 'left' | 'right'; export interface HTMLPinOptions { dataAttributeName?: string; + dataAttributeNameFilters?: RegExp[]; } From b22519fc1747f3cd58c12ccb23a3bf13fcdd622f Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 9 Jan 2024 23:04:11 -0300 Subject: [PATCH 36/70] fix: do not show 'Click to Follow' if other participant or user is not in presence --- src/components/who-is-online/index.ts | 11 ++++++++--- .../who-is-online/components/dropdown.ts | 12 +++++++++++- .../who-is-online/components/types.ts | 1 + src/web-components/who-is-online/who-is-online.ts | 15 ++++++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index cdcfadcc..694b270a 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -133,7 +133,7 @@ export class WhoIsOnline extends BaseComponent { const { color } = this.realtime.getSlotColor(slotIndex); const isLocal = this.localParticipant.id === id; const joinedPresence = activeComponents.some((component) => component.includes('presence')); - this.setLocalData(isLocal, !joinedPresence, color); + this.setLocalData(isLocal, !joinedPresence, color, joinedPresence); return { name, id, slotIndex, color, isLocal, joinedPresence, avatar }; }); @@ -149,11 +149,16 @@ export class WhoIsOnline extends BaseComponent { this.element.updateParticipants(this.participants); }; - private setLocalData = (local: boolean, disable: boolean, color: string) => { + private setLocalData = ( + local: boolean, + disable: boolean, + color: string, + joinedPresence: boolean, + ) => { if (!local) return; this.element.disableDropdown = disable; - this.element.localParticipantData = { color, id: this.localParticipant.id }; + this.element.localParticipantData = { color, id: this.localParticipant.id, joinedPresence }; }; /** diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 5c66dbc3..712e43d7 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -30,6 +30,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { declare showSeeMoreTooltip: boolean; declare showParticipantTooltip: boolean; declare following: Following; + declare localParticipantJoinedPresence: boolean; static properties = { open: { type: Boolean }, @@ -41,6 +42,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { following: { type: Object }, showSeeMoreTooltip: { type: Boolean }, showParticipantTooltip: { type: Boolean }, + localParticipantJoinedPresence: { type: Boolean }, }; constructor() { @@ -161,6 +163,14 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { })) .slice(0, 2); + const tooltipData = { + name, + } as { name: string; action: string }; + + if (this.localParticipantJoinedPresence && joinedPresence) { + tooltipData.action = 'Click to Follow'; + } + return html`
+${excess}
@@ -319,6 +320,18 @@ export class WhoIsOnline extends WebComponentsBaseElement { const append = isLocal ? ' (you)' : ''; const participantName = name + append; + const tooltipData = { + name, + } as { name: string; action: string }; + + if (this.localParticipantData?.joinedPresence && joinedPresence && !isLocal) { + tooltipData.action = 'Click to Follow'; + } + + if (isLocal) { + tooltipData.action = 'You'; + } + return html`
${this.getAvatar(participant)} From c79e944e351fc6a0357ddf4ecba03e7dfa0e6ca8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 9 Jan 2024 23:06:50 -0300 Subject: [PATCH 37/70] fix: do not show presence mouse over wio --- src/web-components/who-is-online/css/who-is-online-style.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web-components/who-is-online/css/who-is-online-style.ts b/src/web-components/who-is-online/css/who-is-online-style.ts index 7cd5adce..4d74f251 100644 --- a/src/web-components/who-is-online/css/who-is-online-style.ts +++ b/src/web-components/who-is-online/css/who-is-online-style.ts @@ -12,6 +12,7 @@ export const whoIsOnlineStyle = css` display: flex; flex-direction: column; position: fixed; + z-index: 99; } .superviz-who-is-online__participant { From 49ddddf56f657dab861a66e25232b352b061cca3 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 10 Jan 2024 14:08:16 -0300 Subject: [PATCH 38/70] fix: base participant type --- src/core/launcher/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/launcher/types.ts b/src/core/launcher/types.ts index b40f2019..6052caf4 100644 --- a/src/core/launcher/types.ts +++ b/src/core/launcher/types.ts @@ -11,6 +11,6 @@ export interface LauncherFacade { subscribe: typeof Observable.prototype.subscribe; unsubscribe: typeof Observable.prototype.unsubscribe; destroy: () => void; - addComponent: (component: BaseComponent) => void; - removeComponent: (component: BaseComponent) => void; + addComponent: (component: Partial) => void; + removeComponent: (component: Partial) => void; } From 5bf8f14f2f9d2f431ae5e34815a73708cd373487 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 10 Jan 2024 14:09:06 -0300 Subject: [PATCH 39/70] ci: remove sonar from checks --- .github/workflows/checks.yml | 14 +------------- sonar-project.properties | 2 -- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 sonar-project.properties diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4be26a57..44047e1a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,18 +6,6 @@ on: - opened - synchronize jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} test-unit: runs-on: ubuntu-latest steps: @@ -32,7 +20,7 @@ jobs: touch .version.js && echo "echo \"export const version = 'test'\" > .version.js" | bash - - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - - name: Run tests run: yarn test:unit:ci --coverage - name: Code Coverage Report diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index f8830a18..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,2 +0,0 @@ -sonar.projectKey=superviz_sdk -sonar.organization=superviz \ No newline at end of file From 88fec025dac94a01aa9dcab07ab389ffb14bfbd9 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 10 Jan 2024 14:31:41 -0300 Subject: [PATCH 40/70] refactor: use types or interfaces instead of casting with 'as' --- .../who-is-online/components/dropdown.ts | 23 ++++++++++++------- .../who-is-online/components/types.ts | 9 ++++++++ .../who-is-online/who-is-online.ts | 6 ++--- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 712e43d7..46309f32 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -7,7 +7,14 @@ import { Participant } from '../../../components/who-is-online/types'; import { WebComponentsBase } from '../../base'; import { dropdownStyle } from '../css'; -import { Following, WIODropdownOptions, PositionOptions } from './types'; +import { + Following, + WIODropdownOptions, + PositionOptions, + TooltipData, + VerticalSide, + HorizontalSide, +} from './types'; const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, dropdownStyle]; @@ -17,12 +24,12 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { static styles = styles; declare open: boolean; - declare align: 'left' | 'right'; - declare position: 'top' | 'bottom'; + declare align: HorizontalSide; + declare position: VerticalSide; declare participants: Participant[]; private textColorValues: number[]; declare selected: string; - private originalPosition: 'top' | 'bottom'; + private originalPosition: VerticalSide; private menu: HTMLElement; private dropdownContent: HTMLElement; private host: HTMLElement; @@ -163,9 +170,9 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { })) .slice(0, 2); - const tooltipData = { + const tooltipData: TooltipData = { name, - } as { name: string; action: string }; + }; if (this.localParticipantJoinedPresence && joinedPresence) { tooltipData.action = 'Click to Follow'; @@ -320,13 +327,13 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { if (action === PositionOptions['USE-ORIGINAL']) { const originalVertical = this.originalPosition.split('-')[0]; - this.position = this.position.replace(/top|bottom/, originalVertical) as 'top' | 'bottom'; + this.position = this.position.replace(/top|bottom/, originalVertical) as VerticalSide; return; } const newSide = innerHeight - bottom > top ? 'bottom' : 'top'; const previousSide = this.position.split('-')[0]; - const newPosition = this.position.replace(previousSide, newSide) as 'top' | 'bottom'; + const newPosition = this.position.replace(previousSide, newSide) as VerticalSide; this.position = newPosition; }; diff --git a/src/web-components/who-is-online/components/types.ts b/src/web-components/who-is-online/components/types.ts index 0280ca8f..e902e91a 100644 --- a/src/web-components/who-is-online/components/types.ts +++ b/src/web-components/who-is-online/components/types.ts @@ -35,3 +35,12 @@ export interface LocalParticipantData { color: string; joinedPresence: boolean; } + +export interface TooltipData { + name: string; + action?: string; +} + +export type VerticalSide = 'top' | 'bottom'; + +export type HorizontalSide = 'left' | 'right'; diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index 033f80f0..b278faf5 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -7,7 +7,7 @@ import { RealtimeEvent } from '../../common/types/events.types'; import { Participant } from '../../components/who-is-online/types'; import { WebComponentsBase } from '../base'; -import type { LocalParticipantData } from './components/types'; +import type { LocalParticipantData, TooltipData } from './components/types'; import { Following, WIODropdownOptions } from './components/types'; import { whoIsOnlineStyle } from './css/index'; @@ -320,9 +320,9 @@ export class WhoIsOnline extends WebComponentsBaseElement { const append = isLocal ? ' (you)' : ''; const participantName = name + append; - const tooltipData = { + const tooltipData: TooltipData = { name, - } as { name: string; action: string }; + }; if (this.localParticipantData?.joinedPresence && joinedPresence && !isLocal) { tooltipData.action = 'Click to Follow'; From 9936ae010f41dbfd3afb38334862224c4890bdaf Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 10 Jan 2024 14:33:17 -0300 Subject: [PATCH 41/70] feat: rename option name in HTML Pin constructor --- .../comments/html-pin-adapter/index.test.ts | 6 +++--- src/components/comments/html-pin-adapter/index.ts | 12 ++++++------ src/components/comments/html-pin-adapter/types.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 7862f213..58de74c9 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -1047,7 +1047,7 @@ describe('HTMLPinAdapter', () => { test('should clear element then do nothing if new value is filtered', () => { document.body.innerHTML = '
'; - instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + instance = new HTMLPin('container', { dataAttributeValueFilters: [/.*-matches$/] }); const change = { target: document.body.querySelector('[data-superviz-id="1-matches"]') as HTMLElement, oldValue: '2', @@ -1069,7 +1069,7 @@ describe('HTMLPinAdapter', () => { test('should not clear element if old value was skipped', () => { document.body.innerHTML = '
'; - instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + instance = new HTMLPin('container', { dataAttributeValueFilters: [/.*-matches$/] }); const change = { target: document.body.querySelector('[data-superviz-id="does-not-match"]') as HTMLElement, oldValue: '1-matches', @@ -1229,7 +1229,7 @@ describe('HTMLPinAdapter', () => { '
'; const container = document.getElementById('container') as HTMLElement; - instance = new HTMLPin('container', { dataAttributeNameFilters: [/.*-matches$/] }); + instance = new HTMLPin('container', { dataAttributeValueFilters: [/.*-matches$/] }); const spy = jest.spyOn(instance as any, 'setElementReadyToPin'); instance['container'] = container; diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3bf7f98a..ba665b12 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -34,7 +34,7 @@ export class HTMLPin implements PinAdapter { private selectedPin: HTMLElement | null = null; private dataAttribute: string = 'data-superviz-id'; private animateFrame: number; - private dataAttributeNameFilters: RegExp[]; + private dataAttributeValueFilters: RegExp[]; // Coordinates/Positions private mouseDownCoordinates: Simple2DPoint; @@ -82,7 +82,7 @@ export class HTMLPin implements PinAdapter { if (typeof options !== 'object') throw new Error('Second argument of the HTMLPin constructor must be an object'); - const { dataAttributeName, dataAttributeNameFilters } = options; + const { dataAttributeName, dataAttributeValueFilters } = options; if (dataAttributeName === '') throw new Error('dataAttributeName must be a non-empty string'); if (dataAttributeName === null) throw new Error('dataAttributeName cannot be null'); @@ -90,7 +90,7 @@ export class HTMLPin implements PinAdapter { throw new Error('dataAttributeName must be a non-empty string'); this.dataAttribute = dataAttributeName || this.dataAttribute; - this.dataAttributeNameFilters = dataAttributeNameFilters || []; + this.dataAttributeValueFilters = dataAttributeValueFilters || []; this.isActive = false; this.prepareElements(); @@ -225,7 +225,7 @@ export class HTMLPin implements PinAdapter { elementsWithDataId.forEach((el: HTMLElement) => { const id = el.getAttribute(this.dataAttribute); - const skip = this.dataAttributeNameFilters.some((filter, index) => { + const skip = this.dataAttributeValueFilters.some((filter, index) => { return id.match(filter); }); @@ -800,7 +800,7 @@ export class HTMLPin implements PinAdapter { const dataId = (target as HTMLElement).getAttribute(this.dataAttribute); if ((!dataId && !oldValue) || dataId === oldValue) return; - const oldValueSkipped = this.dataAttributeNameFilters.some((filter) => + const oldValueSkipped = this.dataAttributeValueFilters.some((filter) => oldValue.match(filter), ); @@ -818,7 +818,7 @@ export class HTMLPin implements PinAdapter { return; } - const skip = this.dataAttributeNameFilters.some((filter) => dataId.match(filter)); + const skip = this.dataAttributeValueFilters.some((filter) => dataId.match(filter)); if ((oldValue && this.elementsWithDataId[oldValue]) || skip) { this.clearElement(oldValue); diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index c2712194..70694d3d 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -16,5 +16,5 @@ export type HorizontalSide = 'left' | 'right'; export interface HTMLPinOptions { dataAttributeName?: string; - dataAttributeNameFilters?: RegExp[]; + dataAttributeValueFilters?: RegExp[]; } From fc83b197adde56941b1176c90963b7a828990bf6 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 10 Jan 2024 22:35:10 -0300 Subject: [PATCH 42/70] fix: use correct pin url --- src/components/comments/html-pin-adapter/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index ba665b12..e125c9bd 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -243,7 +243,7 @@ export class HTMLPin implements PinAdapter { private setAddCursor(): void { Object.keys(this.elementsWithDataId).forEach((id) => { this.divWrappers.get(id).style.cursor = - 'url("https://production.cdn.superviz.com/static/pin-add.png") 0 100, pointer'; + 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer'; this.divWrappers.get(id).style.pointerEvents = 'auto'; }); } @@ -526,7 +526,7 @@ export class HTMLPin implements PinAdapter { if (!this.isActive || !this.isPinsVisible) return; this.divWrappers.get(id).style.cursor = - 'url("https://production.cdn.superviz.com/static/pin-add.png") 0 100, pointer'; + 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer'; this.divWrappers.get(id).style.pointerEvents = 'auto'; this.addElementListeners(id); } From f4c7d36905e36b2c800984d8b047277d19b8bf7e Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 00:51:17 -0300 Subject: [PATCH 43/70] fix: do not hide pins that go beyond element rect with overflow: hidden --- .../comments/html-pin-adapter/index.test.ts | 53 +++-------------- .../comments/html-pin-adapter/index.ts | 59 +++---------------- 2 files changed, 17 insertions(+), 95 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 58de74c9..08ba0a44 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -284,10 +284,7 @@ describe('HTMLPinAdapter', () => { }, ]; - instance['divWrappers'] - .get('1') - ?.querySelector('[data-pins-wrapper]')! - .removeAttribute('data-pins-wrapper'); + instance['divWrappers'].delete('1'); instance['pins'].clear(); instance['renderAnnotationsPins'](); @@ -708,7 +705,7 @@ describe('HTMLPinAdapter', () => { jest.restoreAllMocks(); }); - test('should remove previous temporary pin container when rendering temporary pin over another element', () => { + test('should remove previous temporary pin when rendering temporary pin over another element', () => { instance['onClick']({ clientX: 100, clientY: 100, @@ -721,7 +718,6 @@ describe('HTMLPinAdapter', () => { currentTarget.getAttribute('data-wrapper-id'), ); - const removeSpy = jest.spyOn(instance['temporaryPinContainer'] as any, 'remove'); const deleteSpy = jest.spyOn(instance['pins'], 'delete'); instance['onClick']({ @@ -731,7 +727,6 @@ describe('HTMLPinAdapter', () => { currentTarget: document.body.querySelector('[data-wrapper-id="2"]') as HTMLElement, } as unknown as MouseEvent); - expect(removeSpy).toHaveBeenCalled(); expect(deleteSpy).toHaveBeenCalled(); expect(instance['pins'].has('temporary-pin')).toBeTruthy(); expect(instance['temporaryPinCoordinates'].elementId).toBe('2'); @@ -807,54 +802,33 @@ describe('HTMLPinAdapter', () => { test('should add temporary pin to element', () => { const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - const temporaryPinContainer = document.createElement('div'); - temporaryPinContainer.id = 'temp-container'; const pin = document.createElement('div'); pin.id = 'temp-pin'; - const spy = jest - .spyOn(instance as any, 'createTemporaryPinContainer') - .mockReturnValue(temporaryPinContainer); instance['addTemporaryPinToElement']('1', pin); - expect(spy).toHaveBeenCalled(); - expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(pin); - expect(instance['divWrappers'].get('1')?.querySelector('#temp-container')).toBe( - temporaryPinContainer, - ); + const wrapper = instance['divWrappers'].get('1') as HTMLElement; + expect(element.firstElementChild).toBe(wrapper); + expect(wrapper.firstElementChild).toBe(pin); }); test('should not add temporary pin to element if element is not found', () => { - const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - const temporaryPinContainer = document.createElement('div'); - temporaryPinContainer.id = 'temp-container'; const pin = document.createElement('div'); pin.id = 'temp-pin'; - const spy = jest - .spyOn(instance as any, 'createTemporaryPinContainer') - .mockReturnValue(temporaryPinContainer); instance['addTemporaryPinToElement']('not-found', pin); - expect(spy).not.toHaveBeenCalled(); - expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(null); - expect(instance['divWrappers'].get('1')?.querySelector('#temp-container')).toBe(null); + expect(instance['divWrappers'].get('1')?.querySelector('#temp-pin')).toBe(null); }); test('should not add temporary pin to element if wrapper is not found', () => { - const element = document.body.querySelector('[data-superviz-id="1"]') as HTMLElement; - const temporaryPinContainer = document.createElement('div'); - temporaryPinContainer.id = 'temp-container'; const pin = document.createElement('div'); pin.id = 'temp-pin'; - const spy = jest - .spyOn(instance as any, 'createTemporaryPinContainer') - .mockReturnValue(temporaryPinContainer); + instance['divWrappers'].delete('1'); instance['addTemporaryPinToElement']('1', pin); - expect(spy).not.toHaveBeenCalled(); - expect(temporaryPinContainer.querySelector('#temp-pin')).toBe(null); + expect(document.getElementById('temp-pin')).toBe(null); }); }); @@ -865,9 +839,6 @@ describe('HTMLPinAdapter', () => { instance['divWrappers'].clear(); const wrapper = instance['createWrapper'](element, '1'); - const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLElement; - const containerRect = element.getBoundingClientRect(); - expect(wrapper).toBeInstanceOf(HTMLDivElement); expect(wrapper.style.position).toEqual('absolute'); expect(wrapper.style.top).toEqual('0px'); @@ -878,14 +849,6 @@ describe('HTMLPinAdapter', () => { expect(wrapper.style.cursor).toEqual('default'); expect(wrapper.getAttribute('data-wrapper-id')).toEqual('1'); expect(wrapper.id).toEqual('superviz-id-1'); - - expect(pinsWrapper).toBeInstanceOf(HTMLDivElement); - expect(pinsWrapper.style.position).toEqual('absolute'); - expect(pinsWrapper.style.overflow).toEqual('hidden'); - expect(pinsWrapper.style.top).toEqual('0px'); - expect(pinsWrapper.style.left).toEqual('0px'); - expect(pinsWrapper.style.width).toEqual('100%'); - expect(pinsWrapper.style.height).toEqual('100%'); }); test('should not create a new wrapper if wrapper already exists', () => { diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index e125c9bd..e4f85574 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -360,8 +360,9 @@ export class HTMLPin implements PinAdapter { let temporaryPin = this.pins.get('temporary-pin'); if (elementId && elementId !== this.temporaryPinCoordinates.elementId) { - this.temporaryPinContainer.remove(); + this.pins.get('temporary-pin').remove(); this.pins.delete('temporary-pin'); + this.temporaryPinCoordinates.elementId = elementId; temporaryPin = null; } @@ -411,18 +412,16 @@ export class HTMLPin implements PinAdapter { const { x, y, elementId, type } = JSON.parse(annotation.position) as PinCoordinates; if (type !== 'html') return; + if (this.pins.has(annotation.uuid)) { + return; + } + const element = this.elementsWithDataId[elementId]; if (!element) return; - const wrapper = this.divWrappers - .get(elementId) - ?.querySelector('[data-pins-wrapper]') as HTMLDivElement; + const wrapper = this.divWrappers.get(elementId); if (!wrapper) return; - if (this.pins.has(annotation.uuid)) { - return; - } - const pinElement = this.createPin(annotation, x, y); wrapper.appendChild(pinElement); this.pins.set(annotation.uuid, pinElement); @@ -460,8 +459,7 @@ export class HTMLPin implements PinAdapter { const wrapper = this.divWrappers.get(id); if (wrapper) { - const pinsWrapper = wrapper.querySelector('[data-pins-wrapper]') as HTMLDivElement; - const pins = pinsWrapper.children; + const pins = wrapper.children; const { length } = pins; for (let i = 0; i < length; ++i) { @@ -545,7 +543,6 @@ export class HTMLPin implements PinAdapter { if (!this.temporaryPinCoordinates.elementId) return; this.removeAnnotationPin('temporary-pin'); - this.temporaryPinContainer?.remove(); this.temporaryPinCoordinates = {}; }; @@ -597,29 +594,10 @@ export class HTMLPin implements PinAdapter { const wrapper = this.divWrappers.get(elementId); if (!wrapper) return; - const temporaryContainer = this.createTemporaryPinContainer(); - temporaryContainer.appendChild(pin); - - wrapper.appendChild(temporaryContainer); + wrapper.appendChild(pin); } // ------- helper functions ------- - /** - * @function createTemporaryPinContainer - * @description return a temporary pin container - * @returns {HTMLDivElement} the temporary pin container, separated from the main pins container to avoid overflow issues - */ - private createTemporaryPinContainer(): HTMLDivElement { - const temporaryContainer = document.createElement('div'); - temporaryContainer.style.position = 'absolute'; - temporaryContainer.style.top = '0'; - temporaryContainer.style.left = '0'; - temporaryContainer.style.width = '100%'; - temporaryContainer.style.height = '100%'; - temporaryContainer.id = 'temporary-pin-container'; - return temporaryContainer; - } - /** * @function createPin * @description creates a pin element and sets its properties @@ -666,16 +644,6 @@ export class HTMLPin implements PinAdapter { containerWrapper.style.width = `100%`; containerWrapper.style.height = `100%`; - const pinsWrapper = document.createElement('div'); - pinsWrapper.setAttribute('data-pins-wrapper', ''); - pinsWrapper.style.position = 'absolute'; - pinsWrapper.style.overflow = 'hidden'; - pinsWrapper.style.top = '0'; - pinsWrapper.style.left = '0'; - pinsWrapper.style.width = '100%'; - pinsWrapper.style.height = '100%'; - containerWrapper.appendChild(pinsWrapper); - if (!this.VOID_ELEMENTS.includes(this.elementsWithDataId[id].tagName.toLowerCase())) { this.elementsWithDataId[id].appendChild(containerWrapper); this.setPositionNotStatic(this.elementsWithDataId[id]); @@ -717,15 +685,6 @@ export class HTMLPin implements PinAdapter { element.style.setProperty('position', 'relative'); } - /** - * @function temporaryPinContainer - * @description returns the temporary pin container - * @returns {HTMLDivElement} the temporary pin container - */ - private get temporaryPinContainer(): HTMLDivElement { - return document.getElementById('temporary-pin-container') as HTMLDivElement; - } - // ------- callbacks ------- /** * @function onClick From d7ba44a78bb5b0091c27d4a49bd4b1a83d8626ed Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 08:14:19 -0300 Subject: [PATCH 44/70] fix: prevent error being thrown when destroying html pin --- src/components/comments/html-pin-adapter/index.test.ts | 3 --- src/components/comments/html-pin-adapter/index.ts | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 08ba0a44..6b2f9276 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -289,9 +289,6 @@ describe('HTMLPinAdapter', () => { instance['renderAnnotationsPins'](); - // // delete this avoids an error being throw when the pin is destroyed - // delete instance['elementsWithDataId']['1']; - expect(instance['pins'].size).toEqual(0); }); diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index e4f85574..22f46e11 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -133,9 +133,10 @@ export class HTMLPin implements PinAdapter { this.voidElementsWrappers.clear(); this.voidElementsWrappers = undefined; this.annotations = []; - cancelAnimationFrame(this.animateFrame); document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); + + cancelAnimationFrame(this.animateFrame); } /** @@ -187,8 +188,10 @@ export class HTMLPin implements PinAdapter { * @returns {void} */ private animate = (): void => { - this.updatePinsPositions(); - requestAnimationFrame(this.animate); + if (this.voidElementsWrappers) { + this.updatePinsPositions(); + this.animateFrame = requestAnimationFrame(this.animate); + } }; /** From 5ecbf1bafdf19b96c4c9a7234b6fbadfa61e733b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 10:58:13 -0300 Subject: [PATCH 45/70] feat: calculate pin position in html based on the ratio of where it was positioned --- .../comments/html-pin-adapter/index.ts | 17 +++++++++---- .../comments/components/annotation-pin.ts | 24 +++++++++---------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 22f46e11..ef67e0fe 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -383,11 +383,13 @@ export class HTMLPin implements PinAdapter { temporaryPin.setAttribute('annotation', JSON.stringify({})); temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); + temporaryPin.setAttribute('keepPositionRatio', ''); temporaryPin.setAttributeNode(document.createAttribute('active')); this.addTemporaryPinToElement(elementId, temporaryPin); } + const { width, height } = this.divWrappers.get(elementId).getBoundingClientRect(); const { x, y } = this.temporaryPinCoordinates; temporaryPin.setAttribute('position', JSON.stringify({ x, y })); @@ -425,6 +427,8 @@ export class HTMLPin implements PinAdapter { const wrapper = this.divWrappers.get(elementId); if (!wrapper) return; + const { width, height } = wrapper.getBoundingClientRect(); + console.error(x, width, y, height); const pinElement = this.createPin(annotation, x, y); wrapper.appendChild(pinElement); this.pins.set(annotation.uuid, pinElement); @@ -614,6 +618,7 @@ export class HTMLPin implements PinAdapter { pinElement.setAttribute('type', PinMode.SHOW); pinElement.setAttribute('annotation', JSON.stringify(annotation)); pinElement.setAttribute('position', JSON.stringify({ x, y })); + pinElement.setAttribute('keepPositionRatio', ''); pinElement.id = annotation.uuid; return pinElement; @@ -710,8 +715,8 @@ export class HTMLPin implements PinAdapter { const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; const scale = wrapper.getBoundingClientRect().width / wrapper.offsetWidth || 1; - const x = (event.clientX - rect.left) / scale; - const y = (event.clientY - rect.top) / scale; + let x = (event.clientX - rect.left) / scale; + let y = (event.clientY - rect.top) / scale; const originalX = (mouseDownX - rect.x) / scale; const originalY = (mouseDownY - rect.y) / scale; @@ -719,15 +724,19 @@ export class HTMLPin implements PinAdapter { const distance = Math.hypot(x - originalX, y - originalY); if (distance > 10) return; + const { width, height } = wrapper.getBoundingClientRect(); + + x /= width; + y = (y - 32) / height; this.onPinFixedObserver.publish({ x, - y: y - 30, + y, type: 'html', elementId, } as PinCoordinates); this.resetSelectedPin(); - this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y: y - 30 }; + this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; this.renderTemporaryPin(elementId); const temporaryPin = this.divWrappers.get(elementId).querySelector('#superviz-temporary-pin'); diff --git a/src/web-components/comments/components/annotation-pin.ts b/src/web-components/comments/components/annotation-pin.ts index 4c7ef1ec..6daead38 100644 --- a/src/web-components/comments/components/annotation-pin.ts +++ b/src/web-components/comments/components/annotation-pin.ts @@ -26,6 +26,7 @@ export class CommentsAnnotationPin extends WebComponentsBaseElement { declare localAvatar: string | undefined; declare annotationSent: boolean; declare localName: string; + declare keepPositionRatio: boolean; private originalPosition: Partial; private annotationSides: Sides; @@ -46,6 +47,7 @@ export class CommentsAnnotationPin extends WebComponentsBaseElement { localAvatar: { type: String }, annotationSent: { type: Boolean }, localName: { type: String }, + keepPositionRatio: { type: Boolean }, }; constructor() { @@ -221,25 +223,21 @@ export class CommentsAnnotationPin extends WebComponentsBaseElement { }; classes[this.horizontalSide] = true; + let style = ''; + if (this.keepPositionRatio) { + style = `top: ${this.position.y * 100}%; left: ${this.position.x * 100}%;`; + } else { + style = `top: ${this.position.y * 100}px; left: ${this.position.x * 100}px;`; + } + if (this.type === PinMode.ADD) { return html` -
- ${this.avatar()} ${this.input()} -
+
${this.avatar()} ${this.input()}
`; } return html` -
- ${this.avatar()} -
+
${this.avatar()}
`; } } From e7f96310759cde0388651955459759f1ff81349d Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 11:11:33 -0300 Subject: [PATCH 46/70] fix: remove console --- src/components/comments/html-pin-adapter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index ef67e0fe..619cbd20 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -428,7 +428,7 @@ export class HTMLPin implements PinAdapter { if (!wrapper) return; const { width, height } = wrapper.getBoundingClientRect(); - console.error(x, width, y, height); + const pinElement = this.createPin(annotation, x, y); wrapper.appendChild(pinElement); this.pins.set(annotation.uuid, pinElement); From 4bee8e795acb4cda4120eeb103403817c2cea519 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 13:09:52 -0300 Subject: [PATCH 47/70] fix: stop multiplying pin position by 100 in types other than html --- src/components/comments/html-pin-adapter/index.ts | 6 +++--- src/web-components/comments/components/annotation-pin.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 619cbd20..9ca6b66d 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -389,7 +389,6 @@ export class HTMLPin implements PinAdapter { this.addTemporaryPinToElement(elementId, temporaryPin); } - const { width, height } = this.divWrappers.get(elementId).getBoundingClientRect(); const { x, y } = this.temporaryPinCoordinates; temporaryPin.setAttribute('position', JSON.stringify({ x, y })); @@ -726,8 +725,9 @@ export class HTMLPin implements PinAdapter { const { width, height } = wrapper.getBoundingClientRect(); - x /= width; - y = (y - 32) / height; + // save coordinates as percentages + x = (x * 100) / width; + y = ((y - 32) * 100) / height; this.onPinFixedObserver.publish({ x, y, diff --git a/src/web-components/comments/components/annotation-pin.ts b/src/web-components/comments/components/annotation-pin.ts index 6daead38..1b324752 100644 --- a/src/web-components/comments/components/annotation-pin.ts +++ b/src/web-components/comments/components/annotation-pin.ts @@ -225,9 +225,9 @@ export class CommentsAnnotationPin extends WebComponentsBaseElement { let style = ''; if (this.keepPositionRatio) { - style = `top: ${this.position.y * 100}%; left: ${this.position.x * 100}%;`; + style = `top: ${this.position.y}%; left: ${this.position.x}%;`; } else { - style = `top: ${this.position.y * 100}px; left: ${this.position.x * 100}px;`; + style = `top: ${this.position.y}px; left: ${this.position.x}px;`; } if (this.type === PinMode.ADD) { From 825676b238f6123509a9fe62fc6ea4210a7edeb5 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 11 Jan 2024 14:41:24 -0300 Subject: [PATCH 48/70] fix: correctly position pin if transform: scale() was applied on element --- src/components/comments/html-pin-adapter/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 9ca6b66d..5240bdb5 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -712,22 +712,25 @@ export class HTMLPin implements PinAdapter { const elementId = wrapper.getAttribute('data-wrapper-id'); const rect = wrapper.getBoundingClientRect(); const { x: mouseDownX, y: mouseDownY } = this.mouseDownCoordinates; - const scale = wrapper.getBoundingClientRect().width / wrapper.offsetWidth || 1; - let x = (event.clientX - rect.left) / scale; - let y = (event.clientY - rect.top) / scale; + let x = event.clientX - rect.left; + let y = event.clientY - rect.top; - const originalX = (mouseDownX - rect.x) / scale; - const originalY = (mouseDownY - rect.y) / scale; + const originalX = mouseDownX - rect.x; + const originalY = mouseDownY - rect.y; const distance = Math.hypot(x - originalX, y - originalY); if (distance > 10) return; const { width, height } = wrapper.getBoundingClientRect(); + const cursorHeight = 32; + const scale = wrapper.getBoundingClientRect().width / wrapper.offsetWidth || 1; + // save coordinates as percentages x = (x * 100) / width; - y = ((y - 32) * 100) / height; + y = ((y - cursorHeight * scale) * 100) / height; + this.onPinFixedObserver.publish({ x, y, From 45ccda2fab40c1973b518d5659deddc3d95f17e2 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 12 Jan 2024 10:15:40 -0300 Subject: [PATCH 49/70] feat: render add cursor through css implementation instead of html element --- .../comments/canvas-pin-adapter/index.ts | 77 ++----------------- 1 file changed, 6 insertions(+), 71 deletions(-) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index 6a6528ff..c16788eb 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -9,7 +9,6 @@ export class CanvasPin implements PinAdapter { private canvas: HTMLCanvasElement; private canvasSides: CanvasSides; private divWrapper: HTMLElement; - private mouseElement: HTMLElement; private isActive: boolean; private isPinsVisible: boolean = true; private annotations: Annotation[]; @@ -23,6 +22,7 @@ export class CanvasPin implements PinAdapter { private commentsSide: 'left' | 'right' = 'left'; private movedTemporaryPin: boolean; private localParticipant: SimpleParticipant = {}; + private originalCanvasCursor: string; constructor( canvasId: string, @@ -59,7 +59,6 @@ export class CanvasPin implements PinAdapter { public destroy(): void { this.removeListeners(); this.removeAnnotationsPins(); - this.mouseElement = null; this.pins = new Map(); this.divWrapper.remove(); this.onPinFixedObserver.destroy(); @@ -88,14 +87,18 @@ export class CanvasPin implements PinAdapter { */ public setActive(isOpen: boolean): void { this.isActive = isOpen; - this.canvas.style.cursor = isOpen ? 'none' : 'default'; + // this.canvas.style.cursor = isOpen ? 'none' : 'default'; if (this.isActive) { + this.originalCanvasCursor = this.canvas.style.cursor; + this.canvas.style.cursor = + 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer'; this.addListeners(); return; } this.removeListeners(); + this.canvas.style.cursor = this.originalCanvasCursor; } /** @@ -179,9 +182,6 @@ export class CanvasPin implements PinAdapter { private addListeners(): void { this.canvas.addEventListener('click', this.onClick); this.canvas.addEventListener('mousedown', this.setMouseDownCoordinates); - this.canvas.addEventListener('mousemove', this.onMouseMove); - this.canvas.addEventListener('mouseout', this.onMouseLeave); - this.canvas.addEventListener('mouseenter', this.onMouseEnter); document.body.addEventListener('keyup', this.resetPins); document.body.addEventListener('select-annotation', this.annotationSelected); document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); @@ -201,31 +201,11 @@ export class CanvasPin implements PinAdapter { private removeListeners(): void { this.canvas.removeEventListener('click', this.onClick); this.canvas.removeEventListener('mousedown', this.setMouseDownCoordinates); - this.canvas.removeEventListener('mousemove', this.onMouseMove); - this.canvas.removeEventListener('mouseout', this.onMouseLeave); - this.canvas.removeEventListener('mouseenter', this.onMouseEnter); document.body.removeEventListener('keyup', this.resetPins); document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); } - /** - * @function createMouseElement - * @description Creates a new mouse element for the canvas pin adapter. - * @returns {HTMLElement} The newly created mouse element. - */ - private createMouseElement(): HTMLElement { - const mouseElement = document.createElement('superviz-comments-annotation-pin'); - mouseElement.setAttribute('type', PinMode.ADD); - mouseElement.setAttribute('annotation', JSON.stringify({})); - mouseElement.setAttribute('position', JSON.stringify({ x: 0, y: 0 })); - document.body.appendChild(mouseElement); - - this.canvas.style.cursor = 'none'; - - return mouseElement; - } - /** * @function resetSelectedPin * @description Unselects a pin by removing its 'active' attribute @@ -483,51 +463,6 @@ export class CanvasPin implements PinAdapter { document.body.dispatchEvent(new CustomEvent('unselect-annotation')); }; - /** - * @function onMouseMove - * @description handles the mouse move event on the canvas. - * @param event - The mouse event object. - * @returns {void} - */ - private onMouseMove = (event: MouseEvent): void => { - const { x, y } = event; - - if (!this.mouseElement) { - this.mouseElement = this.createMouseElement(); - } - - this.mouseElement.setAttribute('position', JSON.stringify({ x, y })); - }; - - /** - * @function onMouseLeave - * @description - Removes the mouse element and sets the canvas cursor - to default when the mouse leaves the canvas. - * @returns {void} - */ - private onMouseLeave = (): void => { - if (this.mouseElement) { - this.mouseElement.remove(); - this.mouseElement = null; - } - - this.canvas.style.cursor = 'default'; - }; - - /** - * @function onMouseEnter - * @description - Handles the mouse enter event for the canvas pin adapter. - If there is no mouse element, creates one. - * @returns {void} - */ - private onMouseEnter = (): void => { - if (this.mouseElement) return; - - this.mouseElement = this.createMouseElement(); - }; - /** * @function onToggleAnnotationSidebar * @description Removes temporary pin and unselects selected pin From 68f395e309326309dad9d9b8f6dbec73cd4fc45a Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 12 Jan 2024 10:20:43 -0300 Subject: [PATCH 50/70] fix: remove previous mouseElement tests --- .../comments/canvas-pin-adapter/index.test.ts | 44 +++---------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/components/comments/canvas-pin-adapter/index.test.ts b/src/components/comments/canvas-pin-adapter/index.test.ts index 5283ff57..003299e0 100644 --- a/src/components/comments/canvas-pin-adapter/index.test.ts +++ b/src/components/comments/canvas-pin-adapter/index.test.ts @@ -183,6 +183,9 @@ describe('CanvasPinAdapter', () => { const canvasPinAdapter = new CanvasPin('canvas'); canvasPinAdapter.setActive(true); expect(canvasPinAdapter).toBeInstanceOf(CanvasPin); + expect(canvasPinAdapter['canvas'].style.cursor).toBe( + 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer', + ); }); test('should throw an error if no canvas element is found', () => { @@ -194,7 +197,7 @@ describe('CanvasPinAdapter', () => { test('should add event listeners to the canvas element', () => { const addEventListenerSpy = jest.spyOn(instance['canvas'], 'addEventListener'); instance['addListeners'](); - expect(addEventListenerSpy).toHaveBeenCalledTimes(5); + expect(addEventListenerSpy).toHaveBeenCalledTimes(2); }); test('should destroy the canvas pin adapter', () => { @@ -202,29 +205,7 @@ describe('CanvasPinAdapter', () => { instance.destroy(); - expect(instance['mouseElement']).toBeNull(); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(5); - }); - - test('when mouse enters canvas, should create a new mouse element', () => { - const canvasPinAdapter = new CanvasPin('canvas'); - canvasPinAdapter.setActive(true); - const mock = jest.fn().mockImplementation(() => document.createElement('div')); - canvasPinAdapter['createMouseElement'] = mock; - - canvasPinAdapter['canvas'].dispatchEvent(new MouseEvent('mouseenter')); - - expect(canvasPinAdapter['createMouseElement']).toHaveBeenCalledTimes(1); - }); - - test('when mouse leaves canvas, should remove the mouse element', () => { - const canvasPinAdapter = new CanvasPin('canvas'); - canvasPinAdapter.setActive(true); - - canvasPinAdapter['canvas'].dispatchEvent(new MouseEvent('mouseenter')); - canvasPinAdapter['canvas'].dispatchEvent(new MouseEvent('mouseout')); - - expect(canvasPinAdapter['mouseElement']).toBeNull(); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(2); }); test('should create temporary pin when mouse clicks canvas', () => { @@ -358,21 +339,6 @@ describe('CanvasPinAdapter', () => { expect(instance['pins'].size).toEqual(0); }); - test('should update the position of the mouse element', () => { - const event = new MouseEvent('mousemove', { clientX: 100, clientY: 200 }); - const customEvent = { - ...event, - x: event.clientX, - y: event.clientY, - }; - - instance['onMouseMove'](customEvent); - - const element = instance['mouseElement']; - expect(element).toBeDefined(); - expect(element.getAttribute('position')).toBe(JSON.stringify({ x: 100, y: 200 })); - }); - test('should update mouse coordinates on mousedown event', () => { const event = new MouseEvent('mousedown', { clientX: 100, clientY: 200 }); instance['setMouseDownCoordinates'] = jest From 040b01621fbdb282c2ef2359034d29a5ad42adf2 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 12 Jan 2024 10:53:34 -0300 Subject: [PATCH 51/70] feat: hide temporary pin and unselect pins when toggling sidebar --- src/components/comments/canvas-pin-adapter/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index c16788eb..68dac7cc 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -97,6 +97,7 @@ export class CanvasPin implements PinAdapter { return; } + this.resetPins(); this.removeListeners(); this.canvas.style.cursor = this.originalCanvasCursor; } From 8c96dea42dda006694d8c883eadd47813af7c5a5 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Sun, 14 Jan 2024 11:27:27 -0300 Subject: [PATCH 52/70] feat: add flag to show/hide audience list --- src/components/video/index.ts | 2 +- src/components/video/types.ts | 1 + src/services/realtime/ably/index.ts | 1 - src/services/video-conference-manager/index.test.ts | 1 + src/services/video-conference-manager/index.ts | 2 ++ src/services/video-conference-manager/types.ts | 2 ++ 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 0df54e87..9e76ebb8 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -168,13 +168,13 @@ export class VideoConference extends BaseComponent { /** * @function startVideo * @description start video manager - * @param {VideoManagerOptions} options - video manager params * @returns {void} */ private startVideo = (): void => { this.videoConfig = { language: this.params?.language, canUseTranscription: this.params?.transcriptOff === false, + canShowAudienceList: this.params?.showAudienceList ?? true, canUseChat: !this.params?.chatOff, canUseCams: !this.params?.camsOff, canUseScreenshare: !this.params?.screenshareOff, diff --git a/src/components/video/types.ts b/src/components/video/types.ts index 8788b42b..ce7ff758 100644 --- a/src/components/video/types.ts +++ b/src/components/video/types.ts @@ -9,6 +9,7 @@ import { } from '../../services/video-conference-manager/types'; export interface VideoComponentOptions { + showAudienceList?: boolean; camsOff?: boolean; screenshareOff?: boolean; chatOff?: boolean; diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index a60e279e..34fe749e 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -46,7 +46,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably private presence3DChannel: Ably.Types.RealtimeChannelCallbacks = null; private clientRoomState: Record = {}; private clientSyncPropertiesQueue: Record = {}; - // private clientSyncPropertiesTimeOut: ReturnType = null; private isReconnecting: boolean = false; diff --git a/src/services/video-conference-manager/index.test.ts b/src/services/video-conference-manager/index.test.ts index 82665204..3d017eca 100644 --- a/src/services/video-conference-manager/index.test.ts +++ b/src/services/video-conference-manager/index.test.ts @@ -20,6 +20,7 @@ const createVideoConfrenceManager = (options?: VideoManagerOptions) => { browserService: new BrowserService(), camerasPosition: CamerasPosition.RIGHT, canUseTranscription: true, + canShowAudienceList: true, canUseCams: true, canUseChat: true, canUseScreenshare: true, diff --git a/src/services/video-conference-manager/index.ts b/src/services/video-conference-manager/index.ts index 68ecd596..b848a913 100644 --- a/src/services/video-conference-manager/index.ts +++ b/src/services/video-conference-manager/index.ts @@ -74,6 +74,7 @@ export default class VideoConfereceManager { canUseFollow, canUseGoTo, canUseGather, + canShowAudienceList, canUseDefaultToolbar, canUseTranscription, browserService, @@ -112,6 +113,7 @@ export default class VideoConfereceManager { canUseScreenshare, canUseDefaultAvatars, canUseTranscription, + canShowAudienceList, camerasPosition: positions.camerasPosition ?? CamerasPosition.RIGHT, canUseDefaultToolbar, devices: { diff --git a/src/services/video-conference-manager/types.ts b/src/services/video-conference-manager/types.ts index 965186a1..865368da 100644 --- a/src/services/video-conference-manager/types.ts +++ b/src/services/video-conference-manager/types.ts @@ -8,6 +8,7 @@ export interface VideoManagerOptions { language?: string; canUseChat: boolean; canUseCams: boolean; + canShowAudienceList: boolean; canUseTranscription: boolean; canUseScreenshare: boolean; canUseDefaultAvatars: boolean; @@ -57,6 +58,7 @@ export interface FrameConfig { roomId: string; debug: boolean; limits: ComponentLimits; + canShowAudienceList: boolean; canUseChat: boolean; canUseCams: boolean; canUseScreenshare: boolean; From efbfec18f6b38e36fad28863c302a51659202ebf Mon Sep 17 00:00:00 2001 From: Vinicius Date: Mon, 15 Jan 2024 11:36:38 -0300 Subject: [PATCH 53/70] fix: update comments position and fix creating first comment --- .../comments/canvas-pin-adapter/index.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index 68dac7cc..1910b4dd 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -241,7 +241,7 @@ export class CanvasPin implements PinAdapter { private animate = (): void => { if (this.isActive || this.isPinsVisible) { this.renderAnnotationsPins(); - this.renderDivWrapper(); + this.divWrapper = this.renderDivWrapper(); } if (this.temporaryPinCoordinates) { @@ -258,24 +258,30 @@ export class CanvasPin implements PinAdapter { * */ private renderDivWrapper(): HTMLElement { const canvasRect = this.canvas.getBoundingClientRect(); - const divWrapper = document.createElement('div'); - divWrapper.id = 'superviz-canvas-wrapper'; + let wrapper = this.divWrapper; + + if (!wrapper) { + wrapper = document.createElement('div') + wrapper.id = 'superviz-canvas-wrapper'; + if (['', 'static'].includes(this.canvas.parentElement.style.position)) { + this.canvas.parentElement.style.position = 'relative'; + }; + } - this.canvas.parentElement.style.position = 'relative'; - divWrapper.style.position = 'fixed'; - divWrapper.style.top = `${canvasRect.top}px`; - divWrapper.style.left = `${canvasRect.left}px`; - divWrapper.style.width = `${canvasRect.width}px`; - divWrapper.style.height = `${canvasRect.height}px`; - divWrapper.style.pointerEvents = 'none'; - divWrapper.style.overflow = 'hidden'; + wrapper.style.position = 'absolute'; + wrapper.style.top = `${this.canvas.offsetTop}px`; + wrapper.style.left = `${this.canvas.offsetLeft}px`; + wrapper.style.width = `${canvasRect.width}px`; + wrapper.style.height = `${canvasRect.height}px`; + wrapper.style.pointerEvents = 'none'; + wrapper.style.overflow = 'hidden'; if (!document.getElementById('superviz-canvas-wrapper')) { - this.canvas.parentElement.appendChild(divWrapper); + this.canvas.parentElement.appendChild(wrapper); } - return divWrapper; + return wrapper; } /** @@ -284,7 +290,7 @@ export class CanvasPin implements PinAdapter { * @returns {void} */ private renderAnnotationsPins(): void { - if (!this.annotations.length || this.canvas.style.display === 'none') { + if ((!this.annotations.length || this.canvas.style.display === 'none') && !this.pins.get('temporary-pin')) { this.removeAnnotationsPins(); return; } From 18e04e1195788e4cbc317fcd02d710680003bbf0 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 15 Jan 2024 14:36:34 -0300 Subject: [PATCH 54/70] feat: notify main channel when the participant data is updated --- src/services/realtime/ably/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 34fe749e..5e45b34f 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -421,6 +421,7 @@ export default class AblyRealtimeService extends RealtimeService implements Ably public setParticipantData = (data: ParticipantDataInput): void => { this.myParticipant.data = Object.assign({}, this.myParticipant.data, data); + this.updateMyProperties(this.myParticipant.data); this.updatePresence3D(this.myParticipant.data); }; From b08995ffcde0d0b6eab590295e70e968fd228abf Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 15 Jan 2024 14:59:56 -0300 Subject: [PATCH 55/70] feat(HTMLPin): hide temporary pin if clicking anywhere else on the screen --- .../comments/html-pin-adapter/index.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 5240bdb5..3f9705b3 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -180,6 +180,7 @@ export class HTMLPin implements PinAdapter { Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); document.body.addEventListener('keyup', this.resetPins); document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); + document.body.addEventListener('click', this.hideTemporaryPin); } /** @@ -202,6 +203,7 @@ export class HTMLPin implements PinAdapter { private removeListeners(): void { Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); document.body.removeEventListener('keyup', this.resetPins); + document.body.removeEventListener('click', this.hideTemporaryPin); } /** @@ -453,6 +455,23 @@ export class HTMLPin implements PinAdapter { }); } + /** + * @function hideTemporaryPin + * @description hides the temporary pin if click outside an observed element + * @param {MouseEvent} event the mouse event object + * @returns {void} + */ + private hideTemporaryPin = (event: MouseEvent): void => { + const target = event.target as HTMLElement; + + this.divWrappers.forEach((wrapper) => { + if (wrapper.contains(target) || this.pins.get('temporary-pin')?.contains(target)) return; + + this.removeAnnotationPin('temporary-pin'); + this.temporaryPinCoordinates = {}; + }); + }; + /** * @function clearElement * @description clears an element that no longer has the specified data attribute @@ -820,6 +839,7 @@ export class HTMLPin implements PinAdapter { if (this.pins.has('temporary-pin')) { this.removeAnnotationPin('temporary-pin'); + this.temporaryPinCoordinates.elementId = undefined; } }; } From ac61e7229a85b72e29453bca1f1415de66f18a93 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 15 Jan 2024 15:03:14 -0300 Subject: [PATCH 56/70] ci: add beta and lab release channels on npm --- .github/workflows/ci.yml | 137 --------------------- .github/workflows/publish-beta-release.yml | 43 +++++++ .github/workflows/publish-lab-release.yml | 43 +++++++ .github/workflows/publish-prod-release.yml | 55 +++++++++ .releaserc | 6 +- 5 files changed, 146 insertions(+), 138 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-beta-release.yml create mode 100644 .github/workflows/publish-lab-release.yml create mode 100644 .github/workflows/publish-prod-release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8f58a745..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Publish SDK Package -on: - push: - branches: - - main - - beta - - lab -jobs: - development: - if: github.ref == 'refs/heads/beta' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: '18.x' - - name: Install dependencies - run: yarn install - - name: Create .version file with beta version - run: | - touch .version.js && echo "echo \"export const version = 'beta'\" > .version.js" | bash - - - name: Create a .remote-config.js file - run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - - - name: Build package - run: | - yarn build - - name: Push - uses: s0/git-publish-subdir-action@develop - env: - REPO: self - BRANCH: beta-release - FOLDER: lib - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MESSAGE: 'BUILD: ({sha}) {msg}' - lab: - if: github.ref == 'refs/heads/lab' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: '18.x' - - name: Install dependencies - run: yarn install - - name: Create .version file with lab version - run: | - touch .version.js && echo "echo \"export const version = 'lab'\" > .version.js" | bash - - - name: Create a .remote-config.js file - run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - - - name: Build package - run: | - yarn build - - name: Push - uses: s0/git-publish-subdir-action@develop - env: - REPO: self - BRANCH: lab-release - FOLDER: lib - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MESSAGE: 'BUILD: ({sha}) {msg}' - main: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: '18.x' - - name: Install dependencies - run: yarn install - env: - NPM_CONFIG_USERCONFIG: .npmrc.ci - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create a .remote-config.js file - run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - - - run: git config --global user.name SuperViz - - run: git config --global user.email ci@superviz.com - - name: Publish npm package - run: npm whoami && npm run semantic-release - env: - NPM_CONFIG_USERCONFIG: .npmrc.ci - GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - slackNotificationDev: - needs: development - name: Slack Notification - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png - MSG_MINIMAL: true - SLACK_USERNAME: Deploy BETA SDK - slackNotificationLab: - needs: lab - name: Slack Notification - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png - MSG_MINIMAL: true - SLACK_USERNAME: Deploy LAB SDK - slackNotificationProd: - needs: main - name: Slack Notification - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png - MSG_MINIMAL: true - SLACK_USERNAME: Deploy SDK - updateSamplesVersion: - needs: main - name: Update samples version - runs-on: ubuntu-latest - steps: - - name: Repository Dispatch - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.SUPERVIZ_DEV_USER_TOKEN }} - repository: superviz/samples - event-type: new-release - client-payload: '{"version": "v0.0.0"}' diff --git a/.github/workflows/publish-beta-release.yml b/.github/workflows/publish-beta-release.yml new file mode 100644 index 00000000..b7c56c68 --- /dev/null +++ b/.github/workflows/publish-beta-release.yml @@ -0,0 +1,43 @@ +name: Publish Beta Version +on: + push: + branches: + - beta +jobs: + beta: + if: github.ref == 'refs/heads/beta' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '18.x' + - name: Install dependencies + run: yarn install + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Create a .remote-config.js file + run: | + touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + - run: git config --global user.name SuperViz + - run: git config --global user.email ci@superviz.com + - name: Publish npm package + run: npm whoami && npm run semantic-release + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + slack: + needs: beta + name: Slack Notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png + MSG_MINIMAL: true + SLACK_USERNAME: Deploy SDK beta version diff --git a/.github/workflows/publish-lab-release.yml b/.github/workflows/publish-lab-release.yml new file mode 100644 index 00000000..bc1ccfd0 --- /dev/null +++ b/.github/workflows/publish-lab-release.yml @@ -0,0 +1,43 @@ +name: Publish Lab Version +on: + push: + branches: + - lab +jobs: + lab: + if: github.ref == 'refs/heads/lab' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '18.x' + - name: Install dependencies + run: yarn install + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Create a .remote-config.js file + run: | + touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + - run: git config --global user.name SuperViz + - run: git config --global user.email ci@superviz.com + - name: Publish npm package + run: npm whoami && npm run semantic-release + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + slack: + needs: lab + name: Slack Notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png + MSG_MINIMAL: true + SLACK_USERNAME: Deploy SDK lab version diff --git a/.github/workflows/publish-prod-release.yml b/.github/workflows/publish-prod-release.yml new file mode 100644 index 00000000..81f3aec5 --- /dev/null +++ b/.github/workflows/publish-prod-release.yml @@ -0,0 +1,55 @@ +name: Publish Latest Version +on: + push: + branches: + - main +jobs: + main: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '18.x' + - name: Install dependencies + run: yarn install + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Create a .remote-config.js file + run: | + touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + - run: git config --global user.name SuperViz + - run: git config --global user.email ci@superviz.com + - name: Publish npm package + run: npm whoami && npm run semantic-release + env: + NPM_CONFIG_USERCONFIG: .npmrc.ci + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + slack: + needs: main + name: Slack Notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png + MSG_MINIMAL: true + SLACK_USERNAME: Deploy SDK latest version + samples: + needs: main + name: Update samples version + runs-on: ubuntu-latest + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.SUPERVIZ_DEV_USER_TOKEN }} + repository: superviz/samples + event-type: new-release + client-payload: '{"version": "v0.0.0"}' diff --git a/.releaserc b/.releaserc index 67b7d5e8..0c835b13 100644 --- a/.releaserc +++ b/.releaserc @@ -1,5 +1,9 @@ { - "branches": ["main"], + "branches": [ + "main", + { "name": "beta", "channel": "beta", "prerelease": true }, + { "name": "lab", "channel": "lab", "prerelease": true } + ], "plugins": [ "@semantic-release/commit-analyzer", "semantic-release-version-file", From a048eb9e8a662b93b64e0abfd2feaa765c2761c7 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 15 Jan 2024 15:03:55 -0300 Subject: [PATCH 57/70] ci(checks): when run checkes delete previus superviz-dev comments --- .github/workflows/checks.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 44047e1a..efb5dc3c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,6 +6,14 @@ on: - opened - synchronize jobs: + delete-comments: + runs-on: ubuntu-latest + steps: + - uses: izhangzhihao/delete-comment@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + delete_user_name: SuperViz-Dev + issue_number: ${{ github.event.number }} test-unit: runs-on: ubuntu-latest steps: From 2abd39bcca49a838d513480bfcc1453a902c711b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 15 Jan 2024 15:04:59 -0300 Subject: [PATCH 58/70] feat: emit flag to not call go to pin when creating annotation --- src/components/comments/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts index 11058525..a6fd5775 100644 --- a/src/components/comments/index.ts +++ b/src/components/comments/index.ts @@ -302,7 +302,7 @@ export class Comments extends BaseComponent { document.body.dispatchEvent( new CustomEvent('select-annotation', { - detail: { uuid: annotation.uuid }, + detail: { uuid: annotation.uuid, haltGoToPin: true }, composed: true, bubbles: true, }), From 6cdac508564267cb3aaa0c334f7127d765a3fb9f Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 15 Jan 2024 15:05:16 -0300 Subject: [PATCH 59/70] fix(CanvasPin): do not go to pin when creating annotation --- src/components/comments/canvas-pin-adapter/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index 68dac7cc..ffbb497c 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -367,7 +367,7 @@ export class CanvasPin implements PinAdapter { * @param {CustomEvent} event * @returns {void} */ - private annotationSelected = ({ detail: { uuid } }: CustomEvent): void => { + private annotationSelected = ({ detail: { uuid, haltGoToPin } }: CustomEvent): void => { if (!uuid) return; const annotation = JSON.parse(this.selectedPin?.getAttribute('annotation') ?? '{}'); @@ -385,6 +385,9 @@ export class CanvasPin implements PinAdapter { pinElement.setAttribute('active', ''); this.selectedPin = pinElement; + + if (haltGoToPin) return; + this.goToPin(uuid); }; From 548a087cfa1bd7eee1ce13c022981c8e0396b5e2 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 15 Jan 2024 15:13:37 -0300 Subject: [PATCH 60/70] feat: hide temporary pin if clicking anywhere else on the screen --- .../comments/canvas-pin-adapter/index.ts | 22 ++++++++++++++++++- .../comments/html-pin-adapter/index.ts | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index ffbb497c..62fd61cc 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -132,6 +132,9 @@ export class CanvasPin implements PinAdapter { pinElement.remove(); this.pins.delete(uuid); + + if (uuid === 'temporary-pin') return; + this.annotations = this.annotations.filter((annotation) => annotation.uuid !== uuid); } @@ -186,6 +189,7 @@ export class CanvasPin implements PinAdapter { document.body.addEventListener('keyup', this.resetPins); document.body.addEventListener('select-annotation', this.annotationSelected); document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); + document.body.addEventListener('click', this.hideTemporaryPin); } public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { @@ -205,6 +209,7 @@ export class CanvasPin implements PinAdapter { document.body.removeEventListener('keyup', this.resetPins); document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); + document.body.addEventListener('click', this.hideTemporaryPin); } /** @@ -444,7 +449,7 @@ export class CanvasPin implements PinAdapter { const transform = context.getTransform(); const invertedMatrix = transform.inverse(); - const transformedPoint = new DOMPoint(x, y).matrixTransform(invertedMatrix); + const transformedPoint = new DOMPoint(x, y - 31).matrixTransform(invertedMatrix); this.onPinFixedObserver.publish({ x: transformedPoint.x, @@ -486,4 +491,19 @@ export class CanvasPin implements PinAdapter { this.removeAnnotationPin('temporary-pin'); } }; + + /** + * @function hideTemporaryPin + * @description hides the temporary pin if click outside an observed element + * @param {MouseEvent} event the mouse event object + * @returns {void} + */ + private hideTemporaryPin = (event: MouseEvent): void => { + const target = event.target as HTMLElement; + + if (this.canvas.contains(target) || this.pins.get('temporary-pin')?.contains(target)) return; + + this.removeAnnotationPin('temporary-pin'); + this.temporaryPinCoordinates = null; + }; } diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3f9705b3..0386c78b 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -298,6 +298,8 @@ export class HTMLPin implements PinAdapter { this.pins.delete(uuid); } + if (uuid === 'temporary-pin') return; + this.annotations = this.annotations.filter((annotation) => { return annotation.uuid !== uuid; }); From 43218714d5caa9db2b739ffc972f5409d3a76d4a Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 15 Jan 2024 15:24:43 -0300 Subject: [PATCH 61/70] fix: correct tests --- src/components/comments/html-pin-adapter/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 6b2f9276..efaaa9b6 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -142,7 +142,7 @@ describe('HTMLPinAdapter', () => { instance['addListeners'](); - expect(bodyAddEventListenerSpy).toHaveBeenCalledTimes(2); + expect(bodyAddEventListenerSpy).toHaveBeenCalledTimes(3); expect(wrapperAddEventListenerSpy).toHaveBeenCalledTimes(6); }); @@ -158,7 +158,7 @@ describe('HTMLPinAdapter', () => { instance['removeListeners'](); - expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(1); + expect(bodyRemoveEventListenerSpy).toHaveBeenCalledTimes(2); expect(wrapperRemoveEventListenerSpy).toHaveBeenCalledTimes(6); }); }); From 8385977c0bab85f7aeff6bd07a3372a480e94282 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 15 Jan 2024 22:37:23 -0300 Subject: [PATCH 62/70] feat: ensure that only one room is open per browser --- src/components/base/index.test.ts | 2 ++ src/core/launcher/index.test.ts | 21 +++++++++++++++++++-- src/core/launcher/index.ts | 17 +++++++++++++++++ src/shims.d.ts | 2 ++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/components/base/index.test.ts b/src/components/base/index.test.ts index d506687a..5821f44a 100644 --- a/src/components/base/index.test.ts +++ b/src/components/base/index.test.ts @@ -45,6 +45,8 @@ describe('BaseComponent', () => { let DummyComponentInstance: DummyComponent; beforeEach(() => { + console.error = jest.fn(); + jest.clearAllMocks(); DummyComponentInstance = new DummyComponent(); }); diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index c95353fc..8fe3bf05 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -40,11 +40,13 @@ describe('Launcher', () => { let LauncherInstance: Launcher; beforeEach(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + console.log = jest.fn(); + jest.clearAllMocks(); LauncherInstance = new Launcher(DEFAULT_INITIALIZATION_MOCK); - console.error = jest.fn(); - console.log = jest.fn(); }); test('should be defined', () => { @@ -449,4 +451,19 @@ describe('Launcher Facade', () => { expect(LauncherFacadeInstance).toHaveProperty('addComponent'); expect(LauncherFacadeInstance).toHaveProperty('removeComponent'); }); + + test('should return the same instance if already initialized', () => { + const instance = Facade(DEFAULT_INITIALIZATION_MOCK); + const instance2 = Facade(DEFAULT_INITIALIZATION_MOCK); + + expect(instance).toStrictEqual(instance2); + }); + + test('should return different instances if it`s destroyed', () => { + const instance = Facade(DEFAULT_INITIALIZATION_MOCK); + instance.destroy(); + const instance2 = Facade(DEFAULT_INITIALIZATION_MOCK); + + expect(instance).not.toStrictEqual(instance2); + }); }); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index d641821f..e939890b 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -128,6 +128,9 @@ export class Launcher extends Observable implements DefaultLauncher { this.realtime.leave(); this.realtime = undefined; this.isDestroyed = true; + + // clean window object + window.SUPERVIZ = undefined; }; /** @@ -353,8 +356,22 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {LauncherFacade} */ export default (options: LauncherOptions): LauncherFacade => { + if (window.SUPERVIZ) { + console.warn('[SUPERVIZ] Room already initialized'); + + return { + destroy: window.SUPERVIZ.destroy, + subscribe: window.SUPERVIZ.subscribe, + unsubscribe: window.SUPERVIZ.unsubscribe, + addComponent: window.SUPERVIZ.addComponent, + removeComponent: window.SUPERVIZ.removeComponent, + }; + } + const launcher = new Launcher(options); + window.SUPERVIZ = launcher; + return { destroy: launcher.destroy, subscribe: launcher.subscribe, diff --git a/src/shims.d.ts b/src/shims.d.ts index fb248ebe..1743607f 100644 --- a/src/shims.d.ts +++ b/src/shims.d.ts @@ -1,7 +1,9 @@ import { SuperVizCdn } from './common/types/cdn.types'; +import { Launcher } from './core/launcher'; declare global { interface Window { SuperVizRoom: SuperVizCdn; + SUPERVIZ: Launcher; } } From 02626ba43cbf35d82d4cfbde1cc09ab536dc54e1 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 16 Jan 2024 08:07:45 -0300 Subject: [PATCH 63/70] fix: limit how much participants dropdown can grow and add scrollbar instead --- src/web-components/dropdown/index.ts | 5 +++++ src/web-components/tooltip/index.style.ts | 3 +-- src/web-components/tooltip/index.ts | 3 --- .../who-is-online/components/dropdown.ts | 13 +++++++++++-- .../who-is-online/css/dropdown.style.ts | 2 ++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/web-components/dropdown/index.ts b/src/web-components/dropdown/index.ts index fe669823..737d0c58 100644 --- a/src/web-components/dropdown/index.ts +++ b/src/web-components/dropdown/index.ts @@ -26,6 +26,7 @@ export class Dropdown extends WebComponentsBaseElement { declare name?: string; declare onHoverData: { name: string; action: string }; declare shiftTooltipLeft: boolean; + declare lastParticipant: boolean; private dropdownContent: HTMLElement; private originalPosition: Positions; @@ -56,6 +57,7 @@ export class Dropdown extends WebComponentsBaseElement { canShowTooltip: { type: Boolean }, drodpdownSizes: { type: Object }, shiftTooltipLeft: { type: Boolean }, + lastParticipant: { type: Boolean }, }; constructor() { @@ -438,9 +440,12 @@ export class Dropdown extends WebComponentsBaseElement { private tooltip = () => { if (!this.canShowTooltip) return ''; + const tooltipVerticalPosition = this.lastParticipant ? 'tooltip-top' : 'tooltip-bottom'; + return html` `; }; diff --git a/src/web-components/tooltip/index.style.ts b/src/web-components/tooltip/index.style.ts index 59d8aab6..74106008 100644 --- a/src/web-components/tooltip/index.style.ts +++ b/src/web-components/tooltip/index.style.ts @@ -20,7 +20,6 @@ export const dropdownStyle = css` cursor: default; display: none; transition: opacity 0.2s ease-in-out display 0s; - overflow-x: clip; z-index: 100; } @@ -114,7 +113,7 @@ export const dropdownStyle = css` .shift-left { left: 0; - transform: translateX(-22%); + transform: translateX(-6%); --vertical-offset: 2px; } diff --git a/src/web-components/tooltip/index.ts b/src/web-components/tooltip/index.ts index d906f67d..d623dd93 100644 --- a/src/web-components/tooltip/index.ts +++ b/src/web-components/tooltip/index.ts @@ -50,9 +50,6 @@ export class Tooltip extends WebComponentsBaseElement { const { parentElement } = this; parentElement?.addEventListener('mouseenter', this.show); parentElement?.addEventListener('mouseleave', this.hide); - - this.tooltipVerticalPosition = PositionsEnum['TOOLTIP-BOTTOM']; - this.tooltipHorizontalPosition = PositionsEnum['TOOLTIP-CENTER']; } private hide = () => { diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 46309f32..365725af 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -60,6 +60,12 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { this.showParticipantTooltip = true; } + protected firstUpdated( + _changedProperties: PropertyValueMap | Map, + ): void { + this.shadowRoot.querySelector('.menu').scrollTop = 0; + } + protected updated(changedProperties: PropertyValueMap | Map): void { if (!changedProperties.has('open')) return; @@ -71,7 +77,6 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { } document.removeEventListener('click', this.onClickOutDropdown); - // this.close(); } private onClickOutDropdown = (event: Event) => { @@ -137,11 +142,12 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { if (!this.participants) return; const icons = ['place', 'send']; + const numberOfParticipants = this.participants.length - 1; return repeat( this.participants, (participant) => participant.id, - (participant) => { + (participant, index) => { const { id, slotIndex, joinedPresence, isLocal, color, name } = participant; const disableDropdown = !joinedPresence || isLocal || this.disableDropdown; @@ -178,6 +184,8 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { tooltipData.action = 'Click to Follow'; } + const isLastParticipant = index === numberOfParticipants; + return html`
Date: Tue, 16 Jan 2024 14:32:32 -0300 Subject: [PATCH 64/70] fix: only hide temporary pin if current wrapper was not clicked --- .../comments/html-pin-adapter/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 0386c78b..78662324 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -465,13 +465,17 @@ export class HTMLPin implements PinAdapter { */ private hideTemporaryPin = (event: MouseEvent): void => { const target = event.target as HTMLElement; + const temporaryPinWrapper = this.divWrappers.get(this.temporaryPinCoordinates.elementId); - this.divWrappers.forEach((wrapper) => { - if (wrapper.contains(target) || this.pins.get('temporary-pin')?.contains(target)) return; + if (!temporaryPinWrapper) return; - this.removeAnnotationPin('temporary-pin'); - this.temporaryPinCoordinates = {}; - }); + const clickedOnWrapper = temporaryPinWrapper.contains(target); + const clickedOnTemporaryPin = this.pins.get('temporary-pin')?.contains(target); + + if (clickedOnWrapper || clickedOnTemporaryPin) return; + + this.removeAnnotationPin('temporary-pin'); + this.temporaryPinCoordinates = {}; }; /** @@ -763,7 +767,7 @@ export class HTMLPin implements PinAdapter { this.temporaryPinCoordinates = { ...this.temporaryPinCoordinates, x, y }; this.renderTemporaryPin(elementId); - const temporaryPin = this.divWrappers.get(elementId).querySelector('#superviz-temporary-pin'); + const temporaryPin = this.pins.get('temporary-pin'); // we don't care about the actual movedTemporaryPin value // it only needs to trigger an update From 78dfa1cce5a517de24b77799c697b028e6a1fca2 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 17 Jan 2024 13:32:28 -0300 Subject: [PATCH 65/70] feat(HTMLPin): support rect and ellipse svg elements --- .../comments/html-pin-adapter/index.ts | 244 +++++++++++++++--- 1 file changed, 215 insertions(+), 29 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 78662324..91689f09 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -47,7 +47,7 @@ export class HTMLPin implements PinAdapter { private divWrappers: Map = new Map(); private pins: Map; private voidElementsWrappers: Map = new Map(); - + private svgWrappers: HTMLElement; // Observers private mutationObserver: MutationObserver; @@ -119,7 +119,14 @@ export class HTMLPin implements PinAdapter { this.logger.log('Destroying HTML Pin Adapter for Comments'); this.removeListeners(); this.removeObservers(); - this.divWrappers.forEach((divWrapper) => divWrapper.remove()); + this.divWrappers.forEach((divWrapper) => { + if (divWrapper.getAttribute('data-wrapper-type')) { + divWrapper.parentElement.remove(); + return; + } + + divWrapper.remove(); + }); this.divWrappers.clear(); this.pins.forEach((pin) => pin.remove()); this.pins.clear(); @@ -133,6 +140,8 @@ export class HTMLPin implements PinAdapter { this.voidElementsWrappers.clear(); this.voidElementsWrappers = undefined; this.annotations = []; + this.svgWrappers?.remove(); + this.svgWrappers = undefined; document.body.removeEventListener('select-annotation', this.annotationSelected); document.body.removeEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); @@ -177,7 +186,7 @@ export class HTMLPin implements PinAdapter { * @returns {void} */ private addListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.addElementListeners(id)); + this.divWrappers.forEach((_, id) => this.addElementListeners(id)); document.body.addEventListener('keyup', this.resetPins); document.body.addEventListener('toggle-annotation-sidebar', this.onToggleAnnotationSidebar); document.body.addEventListener('click', this.hideTemporaryPin); @@ -201,7 +210,7 @@ export class HTMLPin implements PinAdapter { * @returns {void} * */ private removeListeners(): void { - Object.keys(this.elementsWithDataId).forEach((id) => this.removeElementListeners(id)); + this.divWrappers.forEach((_, id) => this.removeElementListeners(id)); document.body.removeEventListener('keyup', this.resetPins); document.body.removeEventListener('click', this.hideTemporaryPin); } @@ -230,7 +239,7 @@ export class HTMLPin implements PinAdapter { elementsWithDataId.forEach((el: HTMLElement) => { const id = el.getAttribute(this.dataAttribute); - const skip = this.dataAttributeValueFilters.some((filter, index) => { + const skip = this.dataAttributeValueFilters.some((filter) => { return id.match(filter); }); @@ -241,15 +250,13 @@ export class HTMLPin implements PinAdapter { } /** - * @function setAddCursor + * @function addAllCursors * @description sets the mouse cursor to a special cursor when hovering over all the elements with the specified data-attribute. * @returns {void} */ - private setAddCursor(): void { - Object.keys(this.elementsWithDataId).forEach((id) => { - this.divWrappers.get(id).style.cursor = - 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer'; - this.divWrappers.get(id).style.pointerEvents = 'auto'; + private addAllCursors(): void { + this.divWrappers.forEach((wrapper, id) => { + this.addCursor(wrapper, id); }); } @@ -259,9 +266,17 @@ export class HTMLPin implements PinAdapter { * @returns {void} */ private removeAddCursor(): void { - Object.keys(this.elementsWithDataId).forEach((id) => { - this.divWrappers.get(id).style.cursor = 'default'; - this.divWrappers.get(id).style.pointerEvents = 'none'; + this.divWrappers.forEach((wrapper, id) => { + let element: HTMLElement | SVGElement = wrapper; + + const isSvgElement = wrapper.getAttribute('data-wrapper-type'); + if (isSvgElement) { + const elementTagname = isSvgElement.split('-')[2]; + element = this.divWrappers.get(id).querySelector(elementTagname); + } + + element.style.setProperty('cursor', 'default'); + element.style.setProperty('pointer-events', 'none'); }); } @@ -347,7 +362,7 @@ export class HTMLPin implements PinAdapter { if (this.isActive) { this.addListeners(); - this.setAddCursor(); + this.addAllCursors(); this.prepareElements(); return; } @@ -375,7 +390,7 @@ export class HTMLPin implements PinAdapter { } if (!temporaryPin) { - const elementSides = this.elementsWithDataId[elementId].getBoundingClientRect(); + const elementSides = this.elementsWithDataId[elementId]?.getBoundingClientRect(); temporaryPin = document.createElement('superviz-comments-annotation-pin'); temporaryPin.id = 'superviz-temporary-pin'; @@ -430,8 +445,6 @@ export class HTMLPin implements PinAdapter { const wrapper = this.divWrappers.get(elementId); if (!wrapper) return; - const { width, height } = wrapper.getBoundingClientRect(); - const pinElement = this.createPin(annotation, x, y); wrapper.appendChild(pinElement); this.pins.set(annotation.uuid, pinElement); @@ -543,9 +556,10 @@ export class HTMLPin implements PinAdapter { * @param {string} id * @returns {void} */ - private setElementReadyToPin(element: HTMLElement, id: string): void { + private setElementReadyToPin(element: Element, id: string): void { if (this.elementsWithDataId[id]) return; - this.elementsWithDataId[id] = element; + + this.elementsWithDataId[id] = element as HTMLElement; if (!this.divWrappers.get(id)) { const divWrapper = this.createWrapper(element, id); @@ -554,12 +568,143 @@ export class HTMLPin implements PinAdapter { if (!this.isActive || !this.isPinsVisible) return; - this.divWrappers.get(id).style.cursor = - 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer'; - this.divWrappers.get(id).style.pointerEvents = 'auto'; + this.addCursor(this.divWrappers.get(id), id); this.addElementListeners(id); } + /** + * @function handleSvgElement + */ + private handleSvgElement(element: Element, wrapper: HTMLDivElement): HTMLDivElement { + const viewport = (element as SVGElement).viewportElement; + + const isNormalHTML = viewport === undefined; + if (isNormalHTML) return; + + const isSvgElement = element.tagName.toLowerCase() === 'svg'; + if (isSvgElement) { + const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); + foreignObject.setAttribute('height', '100%'); + foreignObject.setAttribute('width', '100%'); + foreignObject.style.setProperty('overflow', 'visible'); + foreignObject.appendChild(wrapper); + element.appendChild(foreignObject); + (element as SVGElement).style.setProperty('overflow', 'visible'); + return wrapper; + } + + const isEllipseElement = element.tagName.toLowerCase() === 'ellipse'; + const isRectElement = element.tagName.toLowerCase() === 'rect'; + + if (!isEllipseElement && !isRectElement) return; + + const elementName = element.tagName.toLowerCase(); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const svgElement = document.createElementNS('http://www.w3.org/2000/svg', elementName); + + let x: number | string; + let y: number | string; + let width: string; + let height: string; + let rx: string; + let ry: string; + + if (isRectElement) { + const rect = element as SVGRectElement; + x = rect.getAttribute('x'); + y = rect.getAttribute('y'); + width = rect.getAttribute('width'); + height = rect.getAttribute('height'); + rx = rect.getAttribute('rx'); + ry = rect.getAttribute('ry'); + svgElement.setAttribute('fill', 'transparent'); + svgElement.setAttribute('stroke', 'transparent'); + svgElement.setAttribute('x', x); + svgElement.setAttribute('y', y); + svgElement.setAttribute('rx', rx); + svgElement.setAttribute('ry', ry); + } + + if (isEllipseElement) { + const cx = element.getAttribute('cx'); + const cy = element.getAttribute('cy'); + + rx = element.getAttribute('rx'); + ry = element.getAttribute('ry'); + x = Number(cx) - Number(rx); + y = Number(cy) - Number(ry); + width = String(2 * Number(cx)); + height = String(2 * Number(cy)); + + svgElement.setAttribute('fill', 'transparent'); + svgElement.setAttribute('stroke', 'transparent'); + svgElement.setAttribute('cx', cx); + svgElement.setAttribute('cy', cy); + svgElement.setAttribute('rx', rx); + svgElement.setAttribute('ry', ry); + } + + svgElement.setAttribute('height', height); + svgElement.setAttribute('width', width); + + svg.setAttribute('height', '100%'); + svg.setAttribute('width', '100%'); + + svg.appendChild(svgElement); + wrapper.appendChild(svg); + + let externalViewport = viewport; + + while (externalViewport.viewportElement) { + externalViewport = externalViewport.viewportElement; + } + + const [transformX, transformY] = this.getTransform(element as SVGElement) ?? [0, 0]; + + svgElement.setAttribute('transform', `translate(${transformX}, ${transformY})`); + + if (!this.svgWrappers) { + const { left, top, width, height } = externalViewport.getBoundingClientRect(); + const svgWrapper = document.createElement('div'); + svgWrapper.style.setProperty('position', 'absolute'); + svgWrapper.style.setProperty('top', `${top}px`); + svgWrapper.style.setProperty('left', `${left}px`); + svgWrapper.style.setProperty('width', `${width}px`); + svgWrapper.style.setProperty('height', `${height}px`); + svgWrapper.style.setProperty('pointer-events', 'none'); + svgWrapper.style.setProperty('overflow', 'visible'); + this.svgWrappers = svgWrapper; + this.container.appendChild(svgWrapper); + } + + this.svgWrappers.appendChild(wrapper); + + (element as SVGElement).style.setProperty('overflow', 'visible'); + + wrapper.setAttribute('data-wrapper-type', 'svg-element-rect'); + + if (!this.svgWrappers) { + const { left, top, width, height } = externalViewport.getBoundingClientRect(); + const svgWrapper = document.createElement('div'); + svgWrapper.style.setProperty('position', 'absolute'); + svgWrapper.style.setProperty('top', `${top}px`); + svgWrapper.style.setProperty('left', `${left}px`); + svgWrapper.style.setProperty('width', `${width}px`); + svgWrapper.style.setProperty('height', `${height}px`); + svgWrapper.style.setProperty('pointer-events', 'none'); + svgWrapper.style.setProperty('overflow', 'visible'); + this.svgWrappers = svgWrapper; + this.container.appendChild(svgWrapper); + } + + this.svgWrappers.appendChild(wrapper); + + (element as SVGElement).style.setProperty('overflow', 'visible'); + + wrapper.setAttribute('data-wrapper-type', `svg-element-${elementName}`); + return wrapper; + } + /** * @function resetPins * @description Unselects selected pin and removes temporary pin. @@ -626,6 +771,7 @@ export class HTMLPin implements PinAdapter { if (!wrapper) return; wrapper.appendChild(pin); + wrapper.parentElement.appendChild(wrapper); } // ------- helper functions ------- @@ -648,6 +794,46 @@ export class HTMLPin implements PinAdapter { return pinElement; } + /** + * @function addCursor + * @description sets the mouse cursor to a special cursor when hovering over the element with the specified id + * @param {HTMLElement} wrapper the wrapper of the element + * @param {string} id the id of the element + * @returns {void} + */ + private addCursor(wrapper: HTMLElement | SVGElement, id: string): void { + let element: HTMLElement | SVGElement = wrapper; + + const isSvgElement = wrapper.getAttribute('data-wrapper-type'); + if (isSvgElement) { + const elementTagname = isSvgElement.split('-')[2]; + element = this.divWrappers.get(id).querySelector(elementTagname); + } + + element.style.setProperty( + 'cursor', + 'url("https://production.cdn.superviz.com/static/pin-html.png") 0 100, pointer', + ); + element.style.setProperty('pointer-events', 'auto'); + } + + /** + * @function getTransform + */ + private getTransform(element: SVGElement): number[] { + const viewport = element.viewportElement; + + const parentWithTransform = element.closest('[transform]'); + if (!parentWithTransform) return; + + if (!viewport.contains(parentWithTransform)) return; + + const transform = parentWithTransform.getAttribute('transform'); + const transformValues = transform.split(')'); + const [x, y] = transformValues[0].split('(')[1].replace(' ', '').split(','); + return [Number(x), Number(y)]; + } + /** * @function createWrapper * @description creates a wrapper for the element with the specified id @@ -655,13 +841,12 @@ export class HTMLPin implements PinAdapter { * @param {string} id the id of the element to be wrapped * @returns {HTMLElement} the new wrapper element */ - private createWrapper(element: HTMLElement, id: string): HTMLElement { - const container = element; + private createWrapper(element: Element, id: string): HTMLElement { const wrapperId = `superviz-id-${id}`; if (this.divWrappers.get(id)) return; - const containerRect = container.getBoundingClientRect(); + const containerRect = element.getBoundingClientRect(); const containerWrapper = document.createElement('div'); containerWrapper.setAttribute('data-wrapper-id', id); @@ -676,6 +861,9 @@ export class HTMLPin implements PinAdapter { containerWrapper.style.width = `100%`; containerWrapper.style.height = `100%`; + const svgWrapper = this.handleSvgElement(element, containerWrapper); + if (svgWrapper) return svgWrapper; + if (!this.VOID_ELEMENTS.includes(this.elementsWithDataId[id].tagName.toLowerCase())) { this.elementsWithDataId[id].appendChild(containerWrapper); this.setPositionNotStatic(this.elementsWithDataId[id]); @@ -730,9 +918,7 @@ export class HTMLPin implements PinAdapter { const target = event.target as HTMLElement; const wrapper = event.currentTarget as HTMLElement; - if (target !== wrapper && this.pins.has(target.id)) { - return; - } + if (target !== wrapper && this.pins.has(target.id)) return; const elementId = wrapper.getAttribute('data-wrapper-id'); const rect = wrapper.getBoundingClientRect(); From 9bf27720036fcf07c85db8ef897c4d36c3a55812 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 17 Jan 2024 14:17:02 -0300 Subject: [PATCH 66/70] fix: remove unecessary scroll --- src/web-components/comments/css/comment-input.style.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web-components/comments/css/comment-input.style.ts b/src/web-components/comments/css/comment-input.style.ts index c13516ae..712cd247 100644 --- a/src/web-components/comments/css/comment-input.style.ts +++ b/src/web-components/comments/css/comment-input.style.ts @@ -29,7 +29,6 @@ export const commentInputStyle = css` font-family: Roboto; white-space: pre-wrap; word-wrap: break-word; - overflow-y: scroll; resize: none; line-height: 1rem; max-height: 5rem; From 592ad366e89c180a74446b7f7d5a562e7cf2b9d7 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 17 Jan 2024 14:50:25 -0300 Subject: [PATCH 67/70] fix: remove same account check from core --- __mocks__/realtime.mock.ts | 1 + src/common/types/events.types.ts | 1 + src/core/index.test.ts | 11 --- src/core/index.ts | 11 --- src/core/launcher/index.test.ts | 54 ++++++++++----- src/core/launcher/index.ts | 7 ++ src/services/api/index.test.ts | 86 ------------------------ src/services/api/index.ts | 29 -------- src/services/realtime/ably/index.test.ts | 1 + src/services/realtime/ably/index.ts | 12 ++++ src/services/realtime/base/index.ts | 2 + 11 files changed, 61 insertions(+), 154 deletions(-) diff --git a/__mocks__/realtime.mock.ts b/__mocks__/realtime.mock.ts index a02019dc..0028e1ee 100644 --- a/__mocks__/realtime.mock.ts +++ b/__mocks__/realtime.mock.ts @@ -76,6 +76,7 @@ export const ABLY_REALTIME_MOCK: AblyRealtimeService = { privateModeWIOObserver: MOCK_OBSERVER_HELPER, followWIOObserver: MOCK_OBSERVER_HELPER, gatherWIOObserver: MOCK_OBSERVER_HELPER, + sameAccountObserver: MOCK_OBSERVER_HELPER, subscribeToParticipantUpdate: jest.fn(), unsubscribeFromParticipantUpdate: jest.fn(), updateMyProperties: jest.fn(), diff --git a/src/common/types/events.types.ts b/src/common/types/events.types.ts index 67de42d9..790e586c 100644 --- a/src/common/types/events.types.ts +++ b/src/common/types/events.types.ts @@ -85,6 +85,7 @@ export enum ParticipantEvent { LOCAL_LEFT = 'participant.local-left', LOCAL_UPDATED = 'participant.updated', LIST_UPDATED = 'participant.list-updated', + SAME_ACCOUNT_ERROR = 'participant.same-account-error', } /** diff --git a/src/core/index.test.ts b/src/core/index.test.ts index cb31db30..ddb7bfa3 100644 --- a/src/core/index.test.ts +++ b/src/core/index.test.ts @@ -149,15 +149,4 @@ describe('initialization errors', () => { 'Color sv-primary-900 is not a valid color variable value. Please check the documentation for more information.', ); }); - - test('should throw an error if the participant is already in the room', async () => { - ApiService.validadeParticipantIsEnteringTwice = jest.fn().mockResolvedValue(true); - ApiService.fetchConfig = jest.fn().mockResolvedValue({ - ablyKey: 'unit-test-ably-key', - }); - - await expect(sdk(UNIT_TEST_API_KEY, SIMPLE_INITIALIZATION_MOCK)).rejects.toThrow( - 'Participant is already in the room', - ); - }); }); diff --git a/src/core/index.ts b/src/core/index.ts index 735ec865..cf212287 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -107,17 +107,6 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { expect(LauncherInstance['activeComponentsInstances'].length).toBe(0); }); - }); - test('should destroy the instance', () => { - LauncherInstance.destroy(); + test('should publish REALTIME_SAME_ACCOUNT_ERROR, when same account callback is called', () => { + LauncherInstance['publish'] = jest.fn(); + + LauncherInstance['onSameAccount'](); - expect(ABLY_REALTIME_MOCK.leave).toHaveBeenCalled(); - expect(EVENT_BUS_MOCK.destroy).toHaveBeenCalled(); + expect(LauncherInstance['publish']).toHaveBeenCalledWith(ParticipantEvent.SAME_ACCOUNT_ERROR); + }); }); - test('should unsubscribe from realtime events', () => { - LauncherInstance.destroy(); + describe('destroy', () => { + test('should destroy the instance', () => { + LauncherInstance.destroy(); + + expect(ABLY_REALTIME_MOCK.leave).toHaveBeenCalled(); + expect(EVENT_BUS_MOCK.destroy).toHaveBeenCalled(); + }); - expect(ABLY_REALTIME_MOCK.participantJoinedObserver.unsubscribe).toHaveBeenCalled(); - expect(ABLY_REALTIME_MOCK.participantLeaveObserver.unsubscribe).toHaveBeenCalled(); - expect(ABLY_REALTIME_MOCK.participantsObserver.unsubscribe).toHaveBeenCalled(); - expect(ABLY_REALTIME_MOCK.hostObserver.unsubscribe).toHaveBeenCalled(); - expect(ABLY_REALTIME_MOCK.hostAvailabilityObserver.unsubscribe).toHaveBeenCalled(); - }); + test('should unsubscribe from realtime events', () => { + LauncherInstance.destroy(); + + expect(ABLY_REALTIME_MOCK.participantJoinedObserver.unsubscribe).toHaveBeenCalled(); + expect(ABLY_REALTIME_MOCK.participantLeaveObserver.unsubscribe).toHaveBeenCalled(); + expect(ABLY_REALTIME_MOCK.participantsObserver.unsubscribe).toHaveBeenCalled(); + expect(ABLY_REALTIME_MOCK.hostObserver.unsubscribe).toHaveBeenCalled(); + expect(ABLY_REALTIME_MOCK.hostAvailabilityObserver.unsubscribe).toHaveBeenCalled(); + }); + + test('should remove all components', () => { + LauncherInstance.addComponent(MOCK_COMPONENT); + LauncherInstance.destroy(); + + expect(MOCK_COMPONENT.detach).toHaveBeenCalled(); + }); - test('should remove all components', () => { - LauncherInstance.addComponent(MOCK_COMPONENT); - LauncherInstance.destroy(); + test('should destroy the instance when same account callback is called', () => { + LauncherInstance['publish'] = jest.fn(); + LauncherInstance['destroy'] = jest.fn(); - expect(MOCK_COMPONENT.detach).toHaveBeenCalled(); + LauncherInstance['onSameAccount'](); + + expect(LauncherInstance['publish']).toHaveBeenCalledWith(ParticipantEvent.SAME_ACCOUNT_ERROR); + expect(LauncherInstance['destroy']).toHaveBeenCalled(); + }); }); }); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index e939890b..5729ce0d 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -120,6 +120,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.eventBus.destroy(); this.eventBus = undefined; + this.realtime.sameAccountObserver.unsubscribe(this.onSameAccount); this.realtime.participantJoinedObserver.unsubscribe(this.onParticipantJoined); this.realtime.participantLeaveObserver.unsubscribe(this.onParticipantLeave); this.realtime.participantsObserver.unsubscribe(this.onParticipantListUpdate); @@ -203,6 +204,7 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {void} */ private subscribeToRealtimeEvents = (): void => { + this.realtime.sameAccountObserver.subscribe(this.onSameAccount); this.realtime.participantJoinedObserver.subscribe(this.onParticipantJoined); this.realtime.participantLeaveObserver.subscribe(this.onParticipantLeave); this.realtime.participantsObserver.subscribe(this.onParticipantListUpdate); @@ -347,6 +349,11 @@ export class Launcher extends Observable implements DefaultLauncher { } this.publish(RealtimeEvent.REALTIME_NO_HOST_AVAILABLE); }; + + private onSameAccount = (): void => { + this.publish(ParticipantEvent.SAME_ACCOUNT_ERROR); + this.destroy(); + }; } /** diff --git a/src/services/api/index.test.ts b/src/services/api/index.test.ts index 975e420e..1e36c8a9 100644 --- a/src/services/api/index.test.ts +++ b/src/services/api/index.test.ts @@ -195,90 +195,4 @@ describe('ApiService', () => { expect(response).toEqual(CHECK_LIMITS_MOCK.usage); }); }); - - describe('validadeParticipantIsEnteringTwice', () => { - test('should return true if the participant is entering twice', async () => { - const participant = { - id: 'any_participant_id', - name: 'any_participant_name', - }; - const roomId = 'any_room_id'; - const apiKey = 'unit-test-valid-api-key'; - const ablyKey = 'unit-test-ably-key'; - - global.fetch = jest.fn().mockResolvedValue({ - json: jest - .fn() - .mockResolvedValue([ - { data: JSON.stringify({ id: 'any_participant_id' }) }, - { data: JSON.stringify({ id: 'another_participant_id' }) }, - ]), - }); - - const response = await ApiService.validadeParticipantIsEnteringTwice( - participant, - roomId, - apiKey, - ablyKey, - ); - - expect(response).toEqual(true); - expect(fetch).toHaveBeenCalledWith( - 'https://rest.ably.io/channels/superviz:any_room_id-unit-test-valid-api-key/presence', - { - headers: { Authorization: 'Basic dW5pdC10ZXN0LWFibHkta2V5' }, - }, - ); - }); - - test('should return false if the participant is not entering twice', async () => { - const participant = { - id: 'any_participant_id', - name: 'any_participant_name', - }; - const roomId = 'any_room_id'; - const apiKey = 'unit-test-valid-api-key'; - const ablyKey = 'unit-test-ably-key'; - - global.fetch = jest.fn().mockResolvedValue({ - json: jest - .fn() - .mockResolvedValue([ - { data: JSON.stringify({ id: 'another_participant_id' }) }, - { data: JSON.stringify({ id: 'yet_another_participant_id' }) }, - ]), - }); - - const response = await ApiService.validadeParticipantIsEnteringTwice( - participant, - roomId, - apiKey, - ablyKey, - ); - - expect(response).toEqual(false); - expect(fetch).toHaveBeenCalledWith( - 'https://rest.ably.io/channels/superviz:any_room_id-unit-test-valid-api-key/presence', - { - headers: { Authorization: 'Basic dW5pdC10ZXN0LWFibHkta2V5' }, - }, - ); - }); - - test('should throw an error if failed to fetch realtime participants', async () => { - const participant = { - id: 'any_participant_id', - name: 'any_participant_name', - }; - const roomId = 'any_room_id'; - const apiKey = 'unit-test-valid-api-key'; - const ablyKey = 'unit-test-ably-key'; - - global.fetch = jest.fn().mockRejectedValue(new Error('Failed to fetch participants')); - - await expect( - ApiService.validadeParticipantIsEnteringTwice(participant, roomId, apiKey, ablyKey), - ).rejects.toThrow('Failed to fetch realtime participants'); - }); - }); }); diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 9de22c0b..d65ef829 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -127,33 +127,4 @@ export default class ApiService { }; return doRequest(url, 'POST', body, { apikey }); } - - static async validadeParticipantIsEnteringTwice( - participant: SuperVizSdkOptions['participant'], - roomId: string, - apiKey: string, - ablyKey: string, - ) { - const ablyKey64 = window.btoa(ablyKey); - const ablyRoom = `superviz:${roomId.toLowerCase()}-${apiKey}`; - const ablyUrl = `https://rest.ably.io/channels/${ablyRoom}/presence`; - - try { - const response = await fetch(ablyUrl, { - headers: { Authorization: `Basic ${ablyKey64}` }, - }); - - const participants = await response.json(); - - const hasParticipantWithSameId = participants.some((presence) => { - const data = JSON.parse(presence.data); - - return data.id === participant.id; - }); - - return hasParticipantWithSameId; - } catch (error) { - throw new Error('Failed to fetch realtime participants'); - } - } } diff --git a/src/services/realtime/ably/index.test.ts b/src/services/realtime/ably/index.test.ts index 97a575c9..1bf2f00f 100644 --- a/src/services/realtime/ably/index.test.ts +++ b/src/services/realtime/ably/index.test.ts @@ -63,6 +63,7 @@ const AblyRealtimeMock = { update: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), + leave: jest.fn(), }, }; }), diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 5e45b34f..91465430 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -216,6 +216,10 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.supervizChannel.on(this.onAblyChannelStateChange); this.supervizChannel.subscribe('update', this.onAblyRoomUpdate); + this.supervizChannel.subscribe('same-account-error', (message) => { + console.log('same-account-error', message); + }); + // join the comments channel this.commentsChannel = this.client.channels.get(`${this.roomId}:comments`); this.commentsChannel.subscribe('update', this.onCommentsChannelUpdate); @@ -238,6 +242,7 @@ export default class AblyRealtimeService extends RealtimeService implements Ably */ public leave(): void { this.logger.log('REALTIME', 'Disconnecting from ably servers'); + this.supervizChannel.presence.leave(); this.client.close(); this.isJoinedRoom = false; this.isReconnecting = false; @@ -472,6 +477,11 @@ export default class AblyRealtimeService extends RealtimeService implements Ably * @returns {void} */ private onAblyPresenceEnter(presenceMessage: Ably.Types.PresenceMessage): void { + if (presenceMessage.clientId === this.myParticipant.data.participantId && this.isJoinedRoom) { + this.sameAccountObserver.publish(true); + return; + } + if (presenceMessage.clientId === this.myParticipant.data.participantId) { this.onJoinRoom(presenceMessage); } else { @@ -590,6 +600,8 @@ export default class AblyRealtimeService extends RealtimeService implements Ably * @returns {void} */ private onAblyRoomUpdate(message: Ably.Types.Message): void { + this.logger.log('REALTIME', 'Room update received', message.data); + this.updateLocalRoomState(message.data); } diff --git a/src/services/realtime/base/index.ts b/src/services/realtime/base/index.ts index 98f2d50a..c2af3a11 100644 --- a/src/services/realtime/base/index.ts +++ b/src/services/realtime/base/index.ts @@ -33,6 +33,7 @@ export class RealtimeService implements DefaultRealtimeService { public presence3dLeaveObserver: Observer; public presence3dJoinedObserver: Observer; public domainRefusedObserver: Observer; + public sameAccountObserver: Observer; constructor() { this.participantObservers = []; @@ -45,6 +46,7 @@ export class RealtimeService implements DefaultRealtimeService { this.participantLeaveObserver = new Observer({ logger: this.logger }); this.syncPropertiesObserver = new Observer({ logger: this.logger }); this.reconnectObserver = new Observer({ logger: this.logger }); + this.sameAccountObserver = new Observer({ logger: this.logger }); // Room info observers helpers this.roomInfoUpdatedObserver = new Observer({ logger: this.logger }); From 6e9151d516b725b8c5c6a0eaa8ee6f21122bb7e8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 17 Jan 2024 20:11:53 -0300 Subject: [PATCH 68/70] fix: correct tests --- .../comments/html-pin-adapter/index.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index efaaa9b6..da5f1cb4 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -96,10 +96,23 @@ describe('HTMLPinAdapter', () => { const removeSpy = jest.fn(); const removeEventListenerSpy = jest.fn(); + const getAttribute = jest + .fn() + .mockResolvedValue(Math.random() > 0.5 ? '' : 'data-wrapper-type'); + const parentElement = { + remove: removeSpy, + }; + const wrappers = [...instance['divWrappers']].map(([entry, value]) => { return [ entry, - { ...value, remove: removeSpy, removeEventListener: removeEventListenerSpy }, + { + ...value, + remove: removeSpy, + removeEventListener: removeEventListenerSpy, + getAttribute, + parentElement, + }, ]; }); instance['divWrappers'] = new Map(wrappers as [key: any, value: any][]); From a56b5cf1e4e602d4bc5784b0058e1602f28df54f Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 18 Jan 2024 00:23:18 -0300 Subject: [PATCH 69/70] fix: position pin correctly on scroll --- .../comments/html-pin-adapter/index.ts | 115 ++++-------------- 1 file changed, 24 insertions(+), 91 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 91689f09..24cae90d 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -67,6 +67,9 @@ export class HTMLPin implements PinAdapter { 'source', 'track', 'wbr', + 'svg', + 'rect', + 'ellipse', ]; constructor(containerId: string, options: HTMLPinOptions = {}) { @@ -119,14 +122,7 @@ export class HTMLPin implements PinAdapter { this.logger.log('Destroying HTML Pin Adapter for Comments'); this.removeListeners(); this.removeObservers(); - this.divWrappers.forEach((divWrapper) => { - if (divWrapper.getAttribute('data-wrapper-type')) { - divWrapper.parentElement.remove(); - return; - } - - divWrapper.remove(); - }); + this.divWrappers.forEach((divWrapper) => divWrapper.remove()); this.divWrappers.clear(); this.pins.forEach((pin) => pin.remove()); this.pins.clear(); @@ -590,15 +586,16 @@ export class HTMLPin implements PinAdapter { foreignObject.appendChild(wrapper); element.appendChild(foreignObject); (element as SVGElement).style.setProperty('overflow', 'visible'); + // wrapper.setAttribute() return wrapper; } - const isEllipseElement = element.tagName.toLowerCase() === 'ellipse'; - const isRectElement = element.tagName.toLowerCase() === 'rect'; + const elementName = element.tagName.toLowerCase(); + const isEllipseElement = elementName === 'ellipse'; + const isRectElement = elementName === 'rect'; if (!isEllipseElement && !isRectElement) return; - const elementName = element.tagName.toLowerCase(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const svgElement = document.createElementNS('http://www.w3.org/2000/svg', elementName); @@ -617,10 +614,10 @@ export class HTMLPin implements PinAdapter { height = rect.getAttribute('height'); rx = rect.getAttribute('rx'); ry = rect.getAttribute('ry'); - svgElement.setAttribute('fill', 'transparent'); - svgElement.setAttribute('stroke', 'transparent'); - svgElement.setAttribute('x', x); - svgElement.setAttribute('y', y); + svgElement.setAttribute('fill', 'red'); + svgElement.setAttribute('stroke', 'blue'); + svgElement.setAttribute('x', '0'); + svgElement.setAttribute('y', '0'); svgElement.setAttribute('rx', rx); svgElement.setAttribute('ry', ry); } @@ -638,8 +635,8 @@ export class HTMLPin implements PinAdapter { svgElement.setAttribute('fill', 'transparent'); svgElement.setAttribute('stroke', 'transparent'); - svgElement.setAttribute('cx', cx); - svgElement.setAttribute('cy', cy); + svgElement.setAttribute('cx', `${Number(cx) - x}`); + svgElement.setAttribute('cy', `${Number(cy) - y}`); svgElement.setAttribute('rx', rx); svgElement.setAttribute('ry', ry); } @@ -647,61 +644,15 @@ export class HTMLPin implements PinAdapter { svgElement.setAttribute('height', height); svgElement.setAttribute('width', width); - svg.setAttribute('height', '100%'); - svg.setAttribute('width', '100%'); + svg.setAttribute('height', height); + svg.setAttribute('width', width); svg.appendChild(svgElement); - wrapper.appendChild(svg); - - let externalViewport = viewport; - - while (externalViewport.viewportElement) { - externalViewport = externalViewport.viewportElement; - } - - const [transformX, transformY] = this.getTransform(element as SVGElement) ?? [0, 0]; - - svgElement.setAttribute('transform', `translate(${transformX}, ${transformY})`); - - if (!this.svgWrappers) { - const { left, top, width, height } = externalViewport.getBoundingClientRect(); - const svgWrapper = document.createElement('div'); - svgWrapper.style.setProperty('position', 'absolute'); - svgWrapper.style.setProperty('top', `${top}px`); - svgWrapper.style.setProperty('left', `${left}px`); - svgWrapper.style.setProperty('width', `${width}px`); - svgWrapper.style.setProperty('height', `${height}px`); - svgWrapper.style.setProperty('pointer-events', 'none'); - svgWrapper.style.setProperty('overflow', 'visible'); - this.svgWrappers = svgWrapper; - this.container.appendChild(svgWrapper); - } - - this.svgWrappers.appendChild(wrapper); - (element as SVGElement).style.setProperty('overflow', 'visible'); - - wrapper.setAttribute('data-wrapper-type', 'svg-element-rect'); - - if (!this.svgWrappers) { - const { left, top, width, height } = externalViewport.getBoundingClientRect(); - const svgWrapper = document.createElement('div'); - svgWrapper.style.setProperty('position', 'absolute'); - svgWrapper.style.setProperty('top', `${top}px`); - svgWrapper.style.setProperty('left', `${left}px`); - svgWrapper.style.setProperty('width', `${width}px`); - svgWrapper.style.setProperty('height', `${height}px`); - svgWrapper.style.setProperty('pointer-events', 'none'); - svgWrapper.style.setProperty('overflow', 'visible'); - this.svgWrappers = svgWrapper; - this.container.appendChild(svgWrapper); - } - - this.svgWrappers.appendChild(wrapper); + wrapper.appendChild(svg); (element as SVGElement).style.setProperty('overflow', 'visible'); - - wrapper.setAttribute('data-wrapper-type', `svg-element-${elementName}`); + wrapper.setAttribute('data-wrapper-type', `svg-${elementName}`); return wrapper; } @@ -806,7 +757,7 @@ export class HTMLPin implements PinAdapter { const isSvgElement = wrapper.getAttribute('data-wrapper-type'); if (isSvgElement) { - const elementTagname = isSvgElement.split('-')[2]; + const elementTagname = isSvgElement.split('-')[1]; element = this.divWrappers.get(id).querySelector(elementTagname); } @@ -817,23 +768,6 @@ export class HTMLPin implements PinAdapter { element.style.setProperty('pointer-events', 'auto'); } - /** - * @function getTransform - */ - private getTransform(element: SVGElement): number[] { - const viewport = element.viewportElement; - - const parentWithTransform = element.closest('[transform]'); - if (!parentWithTransform) return; - - if (!viewport.contains(parentWithTransform)) return; - - const transform = parentWithTransform.getAttribute('transform'); - const transformValues = transform.split(')'); - const [x, y] = transformValues[0].split('(')[1].replace(' ', '').split(','); - return [Number(x), Number(y)]; - } - /** * @function createWrapper * @description creates a wrapper for the element with the specified id @@ -848,7 +782,7 @@ export class HTMLPin implements PinAdapter { const containerRect = element.getBoundingClientRect(); - const containerWrapper = document.createElement('div'); + let containerWrapper = document.createElement('div'); containerWrapper.setAttribute('data-wrapper-id', id); containerWrapper.id = wrapperId; @@ -861,15 +795,14 @@ export class HTMLPin implements PinAdapter { containerWrapper.style.width = `100%`; containerWrapper.style.height = `100%`; - const svgWrapper = this.handleSvgElement(element, containerWrapper); - if (svgWrapper) return svgWrapper; - if (!this.VOID_ELEMENTS.includes(this.elementsWithDataId[id].tagName.toLowerCase())) { this.elementsWithDataId[id].appendChild(containerWrapper); this.setPositionNotStatic(this.elementsWithDataId[id]); return containerWrapper; } + containerWrapper = this.handleSvgElement(element, containerWrapper) ?? containerWrapper; + containerWrapper.style.position = 'fixed'; containerWrapper.style.top = `${containerRect.top}px`; containerWrapper.style.left = `${containerRect.left}px`; @@ -878,9 +811,9 @@ export class HTMLPin implements PinAdapter { let parent = this.elementsWithDataId[id].parentElement; - if (!parent) parent = document.body; + if (!parent || (element as SVGElement).viewportElement) parent = document.body; - this.setPositionNotStatic(parent); + this.setPositionNotStatic(parent as HTMLElement); parent.appendChild(containerWrapper); this.voidElementsWrappers.set(id, containerWrapper); From 2f171c58ecf95ba72a29973705669775a843fdb8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 18 Jan 2024 08:29:02 -0300 Subject: [PATCH 70/70] fix: remove colors --- src/components/comments/html-pin-adapter/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 24cae90d..bf98a89a 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -614,8 +614,8 @@ export class HTMLPin implements PinAdapter { height = rect.getAttribute('height'); rx = rect.getAttribute('rx'); ry = rect.getAttribute('ry'); - svgElement.setAttribute('fill', 'red'); - svgElement.setAttribute('stroke', 'blue'); + svgElement.setAttribute('fill', 'transparent'); + svgElement.setAttribute('stroke', 'transparent'); svgElement.setAttribute('x', '0'); svgElement.setAttribute('y', '0'); svgElement.setAttribute('rx', rx);