/* * XmlDigitalTeaching v0.0.1 * Copyright ©Thu Jul 24 2025 15:16:57 GMT+0800 (中国标准时间) smile * Released under the ISC License. */ import crypto from 'crypto'; import Vue from 'vue'; import lodash from 'lodash'; let componentNamespace = 'xml'; // 组件前缀命名只能更新一次,防止重复执行修改组件注册出错 let isUpdated = false; // scss 命名不能通过 js 更改,在 SCSS 编译已经打包好了 const cssNamespace = 'xml'; const statePrefix = 'is-'; function createNamespace$1(name, { prefix, isUnPrefix }) { let useNamespace; if (!isUpdated) { isUpdated = true; useNamespace = prefix || componentNamespace; // 更改前缀 } if (prefix) { if (name.indexOf(componentNamespace) === 0) { return name.replace(componentNamespace, prefix); } return useNamespace.charAt(0).toUpperCase() + useNamespace.substr(1) + name; } if (name.indexOf(componentNamespace) === 0 || name.indexOf(componentNamespace.charAt(0).toUpperCase()) === 0) { return name.charAt(0).toUpperCase() + name.substr(1); } return isUnPrefix ? name.charAt(0).toLowerCase() + name.substr(1) : componentNamespace + name.charAt(0).toUpperCase() + name.substr(1); } /** * 生成 bem * @param {} namespace 命名空间 * @param {*} block 块 * @param {*} blockSuffix 块多个单词 * @param {*} element 元素 * @param {*} modifier 修饰符 * @returns */ const _bem = (namespace, block, blockSuffix, element, modifier) => { let cls = `${namespace}-${block}`; if (blockSuffix) { cls += `-${blockSuffix}`; } if (element) { cls += `__${element}`; } if (modifier) { cls += `--${modifier}`; } return cls; }; const useNamespace = (block, namespace = cssNamespace) => { const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', ''); const e = element => element ? _bem(namespace, block, '', element, '') : ''; const m = modifier => modifier ? _bem(namespace, block, '', '', modifier) : ''; const be = (blockSuffix, element) => blockSuffix && element ? _bem(namespace, block, blockSuffix, element, '') : ''; const em = (element, modifier) => element && modifier ? _bem(namespace, block, '', element, modifier) : ''; const bm = (blockSuffix, modifier) => blockSuffix && modifier ? _bem(namespace, block, blockSuffix, '', modifier) : ''; const bem = (blockSuffix, element, modifier) => blockSuffix && element && modifier ? _bem(namespace, block, blockSuffix, element, modifier) : ''; const is = (name, ...args) => { const state = args.length >= 1 ? args[0] : true; return name && state ? `${statePrefix}${name}` : ''; }; // for css var // --el-xxx: value; const cssVar = object => { const styles = {}; for (const key in object) { styles[`--${namespace}-${key}`] = object[key]; } return styles; }; // with block const cssVarBlock = object => { const styles = {}; for (const key in object) { styles[`--${namespace}-${block}-${key}`] = object[key]; } return styles; }; const cssVarName = name => `--${namespace}-${name}`; const cssVarBlockName = name => `--${namespace}-${block}-${name}`; return { cssNamespace, componentNamespace, b, e, m, be, em, bm, bem, is, // css cssVar, cssVarName, cssVarBlock, cssVarBlockName }; }; // 样式资源包 let chalk = ''; // 默认主题 let defaultTheme = '#409EFF'; const tintColor = (color, tint) => { color = color.replace('#', ''); let red = parseInt(color.slice(0, 2), 16); let green = parseInt(color.slice(2, 4), 16); let blue = parseInt(color.slice(4, 6), 16); if (tint === 0) { // when primary color is in its rgb space return [red, green, blue].join(','); } else { red += Math.round(tint * (255 - red)); green += Math.round(tint * (255 - green)); blue += Math.round(tint * (255 - blue)); red = red.toString(16); green = green.toString(16); blue = blue.toString(16); console.log('tintColor return', color, tint, `#${red}${green}${blue}`); return `#${red}${green}${blue}`; } }; const shadeColor = (color, shade) => { let red = parseInt(color.slice(0, 2), 16); let green = parseInt(color.slice(2, 4), 16); let blue = parseInt(color.slice(4, 6), 16); red = Math.round((1 - shade) * red); green = Math.round((1 - shade) * green); blue = Math.round((1 - shade) * blue); red = red.toString(16); green = green.toString(16); blue = blue.toString(16); console.log('shadeColor return', color, shade, `#${red}${green}${blue}`); return `#${red}${green}${blue}`; }; const getThemeCluster = function (theme) { const clusters = [theme]; for (let i = 0; i <= 9; i++) { clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))); } clusters.push(shadeColor(theme, 0.1)); console.log('getThemeCluster return', theme, clusters); return clusters; }; const getCSSString = function (url) { return new Promise(resolve => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { chalk = xhr.responseText.replace(/@font-face{[^}]+}/, ''); console.log('getCSSString: chalk', chalk); resolve(); } }; xhr.onerror = err => { console.error('样式下载失败', err); }; xhr.open('GET', url); xhr.send(); }); }; const updateStyle$1 = function (style, oldCluster, newCluster) { let newStyle = style; oldCluster.forEach((color, index) => { newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index]); }); console.log('updateStyle: newStyle', newStyle); return newStyle; }; const updateElementTheme = async function (options = {}) { const { version = '2.15.8', oldTheme, primaryColor, appendDom, insertBefore, cssUrl, chalkStyle = 'chalk-style' } = options; if (typeof primaryColor !== 'string') return; const themeCluster = getThemeCluster(primaryColor.replace('#', '')); const originalCluster = getThemeCluster((oldTheme || defaultTheme).replace('#', '')); const chalkHandler = id => { const newStyle = updateStyle$1(chalk, originalCluster, themeCluster); // 覆盖原来的样式 chalk = newStyle; let styleTag = document.querySelector(id); if (!styleTag) { styleTag = document.createElement('style'); styleTag.setAttribute('id', id); if (appendDom) { if (insertBefore) { appendDom.parentNode.insertBefore(styleTag, appendDom.nextSibling); } else { appendDom.appendChild(styleTag); } } else { document.head.appendChild(styleTag); } } styleTag.innerText = newStyle; }; if (!chalk) { const url = cssUrl || `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`; await getCSSString(url); } chalkHandler(chalkStyle); defaultTheme = primaryColor; }; const updateUITheme = async function (options) { const { oldTheme = '#409EFF', primaryColor, primarySecondColor, opacity = 0.7, elementUI, button = { disabledFontColorPrimary: '#fff', disabledBgColorPrimary: '#ccc', disabledFontColorPlain: '#999', disabledBgColorPlain: '#f5f5f5', disabledFontColorGhost: '#999', disabledBorderColorGhost: '#ccc' } } = options; if (!primaryColor) return; if (elementUI) { try { await updateElementTheme({ oldTheme, primaryColor: primaryColor }); } catch (err) { console.error(err); } } // const primaryOpc = colorPalette(primary, opacity) // 第二种主题透明 const primarySecOpc = primarySecondColor ? tintColor(primarySecondColor, Number((1 - opacity).toFixed(2))) : ''; // const primaryDis = colorPalette(primary, disOpacity) // const primarySecDis = colorPalette(primarySecondColor, disOpacity) const primaryLightColors = []; const el = document.documentElement; el.style.setProperty(`--${cssNamespace}-color-primary`, primaryColor); for (let i = 1; i <= 9; i++) { const color = tintColor(primaryColor, Number((0.1 * i).toFixed(2))); primaryLightColors.push(color); el.style.setProperty(`--${cssNamespace}-color-primary-light-${i}`, color); } // disabled primary el.style.setProperty(`--${cssNamespace}-button-disabled-font-color-primary`, button.disabledFontColorPrimary); el.style.setProperty(`--${cssNamespace}-button-disabled-bg-color-primary`, button.disabledBgColorPrimary); el.style.setProperty(`--${cssNamespace}-button-disabled-border-color-primary`, button.disabledBgColorPrimary); // plain el.style.setProperty(`--${cssNamespace}-button-disabled-font-color-primary-plain`, button.disabledFontColorPlain); el.style.setProperty(`--${cssNamespace}-button-disabled-bg-color-primary-plain`, button.disabledBgColorPlain); // ghost el.style.setProperty(`--${cssNamespace}-button-disabled-font-color-primary-ghost`, button.disabledFontColorGhost); el.style.setProperty(`--${cssNamespace}-button-disabled-border-color-primary-ghost`, button.disabledBorderColorGhost); const buttonTheme = { primary: { 'button-font-color': primarySecondColor ? primaryColor : '#fff', 'button-bg-color': primarySecondColor ? primarySecondColor : primaryColor, 'button-hover-font-color': primarySecondColor ? primaryColor : '#fff', 'button-hover-bg-color': primarySecondColor ? primarySecOpc : primaryLightColors[1], 'button-active-font-color': primarySecondColor ? primaryColor : '#fff', 'button-active-bg-color': primarySecondColor ? primarySecondColor : primaryColor }, plain: { 'button-font-color-primary': primarySecondColor ? primarySecondColor : primaryColor, 'button-bg-color-primary': primarySecondColor ? primaryLightColors[3] : primaryLightColors[7], 'button-hover-font-color-primary': primarySecondColor ? primarySecondColor : primaryColor, 'button-hover-bg-color-primary': primarySecondColor ? primaryLightColors[4] : primaryLightColors[7], 'button-active-font-color-primary': primarySecondColor ? primarySecondColor : primaryColor, 'button-active-bg-color-primary': primarySecondColor ? primaryLightColors[3] : primaryLightColors[7] } }; Object.keys(buttonTheme).forEach(type => { const item = buttonTheme[type]; Object.keys(item).forEach(property => { el.style.setProperty(`--${cssNamespace}-${property}-${type}`, item[property]); }); }); }; const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate let poolPtr = rnds8Pool.length; function rng() { if (poolPtr > rnds8Pool.length - 16) { crypto.randomFillSync(rnds8Pool); poolPtr = 0; } return rnds8Pool.slice(poolPtr, poolPtr += 16); } var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; function validate(uuid) { return typeof uuid === 'string' && REGEX.test(uuid); } /** * Convert array of 16 byte values to UUID string format of the form: * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX */ const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).substr(1)); } function stringify$2(arr, offset = 0) { // Note: Be careful editing this code! It's been tuned for performance // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one // of the following: // - One or more input array values don't map to a hex octet (leading to // "undefined" in the uuid) // - Invalid input values for the RFC `version` or `variant` fields if (!validate(uuid)) { throw TypeError('Stringified UUID is invalid'); } return uuid; } function v4(options, buf, offset) { options = options || {}; const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = rnds[6] & 0x0f | 0x40; rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided if (buf) { offset = offset || 0; for (let i = 0; i < 16; ++i) { buf[offset + i] = rnds[i]; } return buf; } return stringify$2(rnds); } function findNodeTree(node, storeKeyPrefix) { let nodeModels = localStorage.getItem(storeKeyPrefix + 'g-nodes') ? JSON.parse(localStorage.getItem(storeKeyPrefix + 'g-nodes')) : []; let relations = localStorage.getItem(storeKeyPrefix + 'g-relations') ? JSON.parse(localStorage.getItem(storeKeyPrefix + 'g-relations')) : []; let nodeEndRelations = []; let nodeStartRelations = []; let nodeExtraRelations = []; let nodeIds = []; let subNodeIds = []; let nodeId = String(node.id); nodeIds.push(nodeId); function nodeDeepFind(n, originId, level) { let result = relations.filter(item => item.start + '' === n.end + ''); if (result.length) { result.forEach(item => { if (item.end + '' !== originId) { nodeIds.push(item.end + ''); if (!nodeExtraRelations.find(n => n.id === item.id)) { // nodeExtraRelations.push({ ...item, renderType: 'leaf' }) nodeExtraRelations.push({ ...item }); } if (item.end + '' !== n.start + '' && (level === 0 || level > 0 && nodeExtraRelations.find(r => r.start + '' === item.start + '' && r.end + '' === item.end + '' && r.type === item.type) === undefined)) { level += 1; nodeDeepFind(item, originId, level); } } }); } } for (let i = 0, len = relations.length; i < len; i++) { let r = relations[i]; if (r.end + '' === nodeId) { nodeEndRelations.push(r); nodeIds.push(r.start + ''); subNodeIds.push(r.start + ''); } else if (r.start + '' === nodeId) { nodeIds.push(r.end + ''); subNodeIds.push(r.end + ''); nodeStartRelations.push(r); nodeDeepFind(r, nodeId, 0); } } nodeIds = [...new Set(nodeIds)]; let nodes = nodeModels.filter(item => nodeIds.includes(item.id + '')); let links = [...nodeStartRelations, ...nodeEndRelations, ...nodeExtraRelations]; return { ...node, nodes, links, subNodeIds }; } function genBookGraphModel(textBookName, storeKeyPrefix, level = 2) { let nodeModels = localStorage.getItem(storeKeyPrefix + 'g-nodes') ? JSON.parse(localStorage.getItem(storeKeyPrefix + 'g-nodes')) : []; let relations = localStorage.getItem(storeKeyPrefix + 'g-relations') ? JSON.parse(localStorage.getItem(storeKeyPrefix + 'g-relations')) : []; let subNodeIds = []; let mainNode; const nodeChain = ['major', 'course', 'chapter', 'section', 'subsection', 'knowledge', 'catalog']; for (let i = 0, len = nodeChain.length; i < len; i++) { let type = nodeChain[i]; let result = nodeModels.filter(item => item.nodeType[0].indexOf(type) !== -1); if (result.length) { if (result.length === 1) { mainNode = result[0]; break; } else { subNodeIds = result.map(item => item.id); break; } } } function filterWithLevel(main, nodes, links, maxlevel) { let usedIds = [], usedLinkIds = [], mainSubIds = []; let cLevel = 0; function deepFind(subIds) { let nextSubIds = []; for (let i = 0, len = subIds.length; i < len; i++) { let subId = subIds[i]; for (let i = 0, len = links.length; i < len; i++) { let r = links[i]; if (r.end === subId) { nextSubIds.push(r.start); usedIds.push(r.start); if (!usedLinkIds.includes(r.id)) { usedLinkIds.push(r.id); } } else if (r.start === subId) { nextSubIds.push(r.end); usedIds.push(r.end); if (!usedLinkIds.includes(r.id)) { usedLinkIds.push(r.id); } } } } nextSubIds = [...new Set(nextSubIds)]; if (cLevel === 0) { mainSubIds = nextSubIds; } if (cLevel < maxlevel - 1 && nextSubIds.length) { cLevel += 1; deepFind(nextSubIds); } } deepFind([main.id]); usedIds = [...new Set(usedIds)]; return { nodes: nodes.filter(item => usedIds.includes(item.id)), links: links.filter(item => usedLinkIds.includes(item.id)), mainSubIds }; } if (mainNode) { subNodeIds = []; Number(mainNode.id); // for (let i = 0, len = relations.length; i < len; i++) { // let r = relations[i] // if (r.end === nodeId) { // subNodeIds.push(r.start) // } else if (r.start === nodeId) { // subNodeIds.push(r.end) // } // } let { nodes, links, mainSubIds } = filterWithLevel(mainNode, nodeModels, relations, level); let model = { ...mainNode, subNodeIds: mainSubIds, // nodes: nodeModels, // links: relations, nodes, links }; return model; } else if (subNodeIds.length) { let fakeMainNodeId = 'node-textbook'; let extralinks = []; subNodeIds.forEach(end => { extralinks.push({ start: 'textbook', end }); }); let fakeMainNode = { id: fakeMainNodeId, name: textBookName }; let { nodes, links, mainSubIds } = filterWithLevel(fakeMainNode, [...nodeModels, fakeMainNode], [...relations, ...extralinks], level); let model = { ...fakeMainNode, subNodeIds: mainSubIds, // nodes: nodeModels, // links: [...relations, ...extralinks], nodes, links }; return model; } return {}; } function queryGraphIndexs(id, storeKeyPrefix) { let all = localStorage.getItem(storeKeyPrefix + 'g-indexs') ? JSON.parse(localStorage.getItem(storeKeyPrefix + 'g-indexs')) : []; let result = all.filter(item => item.nodesId + '' === id + ''); result.forEach(item => { item.contentWithHighlight = item.html.replace(item.nodesName, `${item.nodesName}`); }); return result; } function getTextNodeList(dom) { const nodeList = [...dom.childNodes]; const textNodes = []; while (nodeList.length) { const node = nodeList.shift(); if (node.nodeType === node.TEXT_NODE) { node.wholeText && textNodes.push(node); } else { nodeList.unshift(...node.childNodes); } } return textNodes; } function getTextInfoList(textNodes) { let length = 0; const textList = textNodes.map(node => { let startIdx = length, endIdx = length + node.wholeText.length; length = endIdx; return { text: node.wholeText, startIdx, endIdx }; }); return textList; } function getMatchList(content, keyword) { const characters = [...'\\[](){}?.+*^$:|'].reduce((r, c) => (r[c] = true, r), {}); keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*'); const reg = new RegExp(keyword, 'gmi'); const matchList = []; let match = reg.exec(content); while (match) { matchList.push(match); match = reg.exec(content); } return matchList; } function replaceMatchResult(textNodes, textList, matchList, options) { let { flag, datasets, g } = options; // 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出 if (!g) { matchList = [matchList[0]]; } for (let i = matchList.length - 1; i >= 0; i--) { const match = matchList[i]; const matchStart = match.index, matchEnd = matchStart + match[0].length; // 匹配结果在拼接字符串中的起止索引 // 遍历文本信息列表,查找匹配的文本节点 let groupStart = true; for (let textIdx = 0; textIdx < textList.length; textIdx++) { const { text, startIdx, endIdx } = textList[textIdx]; // 文本内容、文本在拼接串中开始、结束索引 if (endIdx < matchStart) continue; // 匹配的文本节点还在后面 if (startIdx >= matchEnd) break; // 匹配文本节点已经处理完了 let textNode = textNodes[textIdx]; // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换 const nodeMatchStartIdx = Math.max(0, matchStart - startIdx); // 匹配内容在文本节点内容中的开始索引 const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx; // 文本节点内容匹配关键词的长度 if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx); // textNode取后半部分 if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength); const font = document.createElement('font'); font.setAttribute(flag, i + 1); groupStart ? font.classList.add(flag) : font.classList.add(flag + '-rest'); if (datasets.length && Array.isArray(datasets)) { datasets.forEach(({ key, value }) => { font.dataset[key] = value; }); } font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength); textNode.parentNode.replaceChild(font, textNode); groupStart = false; } } } const defaultOptions$2 = { flag: 'xml-graph-link', datasets: [], g: false }; function replaceKeywordsInHTML(html, keyword, options = defaultOptions$2) { options = { ...defaultOptions$2, ...options }; let matchCount = 0, replacedHtml = ''; if (!html || !keyword) { replacedHtml = html; return [replacedHtml, matchCount]; } const div = document.createElement('div'); div.innerHTML = html; const textNodes = getTextNodeList(div); const textList = getTextInfoList(textNodes); const content = textList.map(({ text }) => text).join(''); const matchList = getMatchList(content, keyword); matchCount = matchList.length; replaceMatchResult(textNodes, textList, matchList, options); replacedHtml = div.innerHTML; return [replacedHtml, matchCount]; } const isBody = node => { return node && node.nodeType === 1 && node.tagName.toLowerCase() === "body"; }; function findParent(node, filterFn, includeSelf = false) { if (node && !isBody(node)) { node = includeSelf ? node : node.parentNode; while (node) { if (!filterFn || filterFn(node) || isBody(node)) { return filterFn && !filterFn(node) && isBody(node) ? null : node; } node = node.parentNode; } } return null; } /** * Custom positioning reference element. * @see https://floating-ui.com/docs/virtual-elements */ const sides = ['top', 'right', 'bottom', 'left']; const alignments = ['start', 'end']; const placements$2 = /*#__PURE__*/sides.reduce((acc, side) => acc.concat(side, side + "-" + alignments[0], side + "-" + alignments[1]), []); const min$6 = Math.min; const max$7 = Math.max; const round$8 = Math.round; const createCoords = v => ({ x: v, y: v }); const oppositeSideMap = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; const oppositeAlignmentMap = { start: 'end', end: 'start' }; function clamp$4(start, value, end) { return max$7(start, min$6(value, end)); } function evaluate(value, param) { return typeof value === 'function' ? value(param) : value; } function getSide(placement) { return placement.split('-')[0]; } function getAlignment(placement) { return placement.split('-')[1]; } function getOppositeAxis(axis) { return axis === 'x' ? 'y' : 'x'; } function getAxisLength(axis) { return axis === 'y' ? 'height' : 'width'; } function getSideAxis(placement) { return ['top', 'bottom'].includes(getSide(placement)) ? 'y' : 'x'; } function getAlignmentAxis(placement) { return getOppositeAxis(getSideAxis(placement)); } function getAlignmentSides(placement, rects, rtl) { if (rtl === void 0) { rtl = false; } const alignment = getAlignment(placement); const alignmentAxis = getAlignmentAxis(placement); const length = getAxisLength(alignmentAxis); let mainAlignmentSide = alignmentAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top'; if (rects.reference[length] > rects.floating[length]) { mainAlignmentSide = getOppositePlacement$1(mainAlignmentSide); } return [mainAlignmentSide, getOppositePlacement$1(mainAlignmentSide)]; } function getExpandedPlacements(placement) { const oppositePlacement = getOppositePlacement$1(placement); return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; } function getOppositeAlignmentPlacement(placement) { return placement.replace(/start|end/g, alignment => oppositeAlignmentMap[alignment]); } function getSideList(side, isStart, rtl) { const lr = ['left', 'right']; const rl = ['right', 'left']; const tb = ['top', 'bottom']; const bt = ['bottom', 'top']; switch (side) { case 'top': case 'bottom': if (rtl) return isStart ? rl : lr; return isStart ? lr : rl; case 'left': case 'right': return isStart ? tb : bt; default: return []; } } function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { const alignment = getAlignment(placement); let list = getSideList(getSide(placement), direction === 'start', rtl); if (alignment) { list = list.map(side => side + "-" + alignment); if (flipAlignment) { list = list.concat(list.map(getOppositeAlignmentPlacement)); } } return list; } function getOppositePlacement$1(placement) { return placement.replace(/left|right|bottom|top/g, side => oppositeSideMap[side]); } function expandPaddingObject(padding) { return { top: 0, right: 0, bottom: 0, left: 0, ...padding }; } function getPaddingObject(padding) { return typeof padding !== 'number' ? expandPaddingObject(padding) : { top: padding, right: padding, bottom: padding, left: padding }; } function rectToClientRect$1(rect) { const { x, y, width, height } = rect; return { width, height, top: y, left: x, right: x + width, bottom: y + height, x, y }; } function computeCoordsFromPlacement(_ref, placement, rtl) { let { reference, floating } = _ref; const sideAxis = getSideAxis(placement); const alignmentAxis = getAlignmentAxis(placement); const alignLength = getAxisLength(alignmentAxis); const side = getSide(placement); const isVertical = sideAxis === 'y'; const commonX = reference.x + reference.width / 2 - floating.width / 2; const commonY = reference.y + reference.height / 2 - floating.height / 2; const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; let coords; switch (side) { case 'top': coords = { x: commonX, y: reference.y - floating.height }; break; case 'bottom': coords = { x: commonX, y: reference.y + reference.height }; break; case 'right': coords = { x: reference.x + reference.width, y: commonY }; break; case 'left': coords = { x: reference.x - floating.width, y: commonY }; break; default: coords = { x: reference.x, y: reference.y }; } switch (getAlignment(placement)) { case 'start': coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); break; case 'end': coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); break; } return coords; } /** * Computes the `x` and `y` coordinates that will place the floating element * next to a given reference element. * * This export does not have any `platform` interface logic. You will need to * write one for the platform you are using Floating UI with. */ const computePosition$1 = async (reference, floating, config) => { const { placement = 'bottom', strategy = 'absolute', middleware = [], platform } = config; const validMiddleware = middleware.filter(Boolean); const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating)); let rects = await platform.getElementRects({ reference, floating, strategy }); let { x, y } = computeCoordsFromPlacement(rects, placement, rtl); let statefulPlacement = placement; let middlewareData = {}; let resetCount = 0; for (let i = 0; i < validMiddleware.length; i++) { const { name, fn } = validMiddleware[i]; const { x: nextX, y: nextY, data, reset } = await fn({ x, y, initialPlacement: placement, placement: statefulPlacement, strategy, middlewareData, rects, platform, elements: { reference, floating } }); x = nextX != null ? nextX : x; y = nextY != null ? nextY : y; middlewareData = { ...middlewareData, [name]: { ...middlewareData[name], ...data } }; if (reset && resetCount <= 50) { resetCount++; if (typeof reset === 'object') { if (reset.placement) { statefulPlacement = reset.placement; } if (reset.rects) { rects = reset.rects === true ? await platform.getElementRects({ reference, floating, strategy }) : reset.rects; } ({ x, y } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); } i = -1; } } return { x, y, placement: statefulPlacement, strategy, middlewareData }; }; /** * Resolves with an object of overflow side offsets that determine how much the * element is overflowing a given clipping boundary on each side. * - positive = overflowing the boundary by that number of pixels * - negative = how many pixels left before it will overflow * - 0 = lies flush with the boundary * @see https://floating-ui.com/docs/detectOverflow */ async function detectOverflow$1(state, options) { var _await$platform$isEle; if (options === void 0) { options = {}; } const { x, y, platform, rects, elements, strategy } = state; const { boundary = 'clippingAncestors', rootBoundary = 'viewport', elementContext = 'floating', altBoundary = false, padding = 0 } = evaluate(options, state); const paddingObject = getPaddingObject(padding); const altContext = elementContext === 'floating' ? 'reference' : 'floating'; const element = elements[altBoundary ? altContext : elementContext]; const clippingClientRect = rectToClientRect$1(await platform.getClippingRect({ element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))), boundary, rootBoundary, strategy })); const rect = elementContext === 'floating' ? { x, y, width: rects.floating.width, height: rects.floating.height } : rects.reference; const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating)); const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || { x: 1, y: 1 } : { x: 1, y: 1 }; const elementClientRect = rectToClientRect$1(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ elements, rect, offsetParent, strategy }) : rect); return { top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x }; } /** * Provides data to position an inner element of the floating element so that it * appears centered to the reference element. * @see https://floating-ui.com/docs/arrow */ const arrow$3 = options => ({ name: 'arrow', options, async fn(state) { const { x, y, placement, rects, platform, elements, middlewareData } = state; // Since `element` is required, we don't Partial<> the type. const { element, padding = 0 } = evaluate(options, state) || {}; if (element == null) { return {}; } const paddingObject = getPaddingObject(padding); const coords = { x, y }; const axis = getAlignmentAxis(placement); const length = getAxisLength(axis); const arrowDimensions = await platform.getDimensions(element); const isYAxis = axis === 'y'; const minProp = isYAxis ? 'top' : 'left'; const maxProp = isYAxis ? 'bottom' : 'right'; const clientProp = isYAxis ? 'clientHeight' : 'clientWidth'; const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; const startDiff = coords[axis] - rects.reference[axis]; const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element)); let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; // DOM platform can return `window` as the `offsetParent`. if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) { clientSize = elements.floating[clientProp] || rects.floating[length]; } const centerToReference = endDiff / 2 - startDiff / 2; // If the padding is large enough that it causes the arrow to no longer be // centered, modify the padding so that it is centered. const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; const minPadding = min$6(paddingObject[minProp], largestPossiblePadding); const maxPadding = min$6(paddingObject[maxProp], largestPossiblePadding); // Make sure the arrow doesn't overflow the floating element if the center // point is outside the floating element's bounds. const min$1 = minPadding; const max = clientSize - arrowDimensions[length] - maxPadding; const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; const offset = clamp$4(min$1, center, max); // If the reference is small enough that the arrow's padding causes it to // to point to nothing for an aligned placement, adjust the offset of the // floating element itself. To ensure `shift()` continues to take action, // a single reset is performed when this is true. const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0; return { [axis]: coords[axis] + alignmentOffset, data: { [axis]: offset, centerOffset: center - offset - alignmentOffset, ...(shouldAddOffset && { alignmentOffset }) }, reset: shouldAddOffset }; } }); function getPlacementList(alignment, autoAlignment, allowedPlacements) { const allowedPlacementsSortedByAlignment = alignment ? [...allowedPlacements.filter(placement => getAlignment(placement) === alignment), ...allowedPlacements.filter(placement => getAlignment(placement) !== alignment)] : allowedPlacements.filter(placement => getSide(placement) === placement); return allowedPlacementsSortedByAlignment.filter(placement => { if (alignment) { return getAlignment(placement) === alignment || (autoAlignment ? getOppositeAlignmentPlacement(placement) !== placement : false); } return true; }); } /** * Optimizes the visibility of the floating element by choosing the placement * that has the most space available automatically, without needing to specify a * preferred placement. Alternative to `flip`. * @see https://floating-ui.com/docs/autoPlacement */ const autoPlacement$1 = function (options) { if (options === void 0) { options = {}; } return { name: 'autoPlacement', options, async fn(state) { var _middlewareData$autoP, _middlewareData$autoP2, _placementsThatFitOnE; const { rects, middlewareData, placement, platform, elements } = state; const { crossAxis = false, alignment, allowedPlacements = placements$2, autoAlignment = true, ...detectOverflowOptions } = evaluate(options, state); const placements$1 = alignment !== undefined || allowedPlacements === placements$2 ? getPlacementList(alignment || null, autoAlignment, allowedPlacements) : allowedPlacements; const overflow = await detectOverflow$1(state, detectOverflowOptions); const currentIndex = ((_middlewareData$autoP = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP.index) || 0; const currentPlacement = placements$1[currentIndex]; if (currentPlacement == null) { return {}; } const alignmentSides = getAlignmentSides(currentPlacement, rects, await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))); // Make `computeCoords` start from the right place. if (placement !== currentPlacement) { return { reset: { placement: placements$1[0] } }; } const currentOverflows = [overflow[getSide(currentPlacement)], overflow[alignmentSides[0]], overflow[alignmentSides[1]]]; const allOverflows = [...(((_middlewareData$autoP2 = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP2.overflows) || []), { placement: currentPlacement, overflows: currentOverflows }]; const nextPlacement = placements$1[currentIndex + 1]; // There are more placements to check. if (nextPlacement) { return { data: { index: currentIndex + 1, overflows: allOverflows }, reset: { placement: nextPlacement } }; } const placementsSortedByMostSpace = allOverflows.map(d => { const alignment = getAlignment(d.placement); return [d.placement, alignment && crossAxis ? // Check along the mainAxis and main crossAxis side. d.overflows.slice(0, 2).reduce((acc, v) => acc + v, 0) : // Check only the mainAxis. d.overflows[0], d.overflows]; }).sort((a, b) => a[1] - b[1]); const placementsThatFitOnEachSide = placementsSortedByMostSpace.filter(d => d[2].slice(0, // Aligned placements should not check their opposite crossAxis // side. getAlignment(d[0]) ? 2 : 3).every(v => v <= 0)); const resetPlacement = ((_placementsThatFitOnE = placementsThatFitOnEachSide[0]) == null ? void 0 : _placementsThatFitOnE[0]) || placementsSortedByMostSpace[0][0]; if (resetPlacement !== placement) { return { data: { index: currentIndex + 1, overflows: allOverflows }, reset: { placement: resetPlacement } }; } return {}; } }; }; /** * Optimizes the visibility of the floating element by flipping the `placement` * in order to keep it in view when the preferred placement(s) will overflow the * clipping boundary. Alternative to `autoPlacement`. * @see https://floating-ui.com/docs/flip */ const flip$3 = function (options) { if (options === void 0) { options = {}; } return { name: 'flip', options, async fn(state) { var _middlewareData$arrow, _middlewareData$flip; const { placement, middlewareData, rects, initialPlacement, platform, elements } = state; const { mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = true, fallbackPlacements: specifiedFallbackPlacements, fallbackStrategy = 'bestFit', fallbackAxisSideDirection = 'none', flipAlignment = true, ...detectOverflowOptions } = evaluate(options, state); // If a reset by the arrow was caused due to an alignment offset being // added, we should skip any logic now since `flip()` has already done its // work. // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643 if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { return {}; } const side = getSide(placement); const initialSideAxis = getSideAxis(initialPlacement); const isBasePlacement = getSide(initialPlacement) === initialPlacement; const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement$1(initialPlacement)] : getExpandedPlacements(initialPlacement)); const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none'; if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); } const placements = [initialPlacement, ...fallbackPlacements]; const overflow = await detectOverflow$1(state, detectOverflowOptions); const overflows = []; let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; if (checkMainAxis) { overflows.push(overflow[side]); } if (checkCrossAxis) { const sides = getAlignmentSides(placement, rects, rtl); overflows.push(overflow[sides[0]], overflow[sides[1]]); } overflowsData = [...overflowsData, { placement, overflows }]; // One or more sides is overflowing. if (!overflows.every(side => side <= 0)) { var _middlewareData$flip2, _overflowsData$filter; const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; const nextPlacement = placements[nextIndex]; if (nextPlacement) { const ignoreCrossAxisOverflow = checkCrossAxis === 'alignment' ? initialSideAxis !== getSideAxis(nextPlacement) : false; if (!ignoreCrossAxisOverflow || // We leave the current main axis only if every placement on that axis // overflows the main axis. overflowsData.every(d => d.overflows[0] > 0 && getSideAxis(d.placement) === initialSideAxis)) { // Try next placement and re-run the lifecycle. return { data: { index: nextIndex, overflows: overflowsData }, reset: { placement: nextPlacement } }; } } // First, find the candidates that fit on the mainAxis side of overflow, // then find the placement that fits the best on the main crossAxis side. let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; // Otherwise fallback. if (!resetPlacement) { switch (fallbackStrategy) { case 'bestFit': { var _overflowsData$filter2; const placement = (_overflowsData$filter2 = overflowsData.filter(d => { if (hasFallbackAxisSideDirection) { const currentSideAxis = getSideAxis(d.placement); return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal // reading directions favoring greater width. currentSideAxis === 'y'; } return true; }).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; if (placement) { resetPlacement = placement; } break; } case 'initialPlacement': resetPlacement = initialPlacement; break; } } if (placement !== resetPlacement) { return { reset: { placement: resetPlacement } }; } } return {}; } }; }; function getSideOffsets$1(overflow, rect) { return { top: overflow.top - rect.height, right: overflow.right - rect.width, bottom: overflow.bottom - rect.height, left: overflow.left - rect.width }; } function isAnySideFullyClipped$1(overflow) { return sides.some(side => overflow[side] >= 0); } /** * Provides data to hide the floating element in applicable situations, such as * when it is not in the same clipping context as the reference element. * @see https://floating-ui.com/docs/hide */ const hide$2 = function (options) { if (options === void 0) { options = {}; } return { name: 'hide', options, async fn(state) { const { rects } = state; const { strategy = 'referenceHidden', ...detectOverflowOptions } = evaluate(options, state); switch (strategy) { case 'referenceHidden': { const overflow = await detectOverflow$1(state, { ...detectOverflowOptions, elementContext: 'reference' }); const offsets = getSideOffsets$1(overflow, rects.reference); return { data: { referenceHiddenOffsets: offsets, referenceHidden: isAnySideFullyClipped$1(offsets) } }; } case 'escaped': { const overflow = await detectOverflow$1(state, { ...detectOverflowOptions, altBoundary: true }); const offsets = getSideOffsets$1(overflow, rects.floating); return { data: { escapedOffsets: offsets, escaped: isAnySideFullyClipped$1(offsets) } }; } default: { return {}; } } } }; }; function getBoundingRect$1(rects) { const minX = min$6(...rects.map(rect => rect.left)); const minY = min$6(...rects.map(rect => rect.top)); const maxX = max$7(...rects.map(rect => rect.right)); const maxY = max$7(...rects.map(rect => rect.bottom)); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } function getRectsByLine(rects) { const sortedRects = rects.slice().sort((a, b) => a.y - b.y); const groups = []; let prevRect = null; for (let i = 0; i < sortedRects.length; i++) { const rect = sortedRects[i]; if (!prevRect || rect.y - prevRect.y > prevRect.height / 2) { groups.push([rect]); } else { groups[groups.length - 1].push(rect); } prevRect = rect; } return groups.map(rect => rectToClientRect$1(getBoundingRect$1(rect))); } /** * Provides improved positioning for inline reference elements that can span * over multiple lines, such as hyperlinks or range selections. * @see https://floating-ui.com/docs/inline */ const inline = function (options) { if (options === void 0) { options = {}; } return { name: 'inline', options, async fn(state) { const { placement, elements, rects, platform, strategy } = state; // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a // ClientRect's bounds, despite the event listener being triggered. A // padding of 2 seems to handle this issue. const { padding = 2, x, y } = evaluate(options, state); const nativeClientRects = Array.from((await (platform.getClientRects == null ? void 0 : platform.getClientRects(elements.reference))) || []); const clientRects = getRectsByLine(nativeClientRects); const fallback = rectToClientRect$1(getBoundingRect$1(nativeClientRects)); const paddingObject = getPaddingObject(padding); function getBoundingClientRect() { // There are two rects and they are disjoined. if (clientRects.length === 2 && clientRects[0].left > clientRects[1].right && x != null && y != null) { // Find the first rect in which the point is fully inside. return clientRects.find(rect => x > rect.left - paddingObject.left && x < rect.right + paddingObject.right && y > rect.top - paddingObject.top && y < rect.bottom + paddingObject.bottom) || fallback; } // There are 2 or more connected rects. if (clientRects.length >= 2) { if (getSideAxis(placement) === 'y') { const firstRect = clientRects[0]; const lastRect = clientRects[clientRects.length - 1]; const isTop = getSide(placement) === 'top'; const top = firstRect.top; const bottom = lastRect.bottom; const left = isTop ? firstRect.left : lastRect.left; const right = isTop ? firstRect.right : lastRect.right; const width = right - left; const height = bottom - top; return { top, bottom, left, right, width, height, x: left, y: top }; } const isLeftSide = getSide(placement) === 'left'; const maxRight = max$7(...clientRects.map(rect => rect.right)); const minLeft = min$6(...clientRects.map(rect => rect.left)); const measureRects = clientRects.filter(rect => isLeftSide ? rect.left === minLeft : rect.right === maxRight); const top = measureRects[0].top; const bottom = measureRects[measureRects.length - 1].bottom; const left = minLeft; const right = maxRight; const width = right - left; const height = bottom - top; return { top, bottom, left, right, width, height, x: left, y: top }; } return fallback; } const resetRects = await platform.getElementRects({ reference: { getBoundingClientRect }, floating: elements.floating, strategy }); if (rects.reference.x !== resetRects.reference.x || rects.reference.y !== resetRects.reference.y || rects.reference.width !== resetRects.reference.width || rects.reference.height !== resetRects.reference.height) { return { reset: { rects: resetRects } }; } return {}; } }; }; // For type backwards-compatibility, the `OffsetOptions` type was also // Derivable. async function convertValueToCoords(state, options) { const { placement, platform, elements } = state; const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); const side = getSide(placement); const alignment = getAlignment(placement); const isVertical = getSideAxis(placement) === 'y'; const mainAxisMulti = ['left', 'top'].includes(side) ? -1 : 1; const crossAxisMulti = rtl && isVertical ? -1 : 1; const rawValue = evaluate(options, state); // eslint-disable-next-line prefer-const let { mainAxis, crossAxis, alignmentAxis } = typeof rawValue === 'number' ? { mainAxis: rawValue, crossAxis: 0, alignmentAxis: null } : { mainAxis: rawValue.mainAxis || 0, crossAxis: rawValue.crossAxis || 0, alignmentAxis: rawValue.alignmentAxis }; if (alignment && typeof alignmentAxis === 'number') { crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis; } return isVertical ? { x: crossAxis * crossAxisMulti, y: mainAxis * mainAxisMulti } : { x: mainAxis * mainAxisMulti, y: crossAxis * crossAxisMulti }; } /** * Modifies the placement by translating the floating element along the * specified axes. * A number (shorthand for `mainAxis` or distance), or an axes configuration * object may be passed. * @see https://floating-ui.com/docs/offset */ const offset$4 = function (options) { if (options === void 0) { options = 0; } return { name: 'offset', options, async fn(state) { var _middlewareData$offse, _middlewareData$arrow; const { x, y, placement, middlewareData } = state; const diffCoords = await convertValueToCoords(state, options); // If the placement is the same and the arrow caused an alignment offset // then we don't need to change the positioning coordinates. if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { return {}; } return { x: x + diffCoords.x, y: y + diffCoords.y, data: { ...diffCoords, placement } }; } }; }; /** * Optimizes the visibility of the floating element by shifting it in order to * keep it in view when it will overflow the clipping boundary. * @see https://floating-ui.com/docs/shift */ const shift$1 = function (options) { if (options === void 0) { options = {}; } return { name: 'shift', options, async fn(state) { const { x, y, placement } = state; const { mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = false, limiter = { fn: _ref => { let { x, y } = _ref; return { x, y }; } }, ...detectOverflowOptions } = evaluate(options, state); const coords = { x, y }; const overflow = await detectOverflow$1(state, detectOverflowOptions); const crossAxis = getSideAxis(getSide(placement)); const mainAxis = getOppositeAxis(crossAxis); let mainAxisCoord = coords[mainAxis]; let crossAxisCoord = coords[crossAxis]; if (checkMainAxis) { const minSide = mainAxis === 'y' ? 'top' : 'left'; const maxSide = mainAxis === 'y' ? 'bottom' : 'right'; const min = mainAxisCoord + overflow[minSide]; const max = mainAxisCoord - overflow[maxSide]; mainAxisCoord = clamp$4(min, mainAxisCoord, max); } if (checkCrossAxis) { const minSide = crossAxis === 'y' ? 'top' : 'left'; const maxSide = crossAxis === 'y' ? 'bottom' : 'right'; const min = crossAxisCoord + overflow[minSide]; const max = crossAxisCoord - overflow[maxSide]; crossAxisCoord = clamp$4(min, crossAxisCoord, max); } const limitedCoords = limiter.fn({ ...state, [mainAxis]: mainAxisCoord, [crossAxis]: crossAxisCoord }); return { ...limitedCoords, data: { x: limitedCoords.x - x, y: limitedCoords.y - y, enabled: { [mainAxis]: checkMainAxis, [crossAxis]: checkCrossAxis } } }; } }; }; /** * Built-in `limiter` that will stop `shift()` at a certain point. */ const limitShift = function (options) { if (options === void 0) { options = {}; } return { options, fn(state) { const { x, y, placement, rects, middlewareData } = state; const { offset = 0, mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = true } = evaluate(options, state); const coords = { x, y }; const crossAxis = getSideAxis(placement); const mainAxis = getOppositeAxis(crossAxis); let mainAxisCoord = coords[mainAxis]; let crossAxisCoord = coords[crossAxis]; const rawOffset = evaluate(offset, state); const computedOffset = typeof rawOffset === 'number' ? { mainAxis: rawOffset, crossAxis: 0 } : { mainAxis: 0, crossAxis: 0, ...rawOffset }; if (checkMainAxis) { const len = mainAxis === 'y' ? 'height' : 'width'; const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis; const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis; if (mainAxisCoord < limitMin) { mainAxisCoord = limitMin; } else if (mainAxisCoord > limitMax) { mainAxisCoord = limitMax; } } if (checkCrossAxis) { var _middlewareData$offse, _middlewareData$offse2; const len = mainAxis === 'y' ? 'width' : 'height'; const isOriginSide = ['top', 'left'].includes(getSide(placement)); const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis); const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0); if (crossAxisCoord < limitMin) { crossAxisCoord = limitMin; } else if (crossAxisCoord > limitMax) { crossAxisCoord = limitMax; } } return { [mainAxis]: mainAxisCoord, [crossAxis]: crossAxisCoord }; } }; }; /** * Provides data that allows you to change the size of the floating element — * for instance, prevent it from overflowing the clipping boundary or match the * width of the reference element. * @see https://floating-ui.com/docs/size */ const size = function (options) { if (options === void 0) { options = {}; } return { name: 'size', options, async fn(state) { var _state$middlewareData, _state$middlewareData2; const { placement, rects, platform, elements } = state; const { apply = () => {}, ...detectOverflowOptions } = evaluate(options, state); const overflow = await detectOverflow$1(state, detectOverflowOptions); const side = getSide(placement); const alignment = getAlignment(placement); const isYAxis = getSideAxis(placement) === 'y'; const { width, height } = rects.floating; let heightSide; let widthSide; if (side === 'top' || side === 'bottom') { heightSide = side; widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right'; } else { widthSide = side; heightSide = alignment === 'end' ? 'top' : 'bottom'; } const maximumClippingHeight = height - overflow.top - overflow.bottom; const maximumClippingWidth = width - overflow.left - overflow.right; const overflowAvailableHeight = min$6(height - overflow[heightSide], maximumClippingHeight); const overflowAvailableWidth = min$6(width - overflow[widthSide], maximumClippingWidth); const noShift = !state.middlewareData.shift; let availableHeight = overflowAvailableHeight; let availableWidth = overflowAvailableWidth; if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) { availableWidth = maximumClippingWidth; } if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) { availableHeight = maximumClippingHeight; } if (noShift && !alignment) { const xMin = max$7(overflow.left, 0); const xMax = max$7(overflow.right, 0); const yMin = max$7(overflow.top, 0); const yMax = max$7(overflow.bottom, 0); if (isYAxis) { availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max$7(overflow.left, overflow.right)); } else { availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max$7(overflow.top, overflow.bottom)); } } await apply({ ...state, availableWidth, availableHeight }); const nextDimensions = await platform.getDimensions(elements.floating); if (width !== nextDimensions.width || height !== nextDimensions.height) { return { reset: { rects: true } }; } return {}; } }; }; function hasWindow() { return typeof window !== 'undefined'; } function getNodeName$2(node) { if (isNode(node)) { return (node.nodeName || '').toLowerCase(); } // Mocked nodes in testing environments may not be instances of Node. By // returning `#document` an infinite loop won't occur. // https://github.com/floating-ui/floating-ui/issues/2317 return '#document'; } function getWindow$2(node) { var _node$ownerDocument; return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; } function getDocumentElement$2(node) { var _ref; return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; } function isNode(value) { if (!hasWindow()) { return false; } return value instanceof Node || value instanceof getWindow$2(value).Node; } function isElement$4(value) { if (!hasWindow()) { return false; } return value instanceof Element || value instanceof getWindow$2(value).Element; } function isHTMLElement$3(value) { if (!hasWindow()) { return false; } return value instanceof HTMLElement || value instanceof getWindow$2(value).HTMLElement; } function isShadowRoot$2(value) { if (!hasWindow() || typeof ShadowRoot === 'undefined') { return false; } return value instanceof ShadowRoot || value instanceof getWindow$2(value).ShadowRoot; } function isOverflowElement(element) { const { overflow, overflowX, overflowY, display } = getComputedStyle$4(element); return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['inline', 'contents'].includes(display); } function isTableElement$2(element) { return ['table', 'td', 'th'].includes(getNodeName$2(element)); } function isTopLayer(element) { return [':popover-open', ':modal'].some(selector => { try { return element.matches(selector); } catch (e) { return false; } }); } function isContainingBlock(elementOrCss) { const webkit = isWebKit(); const css = isElement$4(elementOrCss) ? getComputedStyle$4(elementOrCss) : elementOrCss; // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block // https://drafts.csswg.org/css-transforms-2/#individual-transforms return ['transform', 'translate', 'scale', 'rotate', 'perspective'].some(value => css[value] ? css[value] !== 'none' : false) || (css.containerType ? css.containerType !== 'normal' : false) || !webkit && (css.backdropFilter ? css.backdropFilter !== 'none' : false) || !webkit && (css.filter ? css.filter !== 'none' : false) || ['transform', 'translate', 'scale', 'rotate', 'perspective', 'filter'].some(value => (css.willChange || '').includes(value)) || ['paint', 'layout', 'strict', 'content'].some(value => (css.contain || '').includes(value)); } function getContainingBlock$2(element) { let currentNode = getParentNode$2(element); while (isHTMLElement$3(currentNode) && !isLastTraversableNode(currentNode)) { if (isContainingBlock(currentNode)) { return currentNode; } else if (isTopLayer(currentNode)) { return null; } currentNode = getParentNode$2(currentNode); } return null; } function isWebKit() { if (typeof CSS === 'undefined' || !CSS.supports) return false; return CSS.supports('-webkit-backdrop-filter', 'none'); } function isLastTraversableNode(node) { return ['html', 'body', '#document'].includes(getNodeName$2(node)); } function getComputedStyle$4(element) { return getWindow$2(element).getComputedStyle(element); } function getNodeScroll$2(element) { if (isElement$4(element)) { return { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop }; } return { scrollLeft: element.scrollX, scrollTop: element.scrollY }; } function getParentNode$2(node) { if (getNodeName$2(node) === 'html') { return node; } const result = // Step into the shadow DOM of the parent of a slotted node. node.assignedSlot || // DOM Element detected. node.parentNode || // ShadowRoot detected. isShadowRoot$2(node) && node.host || // Fallback. getDocumentElement$2(node); return isShadowRoot$2(result) ? result.host : result; } function getNearestOverflowAncestor(node) { const parentNode = getParentNode$2(node); if (isLastTraversableNode(parentNode)) { return node.ownerDocument ? node.ownerDocument.body : node.body; } if (isHTMLElement$3(parentNode) && isOverflowElement(parentNode)) { return parentNode; } return getNearestOverflowAncestor(parentNode); } function getOverflowAncestors(node, list, traverseIframes) { var _node$ownerDocument2; if (list === void 0) { list = []; } if (traverseIframes === void 0) { traverseIframes = true; } const scrollableAncestor = getNearestOverflowAncestor(node); const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); const win = getWindow$2(scrollableAncestor); if (isBody) { const frameElement = getFrameElement(win); return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []); } return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); } function getFrameElement(win) { return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; } function getCssDimensions(element) { const css = getComputedStyle$4(element); // In testing environments, the `width` and `height` properties are empty // strings for SVG elements, returning NaN. Fallback to `0` in this case. let width = parseFloat(css.width) || 0; let height = parseFloat(css.height) || 0; const hasOffset = isHTMLElement$3(element); const offsetWidth = hasOffset ? element.offsetWidth : width; const offsetHeight = hasOffset ? element.offsetHeight : height; const shouldFallback = round$8(width) !== offsetWidth || round$8(height) !== offsetHeight; if (shouldFallback) { width = offsetWidth; height = offsetHeight; } return { width, height, $: shouldFallback }; } function unwrapElement(element) { return !isElement$4(element) ? element.contextElement : element; } function getScale(element) { const domElement = unwrapElement(element); if (!isHTMLElement$3(domElement)) { return createCoords(1); } const rect = domElement.getBoundingClientRect(); const { width, height, $ } = getCssDimensions(domElement); let x = ($ ? round$8(rect.width) : rect.width) / width; let y = ($ ? round$8(rect.height) : rect.height) / height; // 0, NaN, or Infinity should always fallback to 1. if (!x || !Number.isFinite(x)) { x = 1; } if (!y || !Number.isFinite(y)) { y = 1; } return { x, y }; } const noOffsets = /*#__PURE__*/createCoords(0); function getVisualOffsets(element) { const win = getWindow$2(element); if (!isWebKit() || !win.visualViewport) { return noOffsets; } return { x: win.visualViewport.offsetLeft, y: win.visualViewport.offsetTop }; } function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { if (isFixed === void 0) { isFixed = false; } if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow$2(element)) { return false; } return isFixed; } function getBoundingClientRect$3(element, includeScale, isFixedStrategy, offsetParent) { if (includeScale === void 0) { includeScale = false; } if (isFixedStrategy === void 0) { isFixedStrategy = false; } const clientRect = element.getBoundingClientRect(); const domElement = unwrapElement(element); let scale = createCoords(1); if (includeScale) { if (offsetParent) { if (isElement$4(offsetParent)) { scale = getScale(offsetParent); } } else { scale = getScale(element); } } const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); let x = (clientRect.left + visualOffsets.x) / scale.x; let y = (clientRect.top + visualOffsets.y) / scale.y; let width = clientRect.width / scale.x; let height = clientRect.height / scale.y; if (domElement) { const win = getWindow$2(domElement); const offsetWin = offsetParent && isElement$4(offsetParent) ? getWindow$2(offsetParent) : offsetParent; let currentWin = win; let currentIFrame = getFrameElement(currentWin); while (currentIFrame && offsetParent && offsetWin !== currentWin) { const iframeScale = getScale(currentIFrame); const iframeRect = currentIFrame.getBoundingClientRect(); const css = getComputedStyle$4(currentIFrame); const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; x *= iframeScale.x; y *= iframeScale.y; width *= iframeScale.x; height *= iframeScale.y; x += left; y += top; currentWin = getWindow$2(currentIFrame); currentIFrame = getFrameElement(currentWin); } } return rectToClientRect$1({ width, height, x, y }); } // If has a CSS width greater than the viewport, then this will be // incorrect for RTL. function getWindowScrollBarX$2(element, rect) { const leftScroll = getNodeScroll$2(element).scrollLeft; if (!rect) { return getBoundingClientRect$3(getDocumentElement$2(element)).left + leftScroll; } return rect.left + leftScroll; } function getHTMLOffset(documentElement, scroll, ignoreScrollbarX) { if (ignoreScrollbarX === void 0) { ignoreScrollbarX = false; } const htmlRect = documentElement.getBoundingClientRect(); const x = htmlRect.left + scroll.scrollLeft - (ignoreScrollbarX ? 0 : // RTL
scrollbar. getWindowScrollBarX$2(documentElement, htmlRect)); const y = htmlRect.top + scroll.scrollTop; return { x, y }; } function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { let { elements, rect, offsetParent, strategy } = _ref; const isFixed = strategy === 'fixed'; const documentElement = getDocumentElement$2(offsetParent); const topLayer = elements ? isTopLayer(elements.floating) : false; if (offsetParent === documentElement || topLayer && isFixed) { return rect; } let scroll = { scrollLeft: 0, scrollTop: 0 }; let scale = createCoords(1); const offsets = createCoords(0); const isOffsetParentAnElement = isHTMLElement$3(offsetParent); if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { if (getNodeName$2(offsetParent) !== 'body' || isOverflowElement(documentElement)) { scroll = getNodeScroll$2(offsetParent); } if (isHTMLElement$3(offsetParent)) { const offsetRect = getBoundingClientRect$3(offsetParent); scale = getScale(offsetParent); offsets.x = offsetRect.x + offsetParent.clientLeft; offsets.y = offsetRect.y + offsetParent.clientTop; } } const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll, true) : createCoords(0); return { width: rect.width * scale.x, height: rect.height * scale.y, x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y }; } function getClientRects(element) { return Array.from(element.getClientRects()); } // Gets the entire size of the scrollable document area, even extending outside // of the `` and `` rect bounds if horizontally scrollable. function getDocumentRect$1(element) { const html = getDocumentElement$2(element); const scroll = getNodeScroll$2(element); const body = element.ownerDocument.body; const width = max$7(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); const height = max$7(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); let x = -scroll.scrollLeft + getWindowScrollBarX$2(element); const y = -scroll.scrollTop; if (getComputedStyle$4(body).direction === 'rtl') { x += max$7(html.clientWidth, body.clientWidth) - width; } return { width, height, x, y }; } function getViewportRect$1(element, strategy) { const win = getWindow$2(element); const html = getDocumentElement$2(element); const visualViewport = win.visualViewport; let width = html.clientWidth; let height = html.clientHeight; let x = 0; let y = 0; if (visualViewport) { width = visualViewport.width; height = visualViewport.height; const visualViewportBased = isWebKit(); if (!visualViewportBased || visualViewportBased && strategy === 'fixed') { x = visualViewport.offsetLeft; y = visualViewport.offsetTop; } } return { width, height, x, y }; } // Returns the inner client rect, subtracting scrollbars if present. function getInnerBoundingClientRect$1(element, strategy) { const clientRect = getBoundingClientRect$3(element, true, strategy === 'fixed'); const top = clientRect.top + element.clientTop; const left = clientRect.left + element.clientLeft; const scale = isHTMLElement$3(element) ? getScale(element) : createCoords(1); const width = element.clientWidth * scale.x; const height = element.clientHeight * scale.y; const x = left * scale.x; const y = top * scale.y; return { width, height, x, y }; } function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { let rect; if (clippingAncestor === 'viewport') { rect = getViewportRect$1(element, strategy); } else if (clippingAncestor === 'document') { rect = getDocumentRect$1(getDocumentElement$2(element)); } else if (isElement$4(clippingAncestor)) { rect = getInnerBoundingClientRect$1(clippingAncestor, strategy); } else { const visualOffsets = getVisualOffsets(element); rect = { x: clippingAncestor.x - visualOffsets.x, y: clippingAncestor.y - visualOffsets.y, width: clippingAncestor.width, height: clippingAncestor.height }; } return rectToClientRect$1(rect); } function hasFixedPositionAncestor(element, stopNode) { const parentNode = getParentNode$2(element); if (parentNode === stopNode || !isElement$4(parentNode) || isLastTraversableNode(parentNode)) { return false; } return getComputedStyle$4(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode); } // A "clipping ancestor" is an `overflow` element with the characteristic of // clipping (or hiding) child elements. This returns all clipping ancestors // of the given element up the tree. function getClippingElementAncestors(element, cache) { const cachedResult = cache.get(element); if (cachedResult) { return cachedResult; } let result = getOverflowAncestors(element, [], false).filter(el => isElement$4(el) && getNodeName$2(el) !== 'body'); let currentContainingBlockComputedStyle = null; const elementIsFixed = getComputedStyle$4(element).position === 'fixed'; let currentNode = elementIsFixed ? getParentNode$2(element) : element; // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block while (isElement$4(currentNode) && !isLastTraversableNode(currentNode)) { const computedStyle = getComputedStyle$4(currentNode); const currentNodeIsContaining = isContainingBlock(currentNode); if (!currentNodeIsContaining && computedStyle.position === 'fixed') { currentContainingBlockComputedStyle = null; } const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); if (shouldDropCurrentNode) { // Drop non-containing blocks. result = result.filter(ancestor => ancestor !== currentNode); } else { // Record last containing block for next iteration. currentContainingBlockComputedStyle = computedStyle; } currentNode = getParentNode$2(currentNode); } cache.set(element, result); return result; } // Gets the maximum area that the element is visible in due to any number of // clipping ancestors. function getClippingRect$1(_ref) { let { element, boundary, rootBoundary, strategy } = _ref; const elementClippingAncestors = boundary === 'clippingAncestors' ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary); const clippingAncestors = [...elementClippingAncestors, rootBoundary]; const firstClippingAncestor = clippingAncestors[0]; const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy); accRect.top = max$7(rect.top, accRect.top); accRect.right = min$6(rect.right, accRect.right); accRect.bottom = min$6(rect.bottom, accRect.bottom); accRect.left = max$7(rect.left, accRect.left); return accRect; }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy)); return { width: clippingRect.right - clippingRect.left, height: clippingRect.bottom - clippingRect.top, x: clippingRect.left, y: clippingRect.top }; } function getDimensions(element) { const { width, height } = getCssDimensions(element); return { width, height }; } function getRectRelativeToOffsetParent(element, offsetParent, strategy) { const isOffsetParentAnElement = isHTMLElement$3(offsetParent); const documentElement = getDocumentElement$2(offsetParent); const isFixed = strategy === 'fixed'; const rect = getBoundingClientRect$3(element, true, isFixed, offsetParent); let scroll = { scrollLeft: 0, scrollTop: 0 }; const offsets = createCoords(0); // If the scrollbar appears on the left (e.g. RTL systems). Use // Firefox with layout.scrollbar.side = 3 in about:config to test this. function setLeftRTLScrollbarOffset() { offsets.x = getWindowScrollBarX$2(documentElement); } if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { if (getNodeName$2(offsetParent) !== 'body' || isOverflowElement(documentElement)) { scroll = getNodeScroll$2(offsetParent); } if (isOffsetParentAnElement) { const offsetRect = getBoundingClientRect$3(offsetParent, true, isFixed, offsetParent); offsets.x = offsetRect.x + offsetParent.clientLeft; offsets.y = offsetRect.y + offsetParent.clientTop; } else if (documentElement) { setLeftRTLScrollbarOffset(); } } if (isFixed && !isOffsetParentAnElement && documentElement) { setLeftRTLScrollbarOffset(); } const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; return { x, y, width: rect.width, height: rect.height }; } function isStaticPositioned(element) { return getComputedStyle$4(element).position === 'static'; } function getTrueOffsetParent$2(element, polyfill) { if (!isHTMLElement$3(element) || getComputedStyle$4(element).position === 'fixed') { return null; } if (polyfill) { return polyfill(element); } let rawOffsetParent = element.offsetParent; // Firefox returns the element as the offsetParent if it's non-static, // while Chrome and Safari return the element. The element must // be used to perform the correct calculations even if the element is // non-static. if (getDocumentElement$2(element) === rawOffsetParent) { rawOffsetParent = rawOffsetParent.ownerDocument.body; } return rawOffsetParent; } // Gets the closest ancestor positioned element. Handles some edge cases, // such as table ancestors and cross browser bugs. function getOffsetParent$2(element, polyfill) { const win = getWindow$2(element); if (isTopLayer(element)) { return win; } if (!isHTMLElement$3(element)) { let svgOffsetParent = getParentNode$2(element); while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { if (isElement$4(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { return svgOffsetParent; } svgOffsetParent = getParentNode$2(svgOffsetParent); } return win; } let offsetParent = getTrueOffsetParent$2(element, polyfill); while (offsetParent && isTableElement$2(offsetParent) && isStaticPositioned(offsetParent)) { offsetParent = getTrueOffsetParent$2(offsetParent, polyfill); } if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { return win; } return offsetParent || getContainingBlock$2(element) || win; } const getElementRects = async function (data) { const getOffsetParentFn = this.getOffsetParent || getOffsetParent$2; const getDimensionsFn = this.getDimensions; const floatingDimensions = await getDimensionsFn(data.floating); return { reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), floating: { x: 0, y: 0, width: floatingDimensions.width, height: floatingDimensions.height } }; }; function isRTL(element) { return getComputedStyle$4(element).direction === 'rtl'; } const platform$1 = { convertOffsetParentRelativeRectToViewportRelativeRect, getDocumentElement: getDocumentElement$2, getClippingRect: getClippingRect$1, getOffsetParent: getOffsetParent$2, getElementRects, getClientRects, getDimensions, getScale, isElement: isElement$4, isRTL }; /** * Modifies the placement by translating the floating element along the * specified axes. * A number (shorthand for `mainAxis` or distance), or an axes configuration * object may be passed. * @see https://floating-ui.com/docs/offset */ const offset$3 = offset$4; /** * Optimizes the visibility of the floating element by choosing the placement * that has the most space available automatically, without needing to specify a * preferred placement. Alternative to `flip`. * @see https://floating-ui.com/docs/autoPlacement */ const autoPlacement = autoPlacement$1; /** * Optimizes the visibility of the floating element by shifting it in order to * keep it in view when it will overflow the clipping boundary. * @see https://floating-ui.com/docs/shift */ const shift = shift$1; /** * Optimizes the visibility of the floating element by flipping the `placement` * in order to keep it in view when the preferred placement(s) will overflow the * clipping boundary. Alternative to `autoPlacement`. * @see https://floating-ui.com/docs/flip */ flip$3; /** * Provides data that allows you to change the size of the floating element — * for instance, prevent it from overflowing the clipping boundary or match the * width of the reference element. * @see https://floating-ui.com/docs/size */ size; /** * Provides data to hide the floating element in applicable situations, such as * when it is not in the same clipping context as the reference element. * @see https://floating-ui.com/docs/hide */ hide$2; /** * Provides data to position an inner element of the floating element so that it * appears centered to the reference element. * @see https://floating-ui.com/docs/arrow */ const arrow$2 = arrow$3; /** * Provides improved positioning for inline reference elements that can span * over multiple lines, such as hyperlinks or range selections. * @see https://floating-ui.com/docs/inline */ inline; /** * Built-in `limiter` that will stop `shift()` at a certain point. */ limitShift; /** * Computes the `x` and `y` coordinates that will place the floating element * next to a given reference element. */ const computePosition = (reference, floating, options) => { // This caches the expensive `getClippingElementAncestors` function so that // multiple lifecycle resets re-use the same result. It only lives for a // single call. If other functions become expensive, we can add them as well. const cache = new Map(); const mergedOptions = { platform: platform$1, ...options }; const platformWithCache = { ...mergedOptions.platform, _c: cache }; return computePosition$1(reference, floating, { ...mergedOptions, platform: platformWithCache }); }; class MagicLinkPopover { /** * @param {Object} options - 配置选项 * @param {HTMLElement} options.parent - 父元素 * @param {string} options.selector - 子元素选择器 * @param {string} [options.titleAttr='popoverTitle'] - 标题对应的 dataset 属性名 * @param {string} [options.contentAttr='popoverContent'] - 内容对应的 dataset 属性名 * @param {string} [options.placement='top'] - 弹出位置 * @param {number} [options.offsetDistance=8] - 偏移距离 * @param {number} [options.showDelay=200] - 显示延迟时间(ms) * @param {number} [options.hideDelay=200] - 隐藏延迟时间(ms) * @param {string} [options.theme='light'] - 主题 */ constructor(options) { this.parent = options.parent; this.selector = options.selector; this.titleAttr = options.titleAttr || 'title'; this.contentAttr = options.contentAttr || 'content'; this.placement = options.placement || 'top'; this.offsetDistance = options.offsetDistance || 8; this.showDelay = options.showDelay || 100; this.hideDelay = options.hideDelay || 200; this.theme = options.theme || 'light'; this.previewFn = options.preview || null; this.popoverElement = null; this.arrowElement = null; this.showTimeout = null; this.hideTimeout = null; this.currentTarget = null; // 绑定方法的 this 上下文 this.handleMouseOver = this.handleMouseOver.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); this.handlePopoverMouseOver = this.handlePopoverMouseOver.bind(this); this.handlePopoverMouseOut = this.handlePopoverMouseOut.bind(this); this.handlePopoverClick = this.handlePopoverClick.bind(this); this.init(); } /** * 初始化 Popover */ init() { this.createPopoverElement(); this.bindEvents(); } /** * 创建 Popover 元素 */ createPopoverElement() { // 创建 popover 容器 this.popoverElement = document.createElement('div'); this.popoverElement.className = `floating-popover floating-popover--${this.theme}`; this.popoverElement.style.cssText = ` position: absolute; top: 0; left: 0; z-index: 9999; visibility: hidden; opacity: 0; background: ${this.theme === 'dark' ? '#333' : '#fff'}; color: ${this.theme === 'dark' ? '#fff' : '#333'}; border-radius: 4px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); padding: 0; transition: opacity 0.3s, visibility 0.3s; max-width: 600px; font-size: 14px; `; const styleElement = document.createElement('style'); styleElement.innerHTML = ` .floating-popover .floating-popover__content table td { border: 1px solid #cacaca; padding: 3px 5px; } .floating-popover .floating-popover__content table tr.firstRow td { background-color: #464646; } `; // 创建标题区域 const titleElement = document.createElement('div'); titleElement.className = 'floating-popover__title'; titleElement.style.cssText = ` padding: 8px 12px; font-weight: bold; border-bottom: 1px solid ${this.theme === 'dark' ? '#444' : '#eee'}; `; // 创建内容区域 const contentElement = document.createElement('div'); contentElement.className = 'floating-popover__content'; contentElement.style.cssText = ` padding: 12px; `; // 创建箭头元素 this.arrowElement = document.createElement('div'); this.arrowElement.className = 'floating-popover__arrow'; this.arrowElement.style.cssText = ` position: absolute; width: 8px; height: 8px; background: inherit; transform: rotate(45deg); `; // 组装 popover this.popoverElement.appendChild(styleElement); this.popoverElement.appendChild(titleElement); this.popoverElement.appendChild(contentElement); this.popoverElement.appendChild(this.arrowElement); // 添加到 body document.body.appendChild(this.popoverElement); } /** * 绑定事件 */ bindEvents() { // 使用事件代理监听鼠标移入事件 this.parent.addEventListener('mouseover', this.handleMouseOver); this.parent.addEventListener('mouseout', this.handleMouseOut); // 监听 popover 自身的鼠标事件 if (this.popoverElement) { this.popoverElement.addEventListener('mouseover', this.handlePopoverMouseOver); this.popoverElement.addEventListener('mouseout', this.handlePopoverMouseOut); this.popoverElement.addEventListener('click', this.handlePopoverClick); } } /** * 处理鼠标移入事件 * @param {MouseEvent} event - 鼠标事件对象 */ handleMouseOver(event) { const target = event.target; const matchedElement = this.findMatchedElement(target); if (matchedElement) { this.currentTarget = matchedElement; this.scheduleShow(); } } /** * 处理鼠标移出事件 * @param {MouseEvent} event - 鼠标事件对象 */ handleMouseOut(event) { const relatedTarget = event.relatedTarget; const isToPopover = this.popoverElement?.contains(relatedTarget); if (!isToPopover) { this.scheduleHide(); } } /** * 处理 Popover 鼠标移入事件 */ handlePopoverMouseOver() { if (this.hideTimeout) { window.clearTimeout(this.hideTimeout); this.hideTimeout = null; } } handlePopoverClick(event) { document.createElement('div'); if (event.target?.nodeName === 'IMG') { let src = event.target.src; let notAllow = event.target.getAttribute('notallowgallery'); if (notAllow !== '1' && this.previewFn) { this.previewFn({ multiple: true, nowImgIndex: 0, imgList: [{ url: src, title: '', description: '' }] }); } } } /** * 处理 Popover 鼠标移出事件 * @param {MouseEvent} event - 鼠标事件对象 */ handlePopoverMouseOut(event) { const relatedTarget = event.relatedTarget; const isBackToTarget = this.currentTarget?.contains(relatedTarget); if (!isBackToTarget) { this.scheduleHide(); } } /** * 查找匹配选择器的元素 * @param {HTMLElement} element - 起始元素 * @returns {HTMLElement|null} 匹配的元素或 null */ findMatchedElement(element) { let current = element; // 向上查找匹配选择器的元素,直到父元素 while (current && current !== this.parent) { if (current.matches(this.selector)) { return current; } current = current.parentElement; } return null; } /** * 显示 Popover */ scheduleShow() { if (this.hideTimeout) { window.clearTimeout(this.hideTimeout); this.hideTimeout = null; } if (!this.showTimeout) { this.showTimeout = window.setTimeout(() => { this.show(); this.showTimeout = null; }, this.showDelay); } } /** * 隐藏 Popover */ scheduleHide() { if (this.showTimeout) { window.clearTimeout(this.showTimeout); this.showTimeout = null; } if (!this.hideTimeout) { this.hideTimeout = window.setTimeout(() => { this.hide(); this.hideTimeout = null; }, this.hideDelay); } } /** * 显示 Popover */ async show() { if (!this.popoverElement || !this.currentTarget) return; // 获取标题和内容 const title = this.currentTarget.dataset[this.titleAttr] || ''; const content = this.currentTarget.dataset[this.contentAttr] || ''; // 更新 popover 内容 const titleElement = this.popoverElement.querySelector('.floating-popover__title'); const contentElement = this.popoverElement.querySelector('.floating-popover__content'); if (title) { titleElement.innerHTML = title; titleElement.style.display = 'block'; } else { titleElement.style.display = 'none'; } contentElement.innerHTML = content; // 计算位置 if (this.arrowElement) { const { x, y, placement, middlewareData } = await computePosition(this.currentTarget, this.popoverElement, { placement: this.placement, middleware: [offset$3(this.offsetDistance), autoPlacement({ allowedPlacements: ['top', 'bottom', 'left', 'right'] }), shift({ padding: 8 }), arrow$2({ element: this.arrowElement, padding: 4 })] }); // 设置 popover 位置 Object.assign(this.popoverElement.style, { left: `${x}px`, top: `${y}px`, visibility: 'visible', opacity: '1' }); // 设置箭头位置 const { x: arrowX, y: arrowY } = middlewareData.arrow || { x: 0, y: 0 }; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]] || 'bottom'; Object.assign(this.arrowElement.style, { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', [staticSide]: '-4px' }); } } /** * 隐藏 Popover */ hide() { if (this.popoverElement) { this.popoverElement.style.visibility = 'hidden'; this.popoverElement.style.opacity = '0'; } this.currentTarget = null; } /** * 销毁 Popover 实例 */ destroy() { // 移除事件监听 this.parent.removeEventListener('mouseover', this.handleMouseOver); this.parent.removeEventListener('mouseout', this.handleMouseOut); if (this.popoverElement) { this.popoverElement.removeEventListener('mouseover', this.handlePopoverMouseOver); this.popoverElement.removeEventListener('mouseout', this.handlePopoverMouseOut); this.popoverElement.removeEventListener('mouseout', this.handlePopoverClick(event)); // 移除 DOM 元素 document.body.removeChild(this.popoverElement); this.popoverElement = null; this.arrowElement = null; } // 清除定时器 if (this.showTimeout) { window.clearTimeout(this.showTimeout); this.showTimeout = null; } if (this.hideTimeout) { window.clearTimeout(this.hideTimeout); this.hideTimeout = null; } } } let _that = {}; /** * * @param { * textBookData:"教材信息", * catalogList:"目录信息", * looseLeafData:"所有活页内容", * isTrial:"是否试读", * textBookMergeLevel:"教材合并到目录的层级", * trialChapterList:"试读章节列表" * } * * @returns */ const formatTextBook = data => { // _that = { ..._that, ...data } _that = { contentList: [], componentIndex: 0, componentTotal: 0, chapterIndex: 0, componentList: [], isTrial: false, isTrialIndex: 10, textBookMergeLevel: 1, trialChapterList: [], firstLevelLabel: '', firstLevelChapterId: '', locationChapterId: '', locationLabel: '', readableChapters: [], chapterData: {}, componentData: {}, textBookResource: { imageList: [], audioList: [], videoList: [], animationList: [], curriculumList: [], resourceList: [], formulaList: [], interactionList: [], questionList: [], testPaperList: [], graphList: [], tableList: [], imageTotal: 0, audioTotal: 0, videoTotal: 0, animationTotal: 0, curriculumTotal: 0, resourceTotal: 0, formulaTotal: 0, interactionTotal: 0, questionTotal: 0, testPaperTotal: 0, graphTotal: 0, tableTotal: 0, teachingResourceTotal: 0, imageCurrent: 0, audioCurrent: 0, videoCurrent: 0, animationCurrent: 0, curriculumCurrent: 0, resourceCurrent: 0, formulaCurrent: 0, interactionCurrent: 0, questionCurrent: 0, tableCurrent: 0, graphCurrent: 0 }, ...data }; return new Promise(resolve => { const getItemByChild = (list, level, orderNum) => { for (let i = 0; i < list.length; i++) { let it = list[i]; if (_that.looseLeafData[it.id] && _that.looseLeafData[it.id].looseLeafInfo) { if (JSON.parse(_that.looseLeafData[it.id].looseLeafInfo).length > 0) { // 更新组件总数 _that.componentTotal += JSON.parse(_that.looseLeafData[it.id].looseLeafInfo).length; it.xmlCompRef = JSON.parse(_that.looseLeafData[it.id].looseLeafInfo)[0].xmlCompRef; _that.looseLeafData[it.id].label = it.label; } else { _that.componentTotal += 1; it.xmlCompRef = v4(); } } else { _that.componentTotal += 1; it.xmlCompRef = v4(); } it.chapterOrderNum = orderNum ? orderNum : i; it.catalogLevel = level; _that.contentList.push(it); if (it.children) { getItemByChild(it.children, level + 1, orderNum ? orderNum : i); } } }; getItemByChild(_that.catalogList, 1, ''); // 处理数据格式 _that.chapterIndex = 0; formattedData(_that.contentList[0].id, resolve); //@format }); }; // 处理活页json数据信息 传入id获取数据解析为list function formattedData(chapterId, resolve) { let list = []; if (_that.looseLeafData[chapterId] && _that.looseLeafData[chapterId].looseLeafInfo) { if (JSON.parse(_that.looseLeafData[chapterId].looseLeafInfo).length > 0) { list = JSON.parse(_that.looseLeafData[chapterId].looseLeafInfo); } else { list = [{ componentsName: 'xml-single-placeholder-group', xmlCompRef: _that.contentList[_that.chapterIndex].xmlCompRef, data: {} }]; } } else { list = [{ componentsName: 'xml-single-placeholder-group', xmlCompRef: _that.contentList[_that.chapterIndex].xmlCompRef, data: {} }]; } let source = { imageList: [], //图片列表 audioList: [], //音频列表 videoList: [], //视频列表 animationList: [], //动画列表 curriculumList: [], //微课列表 formulaList: [], //公式列表 interactionList: [], //互动列表 resourceList: [], //资源列表 questionList: [], //试题列表 testPaperList: [], //试卷列表 graphList: [], //图谱列表 tableList: [] //表格列表 }; if (_that.contentList[_that.chapterIndex].catalogLevel == 1) { _that.firstLevelLabel = _that.contentList[_that.chapterIndex].label; _that.firstLevelChapterId = chapterId; _that.chapterData[_that.firstLevelChapterId] = {}; _that.chapterData[_that.firstLevelChapterId].startIndex = _that.componentIndex; _that.chapterData[_that.firstLevelChapterId].endIndex = _that.componentIndex; } // 设置当前位置的章节ID和名称。 若当前目录层级小于等于教材合并到目录的层级 if (_that.contentList[_that.chapterIndex].catalogLevel <= _that.textBookMergeLevel) { _that.locationChapterId = chapterId; _that.locationLabel = _that.contentList[_that.chapterIndex].label; } let readIndex = Number(_that.componentTotal * (_that.isTrialIndex / 100)).toFixed(0); //可以试读到的组件index let startIndex = _that.componentIndex; let chapterData = _that.catalogList.find(item => item.id == _that.firstLevelChapterId); list.map((element, index) => { let isTrialExternal = _that.isTrial ? _that.componentIndex < readIndex || _that.trialChapterList.includes(chapterId) ? false : true : false; // 向数据里添加位置信息,第一位:章节Id 、第二位:模块Id let location = `xml-digital-teaching/${chapterId}/${element.xmlCompRef}`; element.location = location; element.chapterId = chapterId; element.componentIndex = _that.componentIndex; element.label = _that.contentList[_that.chapterIndex].label; //章节名字 element.firstLevelId = _that.firstLevelChapterId; //章节所在一级标题的Id element.firstLevelLabel = _that.firstLevelLabel; //章节所在一级标题的名称 element.locationChapterId = _that.locationChapterId; //当前内容阅读所在目录id element.locationLabel = _that.locationLabel; //当前内容阅读所在目录名称 element.chapterOrderNum = _that.contentList[_that.chapterIndex].chapterOrderNum; //当前内容阅读所在目录顺序 element.startIndex = startIndex; //章节开始位置 element.endIndex = startIndex + list.length; //章节结束位置 element.endLevelId = chapterData; //章节结束id _that.componentIndex++; _that.chapterData[_that.firstLevelChapterId].endIndex = _that.componentIndex; // 处理文本图片 const processTextImages = e => { e?.html?.replace(/