import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { useStore, ProviderStore } from './state/store'; import { toInt } from './utils/utils'; /** * The Ol Highlight element * * Allows highlighting of the selected lines and elements * on the page. */ @customElement('ol-highlight') export class OlHighlight extends LitElement { // Reactive property to see if the highlight component is active @state() highlight: boolean = useStore(this).highlight; //Reactive property to update the highlightColor @state() highlightColor: string = useStore(this).highlightColor; // Variable to get the active element @state() activeElement: ProviderStore['activeElement'] | null = useStore(this).activeElement; // Reactive property of the last Y position clicked @state() currentY: number = 0; // function the gets the calculated lineheight // the footprint below is suggested but not mandatory. This // will be used to calculate the height of the highlight div. getLineheight(): number { const window = this.activeElement?.ownerDocument.defaultView; if (this.activeElement && window) { const compStyles = window.getComputedStyle(this.activeElement); let height = compStyles.lineHeight == "normal" ? toInt(compStyles.fontSize) * 1.2: toInt(compStyles.lineHeight); return height; } return 0; } // function that calculates the width of the // highlight component based off of the activeElement's width, // padding, content, and margin. getWidth(): number { return this.activeElement?.getBoundingClientRect().width || 0; } // function that calculates the y offset of the // highlight component based off of the activeElement's y position, // padding, and margin. getYOffset(): number { const window = this.activeElement?.ownerDocument.defaultView; if (this.activeElement && window) { const rec = this.activeElement.getBoundingClientRect(); const lh = this.getLineheight(); // Get the padding from the top. const padding = toInt(window.getComputedStyle(this.activeElement, null).paddingTop); // Get the active element's offset from the top of the page const top = rec.y; // Get the currentY's offset from the top of the active element const offset = this.currentY - top; //console.log(top + (offset - offset % lh) + padding) return top + (offset - offset % lh) + padding; } return this.currentY; } // function that calculates the x offset of the // highlight component based off of the activeElement's x position, // padding, and margin. getXOffset(): number { // getYOffset(): number { return this.activeElement?.getBoundingClientRect().x || 0; } //function that determines highlighter color from the store's //highlightColor variable getColor(): string { return this.highlightColor; } //Event listener for the currentY of last clicked position applyCurrentY(): void { const document = this.activeElement?.ownerDocument; if (!document) return; document.onmousedown = (event) => { const { pageY } = event; if (event.target == this.activeElement) this.currentY = pageY; }; document.onkeydown = (event) => { let miny = this.activeElement?.offsetTop || 0; let incr = this.activeElement?.getBoundingClientRect().height || 0; let maxy = miny + incr; if (event.key === 'ArrowDown') { // console.log('Down') let position = 0; position = this.currentY + this.getLineheight(); if (position >= miny && position <= maxy) this.currentY = position; } if (event.key === 'ArrowUp') { // console.log('Up') let position = 0; position = this.currentY - this.getLineheight(); if (position >= miny && position <= maxy) this.currentY = position; } }; } // Base styles for the highlight component. static override styles = css` #ol-highlight { position: absolute; top: 0; left: 0; z-index: 1000; background-color: yellow; opacity: .25; pointer-events: none; } ` override render() { // listener to the active element to see which // line was clicked, this will provide the X and Y coords // which can be used to calculate the line. //if active element does not exist or there's no text //inside active element, then do not display highlighter if (!this.activeElement) return html``; const element = this.activeElement.cloneNode(true); const children = element.children; // removes all children from the element node for (let j = children.length; j--;) { element.removeChild(children[j]) } // console.log(children) // check if content contains text data const content = element.innerText ? element.innerText : element.textContent; //Return nothing if there is no text inside activeElement excluding children if (!content || !(content.trim().length)) return html``; // Return nothing if the highlight boolean is set to false if (!this.highlight) return html``; // Apply the listeners that update current Y this.applyCurrentY(); const document = this.activeElement?.ownerDocument; const prev = document.getElementById("ol-highlight"); if (prev) { document.body.removeChild(prev); } const highlight = document.createElement("div"); highlight.setAttribute("style", ` position: absolute; width: ${this.getWidth()}px; height: ${this.getLineheight()}px; top: ${this.getYOffset()}px; left: ${this.getXOffset()}px; background-color: ${this.getColor()}; opacity: .2; `); highlight.setAttribute("id", "ol-highlight"); document.body.appendChild(highlight); return ( html`` ) } } declare global { interface HTMLElementTagNameMap { 'ol-highlight': OlHighlight; } }