UNPKG

@scrollmeter/core

Version:

Scrollmeter is a lightweight JavaScript library that visually displays scroll progress on web pages.

1 lines 91.5 kB
{"version":3,"file":"index.cjs","sources":["../../../node_modules/html-to-image/es/util.js","../../../node_modules/html-to-image/es/clone-pseudos.js","../../../node_modules/html-to-image/es/mimes.js","../../../node_modules/html-to-image/es/dataurl.js","../../../node_modules/html-to-image/es/clone-node.js","../../../node_modules/html-to-image/es/embed-resources.js","../../../node_modules/html-to-image/es/embed-images.js","../../../node_modules/html-to-image/es/apply-style.js","../../../node_modules/html-to-image/es/embed-webfonts.js","../../../node_modules/html-to-image/es/index.js","../src/types/scrollmeter.types.ts","../src/class/scrollmeter-tooltip.ts","../src/class/scrollmeter-timeline.ts","../src/class/scrollmeter.ts","../src/index.ts"],"sourcesContent":["export function resolveUrl(url, baseUrl) {\n // url is absolute already\n if (url.match(/^[a-z]+:\\/\\//i)) {\n return url;\n }\n // url is absolute already, without protocol\n if (url.match(/^\\/\\//)) {\n return window.location.protocol + url;\n }\n // dataURI, mailto:, tel:, etc.\n if (url.match(/^[a-z]+:/i)) {\n return url;\n }\n const doc = document.implementation.createHTMLDocument();\n const base = doc.createElement('base');\n const a = doc.createElement('a');\n doc.head.appendChild(base);\n doc.body.appendChild(a);\n if (baseUrl) {\n base.href = baseUrl;\n }\n a.href = url;\n return a.href;\n}\nexport const uuid = (() => {\n // generate uuid for className of pseudo elements.\n // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.\n let counter = 0;\n // ref: http://stackoverflow.com/a/6248722/2519373\n const random = () => \n // eslint-disable-next-line no-bitwise\n `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);\n return () => {\n counter += 1;\n return `u${random()}${counter}`;\n };\n})();\nexport function delay(ms) {\n return (args) => new Promise((resolve) => {\n setTimeout(() => resolve(args), ms);\n });\n}\nexport function toArray(arrayLike) {\n const arr = [];\n for (let i = 0, l = arrayLike.length; i < l; i++) {\n arr.push(arrayLike[i]);\n }\n return arr;\n}\nfunction px(node, styleProperty) {\n const win = node.ownerDocument.defaultView || window;\n const val = win.getComputedStyle(node).getPropertyValue(styleProperty);\n return val ? parseFloat(val.replace('px', '')) : 0;\n}\nfunction getNodeWidth(node) {\n const leftBorder = px(node, 'border-left-width');\n const rightBorder = px(node, 'border-right-width');\n return node.clientWidth + leftBorder + rightBorder;\n}\nfunction getNodeHeight(node) {\n const topBorder = px(node, 'border-top-width');\n const bottomBorder = px(node, 'border-bottom-width');\n return node.clientHeight + topBorder + bottomBorder;\n}\nexport function getImageSize(targetNode, options = {}) {\n const width = options.width || getNodeWidth(targetNode);\n const height = options.height || getNodeHeight(targetNode);\n return { width, height };\n}\nexport function getPixelRatio() {\n let ratio;\n let FINAL_PROCESS;\n try {\n FINAL_PROCESS = process;\n }\n catch (e) {\n // pass\n }\n const val = FINAL_PROCESS && FINAL_PROCESS.env\n ? FINAL_PROCESS.env.devicePixelRatio\n : null;\n if (val) {\n ratio = parseInt(val, 10);\n if (Number.isNaN(ratio)) {\n ratio = 1;\n }\n }\n return ratio || window.devicePixelRatio || 1;\n}\n// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size\nconst canvasDimensionLimit = 16384;\nexport function checkCanvasDimensions(canvas) {\n if (canvas.width > canvasDimensionLimit ||\n canvas.height > canvasDimensionLimit) {\n if (canvas.width > canvasDimensionLimit &&\n canvas.height > canvasDimensionLimit) {\n if (canvas.width > canvas.height) {\n canvas.height *= canvasDimensionLimit / canvas.width;\n canvas.width = canvasDimensionLimit;\n }\n else {\n canvas.width *= canvasDimensionLimit / canvas.height;\n canvas.height = canvasDimensionLimit;\n }\n }\n else if (canvas.width > canvasDimensionLimit) {\n canvas.height *= canvasDimensionLimit / canvas.width;\n canvas.width = canvasDimensionLimit;\n }\n else {\n canvas.width *= canvasDimensionLimit / canvas.height;\n canvas.height = canvasDimensionLimit;\n }\n }\n}\nexport function canvasToBlob(canvas, options = {}) {\n if (canvas.toBlob) {\n return new Promise((resolve) => {\n canvas.toBlob(resolve, options.type ? options.type : 'image/png', options.quality ? options.quality : 1);\n });\n }\n return new Promise((resolve) => {\n const binaryString = window.atob(canvas\n .toDataURL(options.type ? options.type : undefined, options.quality ? options.quality : undefined)\n .split(',')[1]);\n const len = binaryString.length;\n const binaryArray = new Uint8Array(len);\n for (let i = 0; i < len; i += 1) {\n binaryArray[i] = binaryString.charCodeAt(i);\n }\n resolve(new Blob([binaryArray], {\n type: options.type ? options.type : 'image/png',\n }));\n });\n}\nexport function createImage(url) {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.decode = () => resolve(img);\n img.onload = () => resolve(img);\n img.onerror = reject;\n img.crossOrigin = 'anonymous';\n img.decoding = 'async';\n img.src = url;\n });\n}\nexport async function svgToDataURL(svg) {\n return Promise.resolve()\n .then(() => new XMLSerializer().serializeToString(svg))\n .then(encodeURIComponent)\n .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);\n}\nexport async function nodeToDataURL(node, width, height) {\n const xmlns = 'http://www.w3.org/2000/svg';\n const svg = document.createElementNS(xmlns, 'svg');\n const foreignObject = document.createElementNS(xmlns, 'foreignObject');\n svg.setAttribute('width', `${width}`);\n svg.setAttribute('height', `${height}`);\n svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n foreignObject.setAttribute('width', '100%');\n foreignObject.setAttribute('height', '100%');\n foreignObject.setAttribute('x', '0');\n foreignObject.setAttribute('y', '0');\n foreignObject.setAttribute('externalResourcesRequired', 'true');\n svg.appendChild(foreignObject);\n foreignObject.appendChild(node);\n return svgToDataURL(svg);\n}\nexport const isInstanceOfElement = (node, instance) => {\n if (node instanceof instance)\n return true;\n const nodePrototype = Object.getPrototypeOf(node);\n if (nodePrototype === null)\n return false;\n return (nodePrototype.constructor.name === instance.name ||\n isInstanceOfElement(nodePrototype, instance));\n};\n//# sourceMappingURL=util.js.map","import { uuid, toArray } from './util';\nfunction formatCSSText(style) {\n const content = style.getPropertyValue('content');\n return `${style.cssText} content: '${content.replace(/'|\"/g, '')}';`;\n}\nfunction formatCSSProperties(style) {\n return toArray(style)\n .map((name) => {\n const value = style.getPropertyValue(name);\n const priority = style.getPropertyPriority(name);\n return `${name}: ${value}${priority ? ' !important' : ''};`;\n })\n .join(' ');\n}\nfunction getPseudoElementStyle(className, pseudo, style) {\n const selector = `.${className}:${pseudo}`;\n const cssText = style.cssText\n ? formatCSSText(style)\n : formatCSSProperties(style);\n return document.createTextNode(`${selector}{${cssText}}`);\n}\nfunction clonePseudoElement(nativeNode, clonedNode, pseudo) {\n const style = window.getComputedStyle(nativeNode, pseudo);\n const content = style.getPropertyValue('content');\n if (content === '' || content === 'none') {\n return;\n }\n const className = uuid();\n try {\n clonedNode.className = `${clonedNode.className} ${className}`;\n }\n catch (err) {\n return;\n }\n const styleElement = document.createElement('style');\n styleElement.appendChild(getPseudoElementStyle(className, pseudo, style));\n clonedNode.appendChild(styleElement);\n}\nexport function clonePseudoElements(nativeNode, clonedNode) {\n clonePseudoElement(nativeNode, clonedNode, ':before');\n clonePseudoElement(nativeNode, clonedNode, ':after');\n}\n//# sourceMappingURL=clone-pseudos.js.map","const WOFF = 'application/font-woff';\nconst JPEG = 'image/jpeg';\nconst mimes = {\n woff: WOFF,\n woff2: WOFF,\n ttf: 'application/font-truetype',\n eot: 'application/vnd.ms-fontobject',\n png: 'image/png',\n jpg: JPEG,\n jpeg: JPEG,\n gif: 'image/gif',\n tiff: 'image/tiff',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\nfunction getExtension(url) {\n const match = /\\.([^./]*?)$/g.exec(url);\n return match ? match[1] : '';\n}\nexport function getMimeType(url) {\n const extension = getExtension(url).toLowerCase();\n return mimes[extension] || '';\n}\n//# sourceMappingURL=mimes.js.map","function getContentFromDataUrl(dataURL) {\n return dataURL.split(/,/)[1];\n}\nexport function isDataUrl(url) {\n return url.search(/^(data:)/) !== -1;\n}\nexport function makeDataUrl(content, mimeType) {\n return `data:${mimeType};base64,${content}`;\n}\nexport async function fetchAsDataURL(url, init, process) {\n const res = await fetch(url, init);\n if (res.status === 404) {\n throw new Error(`Resource \"${res.url}\" not found`);\n }\n const blob = await res.blob();\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onerror = reject;\n reader.onloadend = () => {\n try {\n resolve(process({ res, result: reader.result }));\n }\n catch (error) {\n reject(error);\n }\n };\n reader.readAsDataURL(blob);\n });\n}\nconst cache = {};\nfunction getCacheKey(url, contentType, includeQueryParams) {\n let key = url.replace(/\\?.*/, '');\n if (includeQueryParams) {\n key = url;\n }\n // font resource\n if (/ttf|otf|eot|woff2?/i.test(key)) {\n key = key.replace(/.*\\//, '');\n }\n return contentType ? `[${contentType}]${key}` : key;\n}\nexport async function resourceToDataURL(resourceUrl, contentType, options) {\n const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);\n if (cache[cacheKey] != null) {\n return cache[cacheKey];\n }\n // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache\n if (options.cacheBust) {\n // eslint-disable-next-line no-param-reassign\n resourceUrl += (/\\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();\n }\n let dataURL;\n try {\n const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {\n if (!contentType) {\n // eslint-disable-next-line no-param-reassign\n contentType = res.headers.get('Content-Type') || '';\n }\n return getContentFromDataUrl(result);\n });\n dataURL = makeDataUrl(content, contentType);\n }\n catch (error) {\n dataURL = options.imagePlaceholder || '';\n let msg = `Failed to fetch resource: ${resourceUrl}`;\n if (error) {\n msg = typeof error === 'string' ? error : error.message;\n }\n if (msg) {\n console.warn(msg);\n }\n }\n cache[cacheKey] = dataURL;\n return dataURL;\n}\n//# sourceMappingURL=dataurl.js.map","import { clonePseudoElements } from './clone-pseudos';\nimport { createImage, toArray, isInstanceOfElement } from './util';\nimport { getMimeType } from './mimes';\nimport { resourceToDataURL } from './dataurl';\nasync function cloneCanvasElement(canvas) {\n const dataURL = canvas.toDataURL();\n if (dataURL === 'data:,') {\n return canvas.cloneNode(false);\n }\n return createImage(dataURL);\n}\nasync function cloneVideoElement(video, options) {\n if (video.currentSrc) {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n canvas.width = video.clientWidth;\n canvas.height = video.clientHeight;\n ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n const dataURL = canvas.toDataURL();\n return createImage(dataURL);\n }\n const poster = video.poster;\n const contentType = getMimeType(poster);\n const dataURL = await resourceToDataURL(poster, contentType, options);\n return createImage(dataURL);\n}\nasync function cloneIFrameElement(iframe) {\n var _a;\n try {\n if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {\n return (await cloneNode(iframe.contentDocument.body, {}, true));\n }\n }\n catch (_b) {\n // Failed to clone iframe\n }\n return iframe.cloneNode(false);\n}\nasync function cloneSingleNode(node, options) {\n if (isInstanceOfElement(node, HTMLCanvasElement)) {\n return cloneCanvasElement(node);\n }\n if (isInstanceOfElement(node, HTMLVideoElement)) {\n return cloneVideoElement(node, options);\n }\n if (isInstanceOfElement(node, HTMLIFrameElement)) {\n return cloneIFrameElement(node);\n }\n return node.cloneNode(false);\n}\nconst isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === 'SLOT';\nasync function cloneChildren(nativeNode, clonedNode, options) {\n var _a, _b;\n let children = [];\n if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {\n children = toArray(nativeNode.assignedNodes());\n }\n else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) &&\n ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) {\n children = toArray(nativeNode.contentDocument.body.childNodes);\n }\n else {\n children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes);\n }\n if (children.length === 0 ||\n isInstanceOfElement(nativeNode, HTMLVideoElement)) {\n return clonedNode;\n }\n await children.reduce((deferred, child) => deferred\n .then(() => cloneNode(child, options))\n .then((clonedChild) => {\n if (clonedChild) {\n clonedNode.appendChild(clonedChild);\n }\n }), Promise.resolve());\n return clonedNode;\n}\nfunction cloneCSSStyle(nativeNode, clonedNode) {\n const targetStyle = clonedNode.style;\n if (!targetStyle) {\n return;\n }\n const sourceStyle = window.getComputedStyle(nativeNode);\n if (sourceStyle.cssText) {\n targetStyle.cssText = sourceStyle.cssText;\n targetStyle.transformOrigin = sourceStyle.transformOrigin;\n }\n else {\n toArray(sourceStyle).forEach((name) => {\n let value = sourceStyle.getPropertyValue(name);\n if (name === 'font-size' && value.endsWith('px')) {\n const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1;\n value = `${reducedFont}px`;\n }\n if (isInstanceOfElement(nativeNode, HTMLIFrameElement) &&\n name === 'display' &&\n value === 'inline') {\n value = 'block';\n }\n if (name === 'd' && clonedNode.getAttribute('d')) {\n value = `path(${clonedNode.getAttribute('d')})`;\n }\n targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name));\n });\n }\n}\nfunction cloneInputValue(nativeNode, clonedNode) {\n if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) {\n clonedNode.innerHTML = nativeNode.value;\n }\n if (isInstanceOfElement(nativeNode, HTMLInputElement)) {\n clonedNode.setAttribute('value', nativeNode.value);\n }\n}\nfunction cloneSelectValue(nativeNode, clonedNode) {\n if (isInstanceOfElement(nativeNode, HTMLSelectElement)) {\n const clonedSelect = clonedNode;\n const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute('value'));\n if (selectedOption) {\n selectedOption.setAttribute('selected', '');\n }\n }\n}\nfunction decorate(nativeNode, clonedNode) {\n if (isInstanceOfElement(clonedNode, Element)) {\n cloneCSSStyle(nativeNode, clonedNode);\n clonePseudoElements(nativeNode, clonedNode);\n cloneInputValue(nativeNode, clonedNode);\n cloneSelectValue(nativeNode, clonedNode);\n }\n return clonedNode;\n}\nasync function ensureSVGSymbols(clone, options) {\n const uses = clone.querySelectorAll ? clone.querySelectorAll('use') : [];\n if (uses.length === 0) {\n return clone;\n }\n const processedDefs = {};\n for (let i = 0; i < uses.length; i++) {\n const use = uses[i];\n const id = use.getAttribute('xlink:href');\n if (id) {\n const exist = clone.querySelector(id);\n const definition = document.querySelector(id);\n if (!exist && definition && !processedDefs[id]) {\n // eslint-disable-next-line no-await-in-loop\n processedDefs[id] = (await cloneNode(definition, options, true));\n }\n }\n }\n const nodes = Object.values(processedDefs);\n if (nodes.length) {\n const ns = 'http://www.w3.org/1999/xhtml';\n const svg = document.createElementNS(ns, 'svg');\n svg.setAttribute('xmlns', ns);\n svg.style.position = 'absolute';\n svg.style.width = '0';\n svg.style.height = '0';\n svg.style.overflow = 'hidden';\n svg.style.display = 'none';\n const defs = document.createElementNS(ns, 'defs');\n svg.appendChild(defs);\n for (let i = 0; i < nodes.length; i++) {\n defs.appendChild(nodes[i]);\n }\n clone.appendChild(svg);\n }\n return clone;\n}\nexport async function cloneNode(node, options, isRoot) {\n if (!isRoot && options.filter && !options.filter(node)) {\n return null;\n }\n return Promise.resolve(node)\n .then((clonedNode) => cloneSingleNode(clonedNode, options))\n .then((clonedNode) => cloneChildren(node, clonedNode, options))\n .then((clonedNode) => decorate(node, clonedNode))\n .then((clonedNode) => ensureSVGSymbols(clonedNode, options));\n}\n//# sourceMappingURL=clone-node.js.map","import { resolveUrl } from './util';\nimport { getMimeType } from './mimes';\nimport { isDataUrl, makeDataUrl, resourceToDataURL } from './dataurl';\nconst URL_REGEX = /url\\((['\"]?)([^'\"]+?)\\1\\)/g;\nconst URL_WITH_FORMAT_REGEX = /url\\([^)]+\\)\\s*format\\(([\"']?)([^\"']+)\\1\\)/g;\nconst FONT_SRC_REGEX = /src:\\s*(?:url\\([^)]+\\)\\s*format\\([^)]+\\)[,;]\\s*)+/g;\nfunction toRegex(url) {\n // eslint-disable-next-line no-useless-escape\n const escaped = url.replace(/([.*+?^${}()|\\[\\]\\/\\\\])/g, '\\\\$1');\n return new RegExp(`(url\\\\(['\"]?)(${escaped})(['\"]?\\\\))`, 'g');\n}\nexport function parseURLs(cssText) {\n const urls = [];\n cssText.replace(URL_REGEX, (raw, quotation, url) => {\n urls.push(url);\n return raw;\n });\n return urls.filter((url) => !isDataUrl(url));\n}\nexport async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) {\n try {\n const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL;\n const contentType = getMimeType(resourceURL);\n let dataURL;\n if (getContentFromUrl) {\n const content = await getContentFromUrl(resolvedURL);\n dataURL = makeDataUrl(content, contentType);\n }\n else {\n dataURL = await resourceToDataURL(resolvedURL, contentType, options);\n }\n return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`);\n }\n catch (error) {\n // pass\n }\n return cssText;\n}\nfunction filterPreferredFontFormat(str, { preferredFontFormat }) {\n return !preferredFontFormat\n ? str\n : str.replace(FONT_SRC_REGEX, (match) => {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || [];\n if (!format) {\n return '';\n }\n if (format === preferredFontFormat) {\n return `src: ${src};`;\n }\n }\n });\n}\nexport function shouldEmbed(url) {\n return url.search(URL_REGEX) !== -1;\n}\nexport async function embedResources(cssText, baseUrl, options) {\n if (!shouldEmbed(cssText)) {\n return cssText;\n }\n const filteredCSSText = filterPreferredFontFormat(cssText, options);\n const urls = parseURLs(filteredCSSText);\n return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText));\n}\n//# sourceMappingURL=embed-resources.js.map","import { embedResources } from './embed-resources';\nimport { toArray, isInstanceOfElement } from './util';\nimport { isDataUrl, resourceToDataURL } from './dataurl';\nimport { getMimeType } from './mimes';\nasync function embedProp(propName, node, options) {\n var _a;\n const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName);\n if (propValue) {\n const cssString = await embedResources(propValue, null, options);\n node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName));\n return true;\n }\n return false;\n}\nasync function embedBackground(clonedNode, options) {\n if (!(await embedProp('background', clonedNode, options))) {\n await embedProp('background-image', clonedNode, options);\n }\n if (!(await embedProp('mask', clonedNode, options))) {\n await embedProp('mask-image', clonedNode, options);\n }\n}\nasync function embedImageNode(clonedNode, options) {\n const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);\n if (!(isImageElement && !isDataUrl(clonedNode.src)) &&\n !(isInstanceOfElement(clonedNode, SVGImageElement) &&\n !isDataUrl(clonedNode.href.baseVal))) {\n return;\n }\n const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal;\n const dataURL = await resourceToDataURL(url, getMimeType(url), options);\n await new Promise((resolve, reject) => {\n clonedNode.onload = resolve;\n clonedNode.onerror = reject;\n const image = clonedNode;\n if (image.decode) {\n image.decode = resolve;\n }\n if (image.loading === 'lazy') {\n image.loading = 'eager';\n }\n if (isImageElement) {\n clonedNode.srcset = '';\n clonedNode.src = dataURL;\n }\n else {\n clonedNode.href.baseVal = dataURL;\n }\n });\n}\nasync function embedChildren(clonedNode, options) {\n const children = toArray(clonedNode.childNodes);\n const deferreds = children.map((child) => embedImages(child, options));\n await Promise.all(deferreds).then(() => clonedNode);\n}\nexport async function embedImages(clonedNode, options) {\n if (isInstanceOfElement(clonedNode, Element)) {\n await embedBackground(clonedNode, options);\n await embedImageNode(clonedNode, options);\n await embedChildren(clonedNode, options);\n }\n}\n//# sourceMappingURL=embed-images.js.map","export function applyStyle(node, options) {\n const { style } = node;\n if (options.backgroundColor) {\n style.backgroundColor = options.backgroundColor;\n }\n if (options.width) {\n style.width = `${options.width}px`;\n }\n if (options.height) {\n style.height = `${options.height}px`;\n }\n const manual = options.style;\n if (manual != null) {\n Object.keys(manual).forEach((key) => {\n style[key] = manual[key];\n });\n }\n return node;\n}\n//# sourceMappingURL=apply-style.js.map","import { toArray } from './util';\nimport { fetchAsDataURL } from './dataurl';\nimport { shouldEmbed, embedResources } from './embed-resources';\nconst cssFetchCache = {};\nasync function fetchCSS(url) {\n let cache = cssFetchCache[url];\n if (cache != null) {\n return cache;\n }\n const res = await fetch(url);\n const cssText = await res.text();\n cache = { url, cssText };\n cssFetchCache[url] = cache;\n return cache;\n}\nasync function embedFonts(data, options) {\n let cssText = data.cssText;\n const regexUrl = /url\\([\"']?([^\"')]+)[\"']?\\)/g;\n const fontLocs = cssText.match(/url\\([^)]+\\)/g) || [];\n const loadFonts = fontLocs.map(async (loc) => {\n let url = loc.replace(regexUrl, '$1');\n if (!url.startsWith('https://')) {\n url = new URL(url, data.url).href;\n }\n return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {\n cssText = cssText.replace(loc, `url(${result})`);\n return [loc, result];\n });\n });\n return Promise.all(loadFonts).then(() => cssText);\n}\nfunction parseCSS(source) {\n if (source == null) {\n return [];\n }\n const result = [];\n const commentsRegex = /(\\/\\*[\\s\\S]*?\\*\\/)/gi;\n // strip out comments\n let cssText = source.replace(commentsRegex, '');\n // eslint-disable-next-line prefer-regex-literals\n const keyframesRegex = new RegExp('((@.*?keyframes [\\\\s\\\\S]*?){([\\\\s\\\\S]*?}\\\\s*?)})', 'gi');\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const matches = keyframesRegex.exec(cssText);\n if (matches === null) {\n break;\n }\n result.push(matches[0]);\n }\n cssText = cssText.replace(keyframesRegex, '');\n const importRegex = /@import[\\s\\S]*?url\\([^)]*\\)[\\s\\S]*?;/gi;\n // to match css & media queries together\n const combinedCSSRegex = '((\\\\s*?(?:\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\/)?\\\\s*?@media[\\\\s\\\\S]' +\n '*?){([\\\\s\\\\S]*?)}\\\\s*?})|(([\\\\s\\\\S]*?){([\\\\s\\\\S]*?)})';\n // unified regex\n const unifiedRegex = new RegExp(combinedCSSRegex, 'gi');\n // eslint-disable-next-line no-constant-condition\n while (true) {\n let matches = importRegex.exec(cssText);\n if (matches === null) {\n matches = unifiedRegex.exec(cssText);\n if (matches === null) {\n break;\n }\n else {\n importRegex.lastIndex = unifiedRegex.lastIndex;\n }\n }\n else {\n unifiedRegex.lastIndex = importRegex.lastIndex;\n }\n result.push(matches[0]);\n }\n return result;\n}\nasync function getCSSRules(styleSheets, options) {\n const ret = [];\n const deferreds = [];\n // First loop inlines imports\n styleSheets.forEach((sheet) => {\n if ('cssRules' in sheet) {\n try {\n toArray(sheet.cssRules || []).forEach((item, index) => {\n if (item.type === CSSRule.IMPORT_RULE) {\n let importIndex = index + 1;\n const url = item.href;\n const deferred = fetchCSS(url)\n .then((metadata) => embedFonts(metadata, options))\n .then((cssText) => parseCSS(cssText).forEach((rule) => {\n try {\n sheet.insertRule(rule, rule.startsWith('@import')\n ? (importIndex += 1)\n : sheet.cssRules.length);\n }\n catch (error) {\n console.error('Error inserting rule from remote css', {\n rule,\n error,\n });\n }\n }))\n .catch((e) => {\n console.error('Error loading remote css', e.toString());\n });\n deferreds.push(deferred);\n }\n });\n }\n catch (e) {\n const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];\n if (sheet.href != null) {\n deferreds.push(fetchCSS(sheet.href)\n .then((metadata) => embedFonts(metadata, options))\n .then((cssText) => parseCSS(cssText).forEach((rule) => {\n inline.insertRule(rule, sheet.cssRules.length);\n }))\n .catch((err) => {\n console.error('Error loading remote stylesheet', err);\n }));\n }\n console.error('Error inlining remote css file', e);\n }\n }\n });\n return Promise.all(deferreds).then(() => {\n // Second loop parses rules\n styleSheets.forEach((sheet) => {\n if ('cssRules' in sheet) {\n try {\n toArray(sheet.cssRules || []).forEach((item) => {\n ret.push(item);\n });\n }\n catch (e) {\n console.error(`Error while reading CSS rules from ${sheet.href}`, e);\n }\n }\n });\n return ret;\n });\n}\nfunction getWebFontRules(cssRules) {\n return cssRules\n .filter((rule) => rule.type === CSSRule.FONT_FACE_RULE)\n .filter((rule) => shouldEmbed(rule.style.getPropertyValue('src')));\n}\nasync function parseWebFontRules(node, options) {\n if (node.ownerDocument == null) {\n throw new Error('Provided element is not within a Document');\n }\n const styleSheets = toArray(node.ownerDocument.styleSheets);\n const cssRules = await getCSSRules(styleSheets, options);\n return getWebFontRules(cssRules);\n}\nexport async function getWebFontCSS(node, options) {\n const rules = await parseWebFontRules(node, options);\n const cssTexts = await Promise.all(rules.map((rule) => {\n const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;\n return embedResources(rule.cssText, baseUrl, options);\n }));\n return cssTexts.join('\\n');\n}\nexport async function embedWebFonts(clonedNode, options) {\n const cssText = options.fontEmbedCSS != null\n ? options.fontEmbedCSS\n : options.skipFonts\n ? null\n : await getWebFontCSS(clonedNode, options);\n if (cssText) {\n const styleNode = document.createElement('style');\n const sytleContent = document.createTextNode(cssText);\n styleNode.appendChild(sytleContent);\n if (clonedNode.firstChild) {\n clonedNode.insertBefore(styleNode, clonedNode.firstChild);\n }\n else {\n clonedNode.appendChild(styleNode);\n }\n }\n}\n//# sourceMappingURL=embed-webfonts.js.map","import { cloneNode } from './clone-node';\nimport { embedImages } from './embed-images';\nimport { applyStyle } from './apply-style';\nimport { embedWebFonts, getWebFontCSS } from './embed-webfonts';\nimport { getImageSize, getPixelRatio, createImage, canvasToBlob, nodeToDataURL, checkCanvasDimensions, } from './util';\nexport async function toSvg(node, options = {}) {\n const { width, height } = getImageSize(node, options);\n const clonedNode = (await cloneNode(node, options, true));\n await embedWebFonts(clonedNode, options);\n await embedImages(clonedNode, options);\n applyStyle(clonedNode, options);\n const datauri = await nodeToDataURL(clonedNode, width, height);\n return datauri;\n}\nexport async function toCanvas(node, options = {}) {\n const { width, height } = getImageSize(node, options);\n const svg = await toSvg(node, options);\n const img = await createImage(svg);\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n const ratio = options.pixelRatio || getPixelRatio();\n const canvasWidth = options.canvasWidth || width;\n const canvasHeight = options.canvasHeight || height;\n canvas.width = canvasWidth * ratio;\n canvas.height = canvasHeight * ratio;\n if (!options.skipAutoScale) {\n checkCanvasDimensions(canvas);\n }\n canvas.style.width = `${canvasWidth}`;\n canvas.style.height = `${canvasHeight}`;\n if (options.backgroundColor) {\n context.fillStyle = options.backgroundColor;\n context.fillRect(0, 0, canvas.width, canvas.height);\n }\n context.drawImage(img, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\nexport async function toPixelData(node, options = {}) {\n const { width, height } = getImageSize(node, options);\n const canvas = await toCanvas(node, options);\n const ctx = canvas.getContext('2d');\n return ctx.getImageData(0, 0, width, height).data;\n}\nexport async function toPng(node, options = {}) {\n const canvas = await toCanvas(node, options);\n return canvas.toDataURL();\n}\nexport async function toJpeg(node, options = {}) {\n const canvas = await toCanvas(node, options);\n return canvas.toDataURL('image/jpeg', options.quality || 1);\n}\nexport async function toBlob(node, options = {}) {\n const canvas = await toCanvas(node, options);\n const blob = await canvasToBlob(canvas);\n return blob;\n}\nexport async function getFontEmbedCSS(node, options = {}) {\n return getWebFontCSS(node, options);\n}\n//# sourceMappingURL=index.js.map","export interface ScrollmeterTimelineOptions {\n color?: string\n width?: number\n}\n\nexport interface ScrollmeterBarOptions {\n color?: string\n background?: string\n height?: number\n}\n\nexport interface ScrollmeterTooltipOptions {\n background?: string\n fontColor?: string\n fontSize?: number\n paddingBlock?: number\n paddingInline?: number\n width?: number\n}\n\nexport interface ScrollmeterOptions {\n targetId?: string // @deprecated - No longer in use.\n target?: string | HTMLElement\n useTimeline?: boolean\n useTooltip?: boolean\n usePreview?: boolean\n barOptions?: ScrollmeterBarOptions\n timelineOptions?: ScrollmeterTimelineOptions\n tooltipOptions?: ScrollmeterTooltipOptions\n}\n\nexport abstract class IScrollmeter {\n protected abstract setCSSCustomProperties(): void\n}\n","import styles from '../styles/scrollmeter.module.scss'\nimport { IScrollmeter } from '../types/scrollmeter.types'\nimport { Scrollmeter } from './scrollmeter'\n\nexport class ScrollmeterTooltip extends IScrollmeter {\n #scrollmeter: Scrollmeter\n\n constructor(scrollmeter: Scrollmeter) {\n super()\n this.#scrollmeter = scrollmeter\n }\n\n #cropImageAtPercent = (targetElement: HTMLElement, cropWidth: number = 320) => {\n const captureCanvas = this.#scrollmeter.getCaptureCanvas()\n const ratio = this.#scrollmeter.getCanvasRatio()\n\n if (!captureCanvas) return\n\n const canvasWidth = captureCanvas.width\n const canvasHeight = (canvasWidth * 9) / 16 // 16:9 비율 계산\n const y = Math.max(0, targetElement.getBoundingClientRect().top * ratio + window.scrollY * ratio - canvasHeight / 2)\n\n const cropHeight = (cropWidth * 9) / 16 // 16:9 비율 계산\n\n const tempCanvas = document.createElement('canvas')\n tempCanvas.width = cropWidth\n tempCanvas.height = cropHeight\n\n const ctx = tempCanvas.getContext('2d')\n if (!ctx) return null\n\n // 크롭된 영역 그리기\n ctx.drawImage(\n captureCanvas,\n 0,\n Math.max(0, Math.min(y, captureCanvas.height - canvasHeight)), // y값 범위 제한\n canvasWidth,\n canvasHeight,\n 0,\n 0,\n cropWidth,\n cropHeight\n )\n\n return tempCanvas.toDataURL()\n }\n\n #createPreview = (dataUrl: string) => {\n const div = document.createElement('div')\n div.classList.add(styles.scrollmeter_timeline_preview)\n\n const img = new Image()\n\n img.src = dataUrl\n\n div.appendChild(img)\n return div\n }\n\n public createTimelineTooltip = (\n timelineElement: HTMLDivElement,\n targetElement: HTMLElement,\n direction: 'left' | 'right' | 'center'\n ) => {\n if (!targetElement.textContent) return\n const timelineTooltip = document.createElement('div')\n const timelineTooltipText = document.createElement('p')\n\n if (this.#scrollmeter.getDefaultOptions().usePreview) {\n const dataUrl = this.#cropImageAtPercent(targetElement)\n\n if (dataUrl) {\n const preview = this.#createPreview(dataUrl)\n timelineTooltip.appendChild(preview)\n }\n }\n\n timelineTooltip.classList.add(styles.scrollmeter_timeline_tooltip)\n timelineTooltip.classList.add(styles[`scrollmeter_timeline_tooltip_${direction}`])\n\n timelineTooltipText.textContent = targetElement.textContent\n\n timelineTooltip.appendChild(timelineTooltipText)\n\n this.setCSSCustomProperties()\n\n timelineElement.appendChild(timelineTooltip)\n\n return timelineTooltip\n }\n\n protected setCSSCustomProperties() {\n const defaultOptions = this.#scrollmeter.getDefaultOptions()\n\n // css custom\n if (defaultOptions && defaultOptions.tooltipOptions) {\n const { background, fontColor, fontSize, paddingBlock, paddingInline, width } = defaultOptions.tooltipOptions\n\n if (background) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-background', background)\n }\n if (fontColor) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-font-color', fontColor)\n }\n if (fontSize) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-font-size', `${fontSize}px`)\n }\n if (paddingBlock) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-padding-block', `${paddingBlock}px`)\n }\n if (paddingInline) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-padding-inline', `${paddingInline}px`)\n }\n if (width) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-tooltip-width', `${width}px`)\n }\n }\n }\n}\n","import styles from '../styles/scrollmeter.module.scss'\nimport { IScrollmeter } from '../types/scrollmeter.types'\nimport { Scrollmeter } from './scrollmeter'\nimport { ScrollmeterTooltip } from './scrollmeter-tooltip'\n\nexport class ScrollmeterTimeline extends IScrollmeter {\n #scrollmeter: Scrollmeter\n\n constructor(scrollmeter: Scrollmeter) {\n super()\n this.#scrollmeter = scrollmeter\n }\n\n #findTimelineElements = (element: HTMLElement): HTMLElement[] => {\n const elArray: HTMLElement[] = []\n\n const searchH1 = (el: HTMLElement) => {\n if (el.tagName.toLowerCase() === 'h1') {\n if (this.#isElementVisible(el)) {\n elArray.push(el as HTMLHeadingElement)\n }\n }\n\n Array.from(el.children).forEach((child) => {\n searchH1(child as HTMLElement)\n })\n }\n\n searchH1(element)\n\n return elArray\n }\n\n #isElementVisible(element: HTMLElement): boolean {\n // 요소 자체나 부모 요소들의 style 체크\n const style = window.getComputedStyle(element)\n if (style.display === 'none') return false\n if (style.visibility === 'hidden') return false\n if (style.opacity === '0') return false\n\n // 부모 요소들도 순차적으로 확인\n let currentElement: HTMLElement | null = element.parentElement\n while (currentElement) {\n const parentStyle = window.getComputedStyle(currentElement)\n if (parentStyle.display === 'none') return false\n if (parentStyle.visibility === 'hidden') return false\n if (parentStyle.opacity === '0') return false\n currentElement = currentElement.parentElement\n }\n\n return true\n }\n\n public createTimeline = (highestZIndex: number): ScrollmeterTimeline => {\n const targetContainer = this.#scrollmeter.getTargetContainer()\n if (!targetContainer) return null\n\n const targetElement = this.#findTimelineElements(targetContainer)\n\n if (targetElement.length === 0) return null\n\n const timelineElements: HTMLElement[] = []\n const timelineWidth = this.#scrollmeter.getDefaultOptions().timelineOptions?.width ?? 4\n let outOfBoundIndex = targetElement.length\n\n targetElement.map((element) => {\n const scrollContainer = this.#scrollmeter.getTargetContainer()\n\n if (!scrollContainer) return\n\n const timelineElement = document.createElement('div')\n timelineElement.classList.add(styles.scrollmeter_timeline)\n\n const absoluteElementTop = element.getBoundingClientRect().top + window.scrollY\n const absoluteContainerTop = scrollContainer.getBoundingClientRect().top + window.scrollY\n const relativeTargetTop = absoluteElementTop - absoluteContainerTop\n const scrollableHeight = scrollContainer.clientHeight - document.documentElement.clientHeight\n\n timelineElement.style.zIndex = highestZIndex.toString()\n\n timelineElement.addEventListener('pointerdown', () => {\n element.scrollIntoView({ behavior: 'smooth' })\n })\n\n if (scrollableHeight > absoluteElementTop) {\n const relativePosition = (relativeTargetTop / scrollableHeight) * 100\n\n timelineElement.style.left = `${relativePosition > 100 ? `calc(100% - ${timelineWidth}px)` : `${relativePosition}%`}`\n\n if (this.#scrollmeter.getDefaultOptions().useTooltip) {\n const tooltip = new ScrollmeterTooltip(this.#scrollmeter)\n\n tooltip.createTimelineTooltip(\n timelineElement,\n element,\n relativePosition <= 16 ? 'left' : relativePosition >= 83 ? 'right' : 'center'\n )\n }\n } else {\n timelineElement.style.left = `calc(100% - ${timelineWidth * (outOfBoundIndex-- * 4)}px)`\n\n if (this.#scrollmeter.getDefaultOptions().useTooltip) {\n const tooltip = new ScrollmeterTooltip(this.#scrollmeter)\n\n const tooltipElement = tooltip.createTimelineTooltip(timelineElement, element, 'right')\n\n tooltipElement.addEventListener('touchstart', function () {\n tooltipElement.style.visibility = 'visible'\n tooltipElement.style.opacity = '1'\n\n setTimeout(() => {\n tooltipElement.style.visibility = 'hidden'\n tooltipElement.style.opacity = '0'\n }, 1000)\n })\n }\n }\n\n this.#scrollmeter.getScrollmeterContainer()?.appendChild(timelineElement)\n timelineElements.push(timelineElement)\n })\n\n this.setCSSCustomProperties()\n\n return this\n }\n\n public setCSSCustomProperties() {\n const defaultOptions = this.#scrollmeter.getDefaultOptions()\n // css custom\n if (defaultOptions && defaultOptions.timelineOptions) {\n const { color, width } = defaultOptions.timelineOptions\n\n if (color) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-timeline-color', color)\n }\n if (width) {\n this.#scrollmeter.getScrollmeterContainer()?.style.setProperty('--scrollmeter-timeline-width', `${width}px`)\n }\n }\n }\n}\n","import * as htmlToImage from 'html-to-image'\nimport styles from '../styles/scrollmeter.module.scss'\nimport { IScrollmeter, ScrollmeterOptions } from '../types/scrollmeter.types'\nimport { ScrollmeterTimeline } from './scrollmeter-timeline'\n\nexport class Scrollmeter extends IScrollmeter {\n #defaultOptions: ScrollmeterOptions\n #targetContainer: HTMLElement | null\n #scrollmeterContainer: HTMLDivElement | null\n #scrollmeterBar: HTMLDivElement | null\n #resizeObserver: ResizeObserver | null\n\n #timelineElements: ScrollmeterTimeline | null\n\n #captureCanvas: HTMLCanvasElement | null\n\n #containerHeight: number\n #barWidth: number\n #totalHeight: number\n #elementTop: number\n #highestZIndex: number\n\n #docWidth: number\n #canvasWidth: number\n\n #isInView: boolean\n\n constructor(options: ScrollmeterOptions) {\n super()\n const { targetId, target } = options\n this.#defaultOptions = options\n\n this.#targetContainer = targetId\n ? document.getElementById(targetId)\n : typeof target === 'string'\n ? document.getElementById(target)\n : (target ?? null)\n this.#scrollmeterContainer = null\n this.#scrollmeterBar = null\n this.#resizeObserver = null\n this.#captureCanvas = null\n this.#timelineElements = null\n\n // 숫자 필드 초기화\n this.#containerHeight = 0\n this.#barWidth = 0\n this.#totalHeight = 0\n this.#elementTop = 0\n this.#highestZIndex = 0\n this.#docWidth = 0\n this.#canvasWidth = 0\n\n this.#isInView = false\n\n this.#initResizeObserver()\n\n this.#createScrollmeter()\n }\n\n #initResizeObserver = () => {\n if (!this.#targetContainer) {\n throw new Error('targetContainer is not found')\n }\n\n this.#resizeObserver = new ResizeObserver(async (entries) => {\n if (!this.#targetContainer) return\n\n if (!this.#scrollmeterContainer || this.#containerHeight === entries[0].contentRect.height) return\n\n this.#containerHeight = entries[0].contentRect.height\n\n this.render(this.#defaultOptions)\n })\n }\n\n #createScrollmeterContainer = () => {\n try {\n if (!this.#targetContainer) throw new Error('targetContainer is not found')\n\n const scrollmeterContainer = document.createElement('div') as HTMLDivElement\n scrollmeterContainer.classList.add(styles.scrollmeter_container)\n\n const highestZIndex = this.#findHighestZIndex(this.#targetContainer)\n this.#highestZIndex = highestZIndex\n scrollmeterContainer.style.zIndex = highestZIndex.toString()\n\n const scrollmeterBar = this.#createScrollmeterBar()\n scrollmeterContainer.appendChild(scrollmeterBar)\n\n this.#scrollmeterContainer = scrollmeterContainer\n\n this.setCSSCustomProperties()\n\n return scrollmeterContainer\n } catch (error) {\n console.error(error)\n }\n }\n\n #createScrollmeterBar = () => {\n const scrollmeterBar = document.createElement('div')\n scrollmeterBar.classList.add(styles.scrollmeter_bar)\n\n this.#scrollmeterBar = scrollmeterBar\n\n return scrollmeterBar\n }\n\n #findHighestZIndex = (element: HTMLElement) => {\n let highest = 0\n\n const zIndex = window.getComputedStyle(element).zIndex\n\n if (zIndex !== 'auto') {\n highest = Math.max(highest, parseInt(zIndex))\n }\n\n Array.from(element.children).forEach((child) => {\n highest = Math.max(highest, this.#findHighestZIndex(child as HTMLElement))\n })\n\n return highest + 1\n }\n\n #updateBarWidth = () => {\n if (!this.#targetContainer) return\n if (!this.#isInView) return\n\n const isVisibleScrollmeter = this.#isVisibleScrollmeter()\n\n if (!isVisibleScrollmeter) {\n this.#scrollmeterContainer!.style.opacity = '0'\n return\n }\n\n this.#scrollmeterContainer!.style.opacity = '1'\n\n const currentScroll = window.scrollY - this.#elementTop\n const scrollPercentage = (currentScroll / this.#totalHeight) * 100\n\n