// Thanks to https://stackoverflow.com/a/49349813 import { clamp } from "../helpers/number"; /** * Attibute modifier to create middle ellipsis * When the attribute value is left blank the ellipsis will be in the middle * When positive the attribute value will be used as a percentage * When negative the attribute value will be used as character index counted from the end * @example *
A Javascript solution to middle ellipsis
*
A Javascript solution to middle ellipsis
*
A Javascript solution to middle ellipsis
*/ const attributeName = 'data-middle-ellipsis'; const ellipsis = '…'; const map: Map = new Map(); const testQueue: Set = new Set(); const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif'; const fontSize = '16px'; let timeoutId: number; const setTestQueue = () => { cancelAnimationFrame(timeoutId); timeoutId = window.requestAnimationFrame(testQueueElements); }; const testQueueElements = () => { testQueue.forEach(testElement); testQueue.clear(); }; window.addEventListener('resize', () => { (Array.from(document.querySelectorAll(`[${attributeName}]`)) as HTMLElement[]).forEach(el => testQueue.add(el)); setTestQueue(); }, {capture: true, passive: true}); const testElement = (elm: HTMLElement) => { //const perf = performance.now(); // do not recalculate variables a second time const mapped = map.get(elm); let {text, textLength, from, multiplier, font, textWidth, elementWidth} = mapped || {}; // first time if(!mapped) { text = elm.textContent; textLength = text.length; from = parseFloat(elm.getAttribute(attributeName)) || 50; multiplier = from > 0 && from / 100; //const perf = performance.now(); font = `${elm.dataset.fontWeight || 400} ${fontSize} ${fontFamily}`; /* const computedStyle = window.getComputedStyle(elm, null); font = `${computedStyle.getPropertyValue('font-weight')} ${computedStyle.getPropertyValue('font-size')} ${computedStyle.getPropertyValue('font-family')}`; */ //console.log('testMiddleEllipsis get computed style:', performance.now() - perf, font); textWidth = getTextWidth(text, font); //const perf = performance.now(); elementWidth = elm.offsetWidth; //console.log('testMiddleEllipsis get offsetWidth:', performance.now() - perf, font); map.set(elm, {text, textLength, from, multiplier, font, textWidth, elementWidth}); } const {offsetWidth} = elm; const widthChanged = !mapped || elementWidth !== offsetWidth; mapped && widthChanged && (mapped.elementWidth = elementWidth = offsetWidth); if(widthChanged) { if(textWidth > elementWidth) { elm.setAttribute('title', text); let smallerText = text; let smallerWidth = elementWidth; while(smallerText.length > 3) { let smallerTextLength = smallerText.length; const half = multiplier && clamp(multiplier * smallerTextLength << 0, 1, smallerTextLength - 2) || Math.max(smallerTextLength + from - 1, 1); const half1 = smallerText.substr(0, half).replace(/\s*$/,''); const half2 = smallerText.substr(half + 1).replace(/^\s*/,''); smallerText = half1 + half2; smallerWidth = getTextWidth(smallerText + ellipsis, font); if(smallerWidth < elementWidth) { elm.textContent = half1 + ellipsis + half2; break; } } } else { elm.removeAttribute('title'); } } //console.log('testMiddleEllipsis for element:', elm, performance.now() - perf); }; let context: CanvasRenderingContext2D; /** * Get the text width * @param {string} text * @param {string} font */ function getTextWidth(text: string, font: string) { //const perf = performance.now(); if(!context) { const canvas = document.createElement('canvas'); context = canvas.getContext('2d'); context.font = font; } //context.font = font; const metrics = context.measureText(text); //console.log('getTextWidth perf:', performance.now() - perf); //return metrics.width; return Math.round(metrics.width); } export class MiddleEllipsisElement extends HTMLElement { constructor() { super(); if(this.getAttribute('data-middle-ellipsis') === null) { this.setAttribute('data-middle-ellipsis', ''); } } connectedCallback() { testQueue.add(this); setTestQueue(); //testElement(this); } disconnectedCallback() { map.delete(this); } } customElements.define("middle-ellipsis-element", MiddleEllipsisElement);