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;
}
}