quicktab
Version:
Multi IFrame tab plugin. operate IFrame like operating browser tabs
1,945 lines (1,638 loc) • 60.5 kB
JavaScript
import extend from 'just-extend'
import Constants from './constants'
import Utils from './utils'
import OptionsSchema from './schema/options'
import TabSchema from './schema/tab'
import get from 'just-safe-get'
import set from 'just-safe-set'
import Storagetify from 'storagetify'
import Delegateify from 'delegateify'
import debounce from 'just-debounce-it'
import mitt from 'mitt'
import { kebabCase } from 'change-case'
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from '@floating-ui/dom'
class Quicktab {
// 选项
#options
// 传入的id查找对应的元素
#element
// quicktab的真实的容器元素
#containerEl
// 整个工具条
#toolbarEl
// tab滚动区域包裹层
#toolbarItemTabWrapperEl
// 工具栏-item-下拉菜单按钮
#toolbarItemDropdownEl
// tab的面板区域包裹层
#tabBodyEl
// 缓存句柄
#cacheHandle
// tab持久化的缓存key
#cacheKey
// 初始化缓存选项
#cacheDefaultTabsKey
// 右键菜单的列表组
#contextmenuEl
// 下拉菜单的元素
#dropdownEl
// tab右键菜单floatui自动更新的监听器
#contextmenuCleanup
//下拉菜单floatui自动更新的监听器
#dropdownCleanup
//工具栏需要隐藏的项目
#hideItemSelector
//激活tab居中防抖函数
#debounceCenterActive
// 下拉菜单的一些组成部分
#openTabsSubtitleEl
#openTabsOriginalListEl
#openTabsearchResultsEl
#recentlyClosedTabsSubtitleEl
#recentlyClosedTabsOriginalListEl
#recentlyClosedTabssearchResultsEl
#noResultsMessageEl
// 用以存储最近关闭的缓存的key
#cacheRecentlyClosedTabsKey
#emitter
// 实例集合
static #instances = new Map()
constructor(element, options) {
options = (typeof options === 'object' && options) || {}
if (typeof element === 'string') {
//如果是字符串则当作是选择器
this.#element = document.querySelector(element)
if (!this.#element) {
return Utils.notify(`Invalid element`)
}
} else {
//直接是元素
this.#element = element
}
//合并默认参数
this.#options = extend(
true,
{},
Constants.DEFAULTS,
this.#parseDataOptions(),
options,
)
//参数额外处理
this.#optionsProcess(options)
//参数验证
const result = Utils.validate(OptionsSchema, this.#options)
if (result !== true) {
return Utils.notify(result)
}
if (Quicktab.#instances.has(this.#options.id)) {
return Utils.notify(`The ID ${this.#options.id} has already been used`)
}
//初始化事件调度
this.#emitter = mitt()
// this.on('onTabActivated', this.#options.onTabActivated);
//立马隐藏挂载元素
this.#element.style.setProperty('display', 'none')
Quicktab.#instances.set(this.#options.id, this)
//合并一下单个tab的选项,因为默认单个Tab选项可以只传递url,那么此时需要合并别的选项
this.#options.defaultTabs = this.#options.defaultTabs.map((option) =>
this.#tabOptionExtend(option),
)
this.#cacheKey = `${Constants.NAMESPACE}-${this.#options.id}`
this.#cacheDefaultTabsKey = `${this.#cacheKey}-defaultTabs`
this.#cacheRecentlyClosedTabsKey = `${this.#cacheKey}-recentlyClosed`
this.#hideItemSelector = this.#options.responsive.hide?.map((item) => {
return `.${Constants.CLASSES.toolbar} li.${item}`
})
//这里要注意:实例化多个实例的时候,页面大小可能会改变,会触发该防抖函数
this.#debounceCenterActive = debounce(() => {
this.scrollToTabByUrl(this.#getTabUrl(this.#getActiveTab()))
}, 500)
//初始化
this.#init()
//初始化完毕调用init
this.#trigger('onInit')
}
// 选项的二次加处理
#optionsProcess(options) {
//处理responsive.hide,如果用户传递了该参数,那么直接替换整个数据而不是extend会从索引为0开始逐个替换,这是符合正常的人类逻辑
const hideItemKey = 'responsive.hide'
const hideVal = get(options, hideItemKey)
if (hideVal !== undefined) {
set(this.#options, hideItemKey, get(options, hideItemKey))
}
}
// 获取选项参数
#parseDataOptions() {
return Utils.parseDataOptions(
this.#element,
Constants.OPTIONS.Default,
'data-qt-',
)
}
/**
* public API
* ====================================================
*/
// 直接通过id可以得到实例
static get(id) {
return Quicktab.#instances.get(id)
}
/**
* 关闭所有的可关闭的tabs
*/
closeAllTabs() {
this.#closeTabsByElements(this.#getTabs())
}
/**
* 添加tab
* @param {Object} option
* @returns
*/
addTab(option) {
//参数合并
option = this.#tabOptionExtend(option)
const result = Utils.validate(TabSchema, option)
if (result !== true) {
return Utils.notify(result)
}
const url = option.url
if (!this.#getTabByUrl(url)) {
//如果这个tab不存在
let maxNum = this.#options.tab.maxNum
if (maxNum > 0) {
let closableTabs = this.#getClosableTabs() //获取所有的可删除的tab
if (maxNum === 1) {
//如果只保留一个,那么就把所有的tab给删除掉,因为添加的当前tab将会作为最新的1个tab
for (let tab of closableTabs) {
this.#removeTabByUrl(this.#getTabUrl(tab))
}
} else {
if (closableTabs.length >= maxNum) {
//得到需要排除的tab
closableTabs.slice(0, -(maxNum - 1)).forEach((tab) => {
this.#removeTabByUrl(this.#getTabUrl(tab))
})
}
}
}
const tabEl = Utils.createNode(this.#generateTabHtml(option))
//给选项增加一个时间戳
option[Constants.DATAKEYS.tabOptionTimeKey] = Date.now()
tabEl[Constants.DATAKEYS.tabOptionDataKey] = option
this.#toolbarItemTabWrapperEl.appendChild(tabEl)
//添加进缓存
this.#addCacheTab(option)
}
//激活这个被添加的tab
this.activeTabByUrl(url, true)
}
/**
* 根据url来关闭tab
* @param {String} url
*/
closeTabByUrl(url) {
if (this.#isActiveTabByUrl(url)) {
//判断是否是激活的tab
//下一个即将激活的tab
let nextTab
let tab = this.#getTabByUrl(url)
if (tab?.nextElementSibling) {
//如果后面有就激活后面的
nextTab = tab.nextElementSibling
} else if (tab?.previousElementSibling) {
nextTab = tab.previousElementSibling
}
//删除tab
this.#removeTabByUrl(url)
//激活tab
this.activeTabByUrl(this.#getTabUrl(nextTab))
} else {
this.#removeTabByUrl(url)
}
}
/**
* 滚动到指定url对应的tab的位置
* @param {String} url
*/
scrollToTabByUrl(url) {
this.#scrollToTabByUrl(url)
}
/**
* 上滚动
*/
prevScroll() {
this.#toolbarItemTabWrapperEl.scrollTo({
left:
this.#toolbarItemTabWrapperEl.scrollLeft -
this.#toolbarItemTabWrapperEl.offsetWidth,
behavior: 'smooth',
})
}
/**
* 下滚动
*/
nextScroll() {
this.#toolbarItemTabWrapperEl.scrollTo({
left:
this.#toolbarItemTabWrapperEl.scrollLeft +
this.#toolbarItemTabWrapperEl.offsetWidth,
behavior: 'smooth',
})
}
/**
* 根据url来刷新tab
* @param {String} url
*/
refreshTabByUrl(url) {
//判断tab是否存在,不存在则不执行
if (!Utils.isDOMElement(this.#getTabByUrl(url))) {
return
}
if (!this.#getTabPaneByUrl(url)) {
this.#addTabPaneByUrl(url)
} else {
//首先必须尝试添加loading层
this.#addLoadingByUrl(url)
// 刷新逻辑
!this.#getIFrameByUrl(url)
? this.#addIFrameByUrl(url)
: this.#refreshIFrameByUrl(url)
}
}
/**
* 根据url全屏显示tab
* @param {String} url
*/
fullscreenTabByUrl(url) {
this.activeTabByUrl(url)
this.#getTabPaneByUrl(url).requestFullscreen()
}
/**
* 刷新当前激活的tab
*/
refreshActiveTab() {
this.refreshTabByUrl(this.#getTabUrl(this.#getActiveTab()))
}
/**
* 当前激活的tab全屏显示
*/
fullscreenActiveTab() {
this.fullscreenTabByUrl(this.#getTabUrl(this.#getActiveTab()))
}
/**
* 滚动到当前激活的tab所在位置
*/
scrollToActiveTab() {
this.scrollToTabByUrl(this.#getTabUrl(this.#getActiveTab()))
}
/**
* 在浏览器新选项卡打开指定url的tab
* @param {String} url
*/
openNewTabByUrl(url) {
window.open(url, '_blank')
}
/**
* 关闭当前激活的Tab,这个api特别有用
* @returns Quicktab
*/
closeActiveTab() {
this.closeTabByUrl(this.#getTabUrl(this.#getActiveTab()))
return this
}
/**
* 关闭所有的除了指定url的选项卡
* @param {String} url
*/
closeAllTabsExceptByUrl(url) {
this.#closeTabsByElements(
this.#getTabs().filter((tab) => this.#getTabUrl(tab) !== url),
)
}
/**
* 关闭除了指定url的tab前面所有的选项卡
* @param {String} url
*/
closePrevAllTabsByUrl(url) {
this.#closeTabsByElements(Utils.prevAll(this.#getTabByUrl(url)))
}
/**
* 关闭除了指定url的tab后面所有的选项卡
* @param {String} url
*/
closeNextAllTabsByUrl(url) {
this.#closeTabsByElements(Utils.nextAll(this.#getTabByUrl(url)))
}
/**
* 获取指定url的tab的contentWindow对象
* @param {String} url
*/
getTabWindowByUrl(url) {
const iframe = this.#getIFrameByUrl(url)
if (this.#canAccessIFrame(iframe)) {
return iframe.contentWindow
}
return undefined
}
/**
* private API
* ====================================================
*/
/**
* 根据传入的iframe的dom判断是否可以访问
* @param {Element|Document} iframeEl
* @returns
*/
#canAccessIFrame(iframeEl) {
return (
Utils.isDOMElement(iframeEl) &&
iframeEl[Constants.DATAKEYS.iframeLoaded] === true &&
Utils.isSameOriginIframe(iframeEl)
)
}
/**
* 根据 DOM 元素数组关闭选项卡
* @param {Array} tabElements - 包含选项卡 DOM 元素的数组
*/
#closeTabsByElements(tabElements) {
tabElements.forEach((tabEl) => {
const tabUrl = this.#getTabUrl(tabEl)
if (this.#isClosableTabByUrl(tabUrl)) {
this.closeTabByUrl(tabUrl)
}
})
}
#getActiveTab() {
return this.#getTabs().find((tabEl) =>
tabEl.classList.contains(Constants.CLASSES.tabActive),
)
}
#getTabs() {
return Array.from(
this.#toolbarItemTabWrapperEl.querySelectorAll(
`button[${Constants.DATAKEYS.tabUrl}]`,
),
)
}
/**
* 判断是否是可关闭的tab
* @param {String} url
* @returns
*/
#isClosableTabByUrl(url) {
return this.#getTabByUrl(url)?.querySelector('svg') ? true : false
}
// 获取所有可关闭的tab
#getClosableTabs() {
return this.#getTabs().filter((tabEl) =>
this.#isClosableTabByUrl(this.#getTabUrl(tabEl)),
)
}
#getTabByUrl(url) {
return this.#toolbarItemTabWrapperEl.querySelector(
`[${Constants.DATAKEYS.tabUrl}="${url}"]`,
)
}
#getTabPaneByUrl(url) {
return this.#tabBodyEl.querySelector(
`[${Constants.DATAKEYS.tabUrl}="${url}"]`,
)
}
#getActiveTabPane() {
return this.#tabBodyEl.querySelector(
`li.${Constants.CLASSES.tabPaneActive}`,
)
}
/**
* 根据url来判断tab是否已经激活
* @param {String} url
* @returns
*/
#isActiveTabByUrl(url) {
return this.#getTabByUrl(url)?.classList.contains(
Constants.CLASSES.tabActive,
)
? true
: false
}
// 关闭loading层
#clsoeLoadingByUrl(url) {
this.#getTabPaneByUrl(url)
?.querySelector(`.${Constants.CLASSES.overlays}`)
?.remove()
}
#init() {
this.#initLocale()
this.#initCache()
this.#initContainer()
this.#initContextmenu()
this.#initDropdown()
this.#initEvent()
this.#initTabs()
}
#initDropdown() {
if (this.#options.toolbar.dropdown.enable === false) return
const dropdownOptions = this.#options.toolbar.dropdown
//组织html
const html = [Utils.sprintf(Constants.HTML.dropdown[0], this.#options.id)]
const placeholderText =
dropdownOptions.searchInput.placeholder.trim() !== ''
? dropdownOptions.searchInput.placeholder
: this.#options.formatSearchInputPlaceholder()
html.push(
Utils.sprintf(
Constants.HTML.dropdownHeader,
dropdownOptions.searchInput.prefixIcon,
placeholderText,
),
)
// 加入body的开始标记
html.push(Constants.HTML.dropdownBody[0])
//打开的标签的副标题
const openedTabsText =
dropdownOptions.openedTabs.text.trim() !== ''
? dropdownOptions.openedTabs.text
: this.#options.formatOpenedTabs()
html.push(
Utils.sprintf(Constants.HTML.dropdownBodySticky, '', openedTabsText, ''),
)
//插入两个section(一个存储原来的结果,一个是展示搜索结果,展示搜索结果的默认要隐藏)
html.push(Constants.HTML.dropdownBodySection)
html.push(
Utils.setProperty(
Constants.HTML.dropdownBodySection,
['.section'],
'display',
'none',
),
)
//插入第二个副标题
const recentlyClosedTabsText =
dropdownOptions.recentlyClosedTabs.text.trim() !== ''
? dropdownOptions.recentlyClosedTabs.text
: this.#options.formatRecentlyClosedTabs()
html.push(
Utils.sprintf(
Constants.HTML.dropdownBodySticky,
Constants.CLASSES.dropdownBodyStickyHasIcon,
recentlyClosedTabsText,
Utils.sprintf(
Constants.HTML.iconWrapper,
'tabindex="0"',
dropdownOptions.recentlyClosedTabs.hideIcon,
),
),
)
//这两个section外面再套一个div
html.push('<div>')
//再插入两个section
html.push(Constants.HTML.dropdownBodySection)
html.push(
Utils.setProperty(
Constants.HTML.dropdownBodySection,
['.section'],
'display',
'none',
),
)
html.push('</div>')
//再插入一个无数据时的dom
const noResultsText =
dropdownOptions.emptyText.trim() !== ''
? dropdownOptions.emptyText
: this.#options.formatSearchNoResults()
html.push(Utils.sprintf(Constants.HTML.dropdownEmpty, noResultsText))
html.push(Constants.HTML.dropdownBody[1])
html.push(Constants.HTML.dropdown[1])
//插入到body中去
document.body.insertAdjacentHTML('beforeEnd', html.join(''))
//查找右键菜单
this.#dropdownEl = document.querySelector(
`[${Constants.DATAKEYS.dropdown}="${this.#options.id}"]`,
)
//查找一些必须的元素
const allSticky = this.#dropdownEl.querySelectorAll('.sticky')
const allSection = this.#dropdownEl.querySelectorAll('ul.section')
this.#openTabsSubtitleEl = allSticky[0]
this.#openTabsOriginalListEl = allSection[0]
this.#openTabsearchResultsEl = allSection[1]
this.#recentlyClosedTabsSubtitleEl = allSticky[1]
this.#recentlyClosedTabsOriginalListEl = allSection[2]
this.#recentlyClosedTabssearchResultsEl = allSection[3]
this.#noResultsMessageEl = this.#dropdownEl.querySelector('.empty')
}
#initLocale() {
if (this.#options.lang) {
const langs = Quicktab.LANGS
const parts = this.#options.lang.split(/-|_/)
parts[0] = parts[0].toLowerCase()
if (parts[1]) {
parts[1] = parts[1].toUpperCase()
}
let langsToExtend = {}
if (langs[this.#options.lang]) {
langsToExtend = langs[this.#options.lang]
} else if (langs[parts.join('-')]) {
langsToExtend = langs[parts.join('-')]
} else if (langs[parts[0]]) {
langsToExtend = langs[parts[0]]
}
for (const [formatName, func] of Object.entries(langsToExtend)) {
if (this.#options[formatName] !== Quicktab.DEFAULTS[formatName]) {
continue
}
this.#options[formatName] = func
}
}
}
//合并单个tab的选项
#tabOptionExtend(option) {
return Object.assign(
{ [Constants.CLASSES.tabActive]: false },
Constants.OPTIONS.TabDefault,
option,
)
}
#initCache() {
this.#cacheHandle = new Storagetify({
type: this.#options.cacheType,
})
}
#initEvent() {
let that = this
const d_document = new Delegateify(document)
const events = ['click', 'contextmenu', 'scroll', 'touchstart', 'dragstart']
events.forEach((event) => {
d_document.on(event, function (event) {
const clickedElement = event.target
const eventType = event.type
//上面的这几种事件全部都要关闭tab的右键菜单
that.#closeContextmenu()
if (['click', 'dragstart'].includes(eventType)) {
//对于下拉菜单只处理点击和拖拽用户体验比较好
if (
!that.#dropdownEl?.contains(clickedElement) &&
!that.#toolbarItemDropdownEl?.contains(clickedElement)
) {
that.#closeDropdown()
}
}
})
})
//响应式处理
Utils.onResize(
this.#element.parentNode,
function (entry) {
//关闭tab右键菜单和下拉菜单
that.#closeContextmenu()
that.#closeDropdown()
if (that.#options.responsive.enable !== false) {
//如果启用了响应式就动态设置显示和隐藏
Utils.setProperty(
that.#containerEl,
that.#hideItemSelector,
'display',
entry.contentRect.width < that.#options.responsive.breakpoint
? 'none'
: null,
)
}
if (that.#options.tab.resizeCenterActive === true) {
that.#debounceCenterActive()
}
},
{
type: 'width',
},
)
//添加通过html属性添加tab的能力(这个非常方便)
that.#dataAttrAddTabEventRegister(document, Constants.DATAKEYS.addTabTarget)
//事件委托监听loading过渡完毕
new Delegateify(this.#containerEl).on(
'transitionend',
`.${Constants.CLASSES.tabBody} .${Constants.CLASSES.overlays}`,
function (event) {
if (event.target === event.matchedTarget) {
const maskEl = event.target
const url = that.#getTabUrl(maskEl)
maskEl.remove()
//tab过渡完毕事件回调
that.#trigger('onTabLoadingTransitionend', url)
}
},
)
//双击事件处理函数
let tabDoubleClickHandle = null
if (that.#options.tab.doubleClick.enable === true) {
tabDoubleClickHandle = function () {
const url = that.#getTabUrl(this)
if (that.#options.tab.doubleClick.refresh === true) {
that.refreshTabByUrl(url)
}
//双击事件回调
that.#trigger('onTabDoubleClick', url)
}
}
new Delegateify(this.#toolbarItemTabWrapperEl).on(
'click',
'button',
Utils.handleClickAndDoubleClick(function () {
let url = that.#getTabUrl(this)
//tab被单击的回调
that.#trigger('onTabClick', url)
//激活
that.activeTabByUrl(url)
//滚动到tab所在位置
if (that.#options.tab.clickCenterActive === true) {
that.scrollToTabByUrl(url)
}
}, tabDoubleClickHandle),
)
//tab关闭事件
if (this.#options.tab.closeBtn.enable !== false) {
new Delegateify(this.#toolbarItemTabWrapperEl).on(
'click',
`button > svg`,
function (event) {
event.stopPropagation() //必须要阻止事件的冒泡,否则会冲突
let tab = this.parentNode
//因为阻止了事件的冒泡传递,因此,需要手动关闭右键菜单
that.#closeContextmenu()
that.closeTabByUrl(that.#getTabUrl(tab))
},
)
}
//给工具栏绑定事件
new Delegateify(this.#toolbarEl).on('click', `li > button`, function () {
let classItem = this.parentNode.getAttribute('class')
switch (classItem) {
case 'fullscreen':
that.fullscreenActiveTab()
break
case 'prev':
that.prevScroll()
break
case 'refresh':
that.refreshActiveTab()
break
case 'next':
that.nextScroll()
break
case 'dropdown':
that.#toggleDropdown()
break
}
})
//鼠标滚动切换
if (this.#options.tab.mouseWheelSwitch.enable !== false) {
//鼠标滚轮切换tab功能启用
let centerTabEl
const withTabPaneDebounce = debounce(function (event) {
const activeTab = that.#getActiveTab()
const prev = activeTab.previousElementSibling
const next = activeTab.nextElementSibling
// 判断滚轮方向,负值表示向上滚动,正值表示向下滚动
const direction = Math.sign(event.deltaY)
if (direction === -1 && prev) {
that.activeTabByUrl(that.#getTabUrl(prev))
centerTabEl = prev
} else if (direction === 1 && next) {
that.activeTabByUrl(that.#getTabUrl(next))
centerTabEl = next
}
that.scrollToTabByUrl(that.#getTabUrl(centerTabEl))
}, 200)
this.#toolbarItemTabWrapperEl.addEventListener(
'wheel',
function (event) {
event.preventDefault() //阻止默认事件,否则它会被外部的滚动条影响
//判断是否启用右键菜单,如果启用就要关闭
if (that.#options.tab.contextmenu !== false) {
that.#closeContextmenu()
}
if (that.#options.tab.mouseWheelSwitch.onlyScroll === true) {
//如果只是滚动
that.#toolbarItemTabWrapperEl.scrollLeft +=
(event.deltaY || event.detail || -event.wheelDelta) / 2
} else {
withTabPaneDebounce(event)
}
},
{ passive: false },
) //{ passive: false }解决控制台的警告错误
}
//是否启用右键菜单功能
if (this.#options.tab.contextmenu.enable !== false) {
//tab右键的事件委托
new Delegateify(this.#toolbarItemTabWrapperEl).on(
'contextmenu',
'button',
function (event) {
let tabEl = this
event.preventDefault()
event.stopPropagation() //必须要防止冒泡,防止和外部的右键事件冲突
//显示右键菜单
that.#showContextmenuByUrl(that.#getTabUrl(tabEl))
},
)
const v_contextmenu = new Delegateify(this.#contextmenuEl)
const contextmenuEvents = ['click', 'contextmenu', 'touchstart']
contextmenuEvents.forEach((event) => {
v_contextmenu.on(
event,
`li[${Constants.DATAKEYS.tabUrl}]`,
function (event) {
if (event.type === 'contextmenu') {
event.preventDefault()
}
const url = that.#getTabUrl(this)
const itemClass = this.getAttribute('class')
switch (itemClass) {
case 'refresh':
that.refreshTabByUrl(url)
break
case 'other':
that.closeAllTabsExceptByUrl(url)
break
case 'prev':
that.closePrevAllTabsByUrl(url)
break
case 'next':
that.closeNextAllTabsByUrl(url)
break
case 'all':
that.closeAllTabs()
break
case 'new-blank':
that.openNewTabByUrl(url)
break
case 'fullscreen':
that.fullscreenTabByUrl(url)
break
case 'center-active':
that.scrollToActiveTab()
break
case 'close':
that.closeTabByUrl(url)
break
}
},
)
})
}
//如果启用拖动排序
if (this.#options.tab.dragSort === true) {
//当前拖动的元素
let dragging = null
new Delegateify(this.#toolbarItemTabWrapperEl).on(
'dragstart',
function (event) {
dragging = event.target
},
)
//拖拽移动中
new Delegateify(this.#toolbarItemTabWrapperEl).on(
'dragover',
function (event) {
event.preventDefault()
// 默认无法将数据/元素放置到其他元素中。如果需要设置允许放置,必须阻止对元素的默认处理方式
let target = event.target
//当前拖动的元素是li 且不等于
if (target.nodeName === 'BUTTON' && target !== dragging) {
// 获取初始位置
let targetRect = target.getBoundingClientRect()
let draggingRect = dragging.getBoundingClientRect()
if (target) {
// 判断是否动画元素
if (target.animated) {
return
}
}
if (Utils.index(dragging) < Utils.index(target)) {
// 目标比元素大,插到其后面
// extSibling下一个兄弟元素
target.parentNode.insertBefore(dragging, target.nextSibling)
} else {
// 目标比元素小,插到其前面
target.parentNode.insertBefore(dragging, target)
}
Utils.animate(draggingRect, dragging)
Utils.animate(targetRect, target)
}
},
)
//拖拽结束
new Delegateify(this.#toolbarItemTabWrapperEl).on('dragend', function () {
dragging = null
//判断是否启用了tab缓存
if (that.#options.tab.remember === true) {
that.#cacheHandle.delete(that.#cacheKey)
that.#getTabs().forEach((item) => {
that.#addCacheTab(item[Constants.DATAKEYS.tabOptionDataKey])
})
}
})
}
//下拉菜单的相关事件
if (this.#options.toolbar.dropdown.enable !== false) {
//是否进行正在合成
let isComposing = false
//点击最近关闭折叠
new Delegateify(this.#dropdownEl).on(
'click',
'.has-icon',
function (event) {
const ul = this.nextElementSibling
const iconWrapper = this.querySelector('.icon-wrapper')
iconWrapper.focus()
if (ul.style.display === 'none') {
iconWrapper.innerHTML =
that.#options.toolbar.dropdown.recentlyClosedTabs.hideIcon
ul.style.display = 'block'
} else {
iconWrapper.innerHTML =
that.#options.toolbar.dropdown.recentlyClosedTabs.showIcon
ul.style.display = 'none'
}
},
)
const inputElementSelector = '.header input'
//input框的事件
new Delegateify(this.#dropdownEl).on(
'input',
inputElementSelector,
function () {
if (isComposing) return
that.#search(this.value)
},
)
new Delegateify(this.#dropdownEl).on(
'compositionstart',
inputElementSelector,
function () {
isComposing = true
},
)
new Delegateify(this.#dropdownEl).on(
'compositionend',
inputElementSelector,
function () {
isComposing = false
that.#search(this.value)
},
)
//每个tab的点击事件
new Delegateify(this.#dropdownEl).on('click', '.section li', function () {
const option = this[Constants.DATAKEYS.tabOptionDataKey]
that.addTab({
title: option.title,
url: option.url,
closable: option.closable,
})
//点击完毕后关闭下拉菜单
that.#closeDropdown()
})
//tab的关闭按钮被单击
new Delegateify(this.#dropdownEl).on(
'click',
'.section li .icon-wrapper',
function (event) {
event.stopPropagation()
const tabLiEl = this.parentNode
const option = tabLiEl[Constants.DATAKEYS.tabOptionDataKey]
that.closeTabByUrl(option.url)
tabLiEl.remove()
},
)
}
}
#restoreBodyElement(element, resultsEl, subtitleEl) {
element.style.display = 'block'
subtitleEl.style.display = 'block'
resultsEl.style.display = 'none'
}
#highlightKeyword(text, keyword) {
const regex = new RegExp(`(${keyword})`, 'gi')
return text.replace(regex, '<span class="highlighted">$1</span>')
}
// 匹配关键词高亮
#matchKeyword(keyword, element, resultsEl, subtitleEl) {
let hasResults = false
Array.from(element.children).forEach((li) => {
const title = li.querySelector('.title').textContent
const url = li.querySelector('.url').textContent.toLowerCase()
if (
title.toLowerCase().includes(keyword) ||
url.toLowerCase().includes(keyword)
) {
hasResults = true
let matchLi = li.cloneNode(true)
//bugfix:克隆会导致自定义属性丢失,重新赋值
matchLi[Constants.DATAKEYS.tabOptionDataKey] =
li[Constants.DATAKEYS.tabOptionDataKey]
matchLi.querySelector('.title').innerHTML = this.#highlightKeyword(
title,
keyword,
)
matchLi.querySelector('.url').innerHTML = this.#highlightKeyword(
url,
keyword,
)
resultsEl.appendChild(matchLi)
}
})
if (hasResults) {
resultsEl.style.display = 'block'
subtitleEl.style.display = 'block'
element.style.display = 'none'
} else {
resultsEl.style.display = 'none'
element.style.display = 'none'
subtitleEl.style.display = 'none'
}
return hasResults
}
#search(keyword) {
keyword = keyword.toLowerCase()
//先清空
this.#openTabsearchResultsEl.innerHTML = ''
this.#recentlyClosedTabssearchResultsEl.innerHTML = ''
if (keyword.trim() !== '') {
let results1 = false
let results2 = false
results1 = this.#matchKeyword(
keyword,
this.#openTabsOriginalListEl,
this.#openTabsearchResultsEl,
this.#openTabsSubtitleEl,
)
results2 = this.#matchKeyword(
keyword,
this.#recentlyClosedTabsOriginalListEl,
this.#recentlyClosedTabssearchResultsEl,
this.#recentlyClosedTabsSubtitleEl,
)
if (results1 === false && results2 === false) {
//说明两个都没找到结果
this.#noResultsMessageEl.style.display = 'block'
} else {
this.#noResultsMessageEl.style.display = 'none'
}
} else {
//隐藏结果
this.#noResultsMessageEl.style.display = 'none'
this.#restoreBodyElement(
this.#openTabsOriginalListEl,
this.#openTabsearchResultsEl,
this.#openTabsSubtitleEl,
)
this.#restoreBodyElement(
this.#recentlyClosedTabsOriginalListEl,
this.#recentlyClosedTabssearchResultsEl,
this.#recentlyClosedTabsSubtitleEl,
)
}
}
// 从元素的data属性上获取Url
#getTabUrl(element) {
return element?.getAttribute(Constants.DATAKEYS.tabUrl)
}
#toggleDropdown() {
if (this.#dropdownEl.classList.contains(Constants.CLASSES.dropdownActive)) {
this.#closeDropdown()
} else {
this.#prepareDropdownData()
this.#showDropdown()
}
}
// 准备下拉菜单的数据
#prepareDropdownData() {
const allOpenedTabs = []
this.#getTabs()?.forEach((tabEl) => {
allOpenedTabs.push(tabEl[Constants.DATAKEYS.tabOptionDataKey])
})
// 未激活的,按照timestamp从小到大排序
const newOrderTabs = allOpenedTabs
.filter((tab) => tab.active === false)
.sort(
(a, b) =>
b[Constants.DATAKEYS.tabOptionTimeKey] -
a[Constants.DATAKEYS.tabOptionTimeKey],
)
const ativeTabs = allOpenedTabs.find((tab) => tab.active === true)
if (ativeTabs) {
newOrderTabs.push(ativeTabs)
}
const closeBtnTpl = Utils.sprintf(
Constants.HTML.iconWrapper,
'',
this.#options.toolbar.dropdown.openedTabs.closeIcon,
)
//创建两个虚拟节点
const openTabsFrag = document.createDocumentFragment()
const timeFormatOptions = this.#options.toolbar.dropdown.timeFormat
//国际化时间文本
const customText = {
second:
timeFormatOptions.seconds.trim() !== ''
? timeFormatOptions.seconds
: this.#options.formatTimeSeconds(),
minutes:
timeFormatOptions.minutes.trim() !== ''
? timeFormatOptions.minutes
: this.#options.formatTimeMinutes(),
hours:
timeFormatOptions.hours.trim() !== ''
? timeFormatOptions.hours
: this.#options.formatTimeHours(),
days:
timeFormatOptions.days.trim() !== ''
? timeFormatOptions.days
: this.#options.formatTimeDays(),
months:
timeFormatOptions.months.trim() !== ''
? timeFormatOptions.months
: this.#options.formatTimeMonths(),
years:
timeFormatOptions.year.trim() !== ''
? timeFormatOptions.year
: this.#options.formatTimeYear(),
}
newOrderTabs.forEach((item, index) => {
const dropdownItemEl = Utils.createNode(
Utils.sprintf(
Constants.HTML.sectionItem,
index === 0 ? Constants.CLASSES.dropdownActive : '',
item.title,
item.url,
Utils.timeAgo(item[Constants.DATAKEYS.tabOptionTimeKey], customText),
item.closable === true ? closeBtnTpl : '',
),
)
dropdownItemEl[Constants.DATAKEYS.tabOptionDataKey] = item
openTabsFrag.appendChild(dropdownItemEl)
})
this.#openTabsOriginalListEl.replaceChildren(openTabsFrag)
//然后准备最近关闭的标签
const recentlyClosedTabsFrag = document.createDocumentFragment()
//获取最近关闭的tabs
this.#cacheHandle
.get(this.#cacheRecentlyClosedTabsKey)
?.sort(
(a, b) =>
b[Constants.DATAKEYS.tabOptionTimeKey] -
a[Constants.DATAKEYS.tabOptionTimeKey],
)
.forEach((item) => {
const recentlyClosedTabEl = Utils.createNode(
Utils.sprintf(
Constants.HTML.sectionItem,
'',
item.title,
item.url,
Utils.timeAgo(
item[Constants.DATAKEYS.tabOptionTimeKey],
customText,
),
'',
),
)
recentlyClosedTabEl[Constants.DATAKEYS.tabOptionDataKey] = item
recentlyClosedTabsFrag.appendChild(recentlyClosedTabEl)
})
this.#recentlyClosedTabsOriginalListEl.replaceChildren(
recentlyClosedTabsFrag,
)
}
//显示下拉菜单
#showDropdown() {
this.#dropdownCleanup?.()
// 注册菜单自动更新位置
this.#dropdownCleanup = autoUpdate(
this.#toolbarItemDropdownEl,
this.#dropdownEl,
this.#updatePosition.bind(
this,
this.#toolbarItemDropdownEl,
this.#dropdownEl,
'bottom-end',
),
)
this.#dropdownEl.classList.add(Constants.CLASSES.dropdownActive)
this.#containerEl.classList.add(Constants.CLASSES.dropdownPEN)
//关闭tab的右键菜单
this.#closeContextmenu()
}
//关闭右键菜单
#closeDropdown() {
this.#dropdownCleanup?.()
this.#dropdownEl?.classList.remove(Constants.CLASSES.dropdownActive)
this.#containerEl.classList.remove(Constants.CLASSES.dropdownPEN)
}
//显示右键菜单
#showContextmenuByUrl(url) {
//判断关闭当前菜单选项是否被启用
if (this.#options.tab.contextmenu.close.enable !== false) {
const listGroupCloseItemEl = this.#contextmenuEl.querySelector(
`.${Constants.CLASSES.listGroupCloseItem}`,
)
const enableSeparator =
this.#options.tab.contextmenu.close.separator === true
if (this.#isClosableTabByUrl(url)) {
listGroupCloseItemEl.style.setProperty('display', null) //是可关闭的,因此需要显示右键菜单的关闭当前
enableSeparator &&
listGroupCloseItemEl.nextElementSibling.style.setProperty(
'display',
null,
)
} else {
listGroupCloseItemEl.style.setProperty('display', 'none')
enableSeparator &&
listGroupCloseItemEl.nextElementSibling.style.setProperty(
'display',
'none',
)
}
}
const tabEl = this.#getTabByUrl(url)
this.#contextmenuCleanup?.()
// 注册菜单自动更新位置
this.#contextmenuCleanup = autoUpdate(
tabEl,
this.#contextmenuEl,
this.#updatePosition.bind(this, tabEl, this.#contextmenuEl, 'top'),
)
//显示右键菜单
this.#contextmenuEl.classList.add(Constants.CLASSES.listGroupActive)
//给iframe添加蒙层
this.#containerEl.classList.add(Constants.CLASSES.contextmenuPEN)
//把url属性也给每一个列表项目设置一遍,方便后续事件的处理
this.#contextmenuEl.querySelectorAll('li').forEach(function (li) {
li.setAttribute(Constants.DATAKEYS.tabUrl, url)
})
//关闭下拉菜单
this.#closeDropdown()
}
//关闭右键菜单
#closeContextmenu() {
this.#contextmenuCleanup?.()
this.#contextmenuEl?.classList.remove(Constants.CLASSES.listGroupActive)
this.#containerEl.classList.remove(Constants.CLASSES.contextmenuPEN)
}
#updatePosition(referenceEl, floatingEl, placement = 'top') {
computePosition(referenceEl, floatingEl, {
placement: placement,
strategy: 'fixed', // 默认是'absolute'
middleware: [
offset(3), //offset(6)必须放在数组最前面,官方文档提示
flip(),
shift({ padding: 10 }),
// arrow({element: arrowElement})
],
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floatingEl.style, {
left: `${x}px`,
top: `${y}px`,
})
})
}
#initContainer() {
const toolbarTabWrapperOpsKey = 'tabWrapper'
const toolbarItemClassMap = {
prev: Constants.CLASSES.toolbarPrevItem,
refresh: Constants.CLASSES.toolbarRefreshItem,
[toolbarTabWrapperOpsKey]: Constants.CLASSES.toolbarTabWrapperItem,
next: Constants.CLASSES.toolbarNextItem,
dropdown: Constants.CLASSES.toolbarDropdownItem,
fullscreen: Constants.CLASSES.toolbarFullscreenItem,
}
let html = [Utils.sprintf(Constants.HTML.container[0], this.#options.id)]
const toolbarHtml = [
Utils.sprintf(
Constants.HTML.toolbar[0],
this.#options.toolbar.hide === true
? Constants.CLASSES.toolbarHide
: '',
),
]
Utils.getEnabledAndSortedOpsKey(
this.#options.toolbar,
toolbarItemClassMap,
).map((key) => {
toolbarHtml.push(
Utils.sprintf(
Constants.HTML.toolbarItem,
toolbarItemClassMap[key],
key === toolbarTabWrapperOpsKey
? ''
: `<button>${this.#options.toolbar[key].icon}</button>`,
),
)
})
//加入工具栏的结尾
toolbarHtml.push(Constants.HTML.toolbar[1])
//排序实现
const pos = this.#options.toolbar.position
if (pos === 'bottom') {
toolbarHtml.unshift(Constants.HTML.tabBody)
} else if (pos === 'top') {
toolbarHtml.push(Constants.HTML.tabBody)
}
html.push(...toolbarHtml)
html.push(Constants.HTML.container[1])
html = html.join('') //转换成字符串
// 隐藏特定的项目
if (
this.#options.responsive.enable !== false &&
this.#element.parentNode.getBoundingClientRect().width <
this.#options.responsive.breakpoint
) {
html = Utils.setProperty(html, this.#hideItemSelector, 'display', 'none')
}
//设置容器的尺寸
const { height, width, minHeight } = this.#options
const containerSelector = `[${Constants.DATAKEYS.container}="${this.#options.id}"]`
html = Utils.setProperty(html, [containerSelector], 'height', height)
html = Utils.setProperty(html, [containerSelector], 'width', width)
html = Utils.setProperty(html, [containerSelector], 'min-height', minHeight)
//插入到挂载元素之后
this.#element.insertAdjacentHTML('afterend', html)
this.#containerEl = document.querySelector(containerSelector)
//查找一些需要的dom
this.#toolbarEl = this.#containerEl.querySelector(
`.${Constants.CLASSES.toolbar}`,
)
this.#toolbarItemTabWrapperEl = this.#containerEl.querySelector(
`.${Constants.CLASSES.toolbar} li.${Constants.CLASSES.toolbarTabWrapperItem}`,
)
this.#tabBodyEl = this.#containerEl.querySelector(
`.${Constants.CLASSES.tabBody}`,
)
//下拉菜单的按钮,弹出下拉菜单时需要使用
this.#toolbarItemDropdownEl = this.#toolbarEl.querySelector(
`.${Constants.CLASSES.toolbarDropdownItem} button`,
)
}
#initContextmenu() {
if (this.#options.tab.contextmenu.enable === false) return
const listGroupItemMap = {
close: {
class: Constants.CLASSES.listGroupCloseItem,
text: this.#options.formatContextmenuClose(),
},
closeOthers: {
class: Constants.CLASSES.listGroupCloseOtherItem,
text: this.#options.formatContextmenuCloseOthers(),
},
closePrev: {
class: Constants.CLASSES.listGroupClosePrevItem,
text: this.#options.formatContextmenuClosePrev(),
},
closeNext: {
class: Constants.CLASSES.listGroupCloseNextItem,
text: this.#options.formatContextmenuCloseNext(),
},
closeAll: {
class: Constants.CLASSES.listGroupCloseAllItem,
text: this.#options.formatContextmenuCloseAll(),
},
fullscreen: {
class: Constants.CLASSES.listGroupFullscreenItem,
text: this.#options.formatContextmenuFullscreen(),
},
refresh: {
class: Constants.CLASSES.listGroupRefreshItem,
text: this.#options.formatContextmenuRefresh(),
},
centerActive: {
class: Constants.CLASSES.listGroupCenterActiveItem,
text: this.#options.formatContextmenuCenterActive(),
},
newBlank: {
class: Constants.CLASSES.listGroupNewBlankItem,
text: this.#options.formatContextmenuNewBlank(),
},
}
const html = [Utils.sprintf(Constants.HTML.listGroup[0], this.#options.id)]
Utils.getEnabledAndSortedOpsKey(
this.#options.tab.contextmenu,
listGroupItemMap,
).map((key) => {
//开始组装字符串
//根据key把配置项都解构出来
const { text, separator } = this.#options.tab.contextmenu[key]
let formatText = listGroupItemMap[key].text
if (text !== '') {
formatText = text
}
html.push(
Utils.sprintf(
Constants.HTML.listGroupItem,
listGroupItemMap[key].class,
formatText,
) + (separator ? Constants.HTML.listGroupSeparatorItem : ''),
)
})
html.push(Constants.HTML.listGroup[1])
//插入到body中去
document.body.insertAdjacentHTML('beforeEnd', html.join(''))
//查找右键菜单
this.#contextmenuEl = document.querySelector(
`[${Constants.DATAKEYS.contextmenu}="${this.#options.id}"]`,
)
}
#initTabs() {
const defaultTabs = this.#options.defaultTabs
const cacheTabs = this.#cacheHandle.get(this.#cacheKey) //获取缓存中的tab
const cacheDefaultTabs = this.#cacheHandle.get(this.#cacheDefaultTabsKey) //获取缓存中的tab
if (this.#options.tab.remember === false) {
const cacheStores = ['local', 'session']
const cacheKeys = [this.#cacheKey, this.#cacheDefaultTabsKey]
for (const store of cacheStores) {
for (const key of cacheKeys) {
this.#cacheHandle.store(store).delete(key)
}
}
this.#restoreTabs(defaultTabs)
return
}
if (
cacheTabs !== null &&
cacheDefaultTabs !== null &&
this.#cacheTabsCheck(cacheTabs) &&
JSON.stringify(defaultTabs) === JSON.stringify(cacheDefaultTabs)
) {
//这里是缓存数据一切正常的情况下,直接回显就行
this.#restoreTabs(cacheTabs, this.#getCacheActiveTab()?.url)
} else {
//必须先设置一遍缓存
this.#cacheHandle.set(this.#cacheKey, defaultTabs)
this.#restoreTabs(defaultTabs)
this.#cacheHandle.set(this.#cacheDefaultTabsKey, defaultTabs)
}
}
//检测缓存中的tab的合法性
#cacheTabsCheck(tabs) {
//要检查的键数组
let targetKeys = [
...Object.keys(Constants.OPTIONS.TabDefault),
Constants.CLASSES.tabActive,
]
return tabs.every((obj) =>
targetKeys.every((key) => Object.hasOwnProperty.call(obj, key)),
)
}
/**
* 恢复tab
* @param {Array} options tab选项数组
* @param {String} url 将要激活tab的url,不传将设置options中的最后一项
* @returns
*/
#restoreTabs(options, url = '') {
if (!Array.isArray(options) || options.length === 0) {
return
}
//创建两个虚拟节点
const tabFrag = document.createDocumentFragment()
//这里只添加所有的tab,不添加iframe,否则全部加载iframe将会卡爆(重点优化)
options.forEach((option, index) => {
//克隆
option = extend(true, {}, option)
const tabNode = Utils.createNode(this.#generateTabHtml(option))
//插入一个时间参数,并把整个对象再次存到这个dom节点对象上,是下拉菜单功能需要使用
if (
!Object.hasOwnProperty.call(option, Constants.DATAKEYS.tabOptionTimeKey)
) {
option[Constants.DATAKEYS.tabOptionTimeKey] =
+`${Date.now()}.${index + 1}`
}
tabNode[Constants.DATAKEYS.tabOptionDataKey] = option
tabFrag.appendChild(tabNode)
})
//添加虚拟节点到tab的容器里面
this.#toolbarItemTabWrapperEl.appendChild(tabFrag)
// 默认激活最后一项
url = url || options.slice(-1)?.[0]?.url
//激活最后一个
this.activeTabByUrl(url)
//滚动到激活tab所在位置
this.#scrollToTabByUrl(url, 'auto')
}
//通过data属性快速添加tab事件注册
#dataAttrAddTabEventRegister(doc, targetKey) {
const that = this
//同时给子页面绑定快速打开tab的事件
new Delegateify(doc).on(
'click',
`[${Constants.DATAKEYS.addTabUrl}][${targetKey}]`,
function (event) {
event.preventDefault()
const target = this
const targetID = target.getAttribute(targetKey).replace(/^#/, '')
const url = target.getAttribute(Constants.DATAKEYS.addTabUrl)
const title = target.getAttribute(Constants.DATAKEYS.addTabTitle)
const closable = target.getAttribute(Constants.DATAKEYS.addTabClosable)
if (Quicktab.#instances.has(targetID)) {
let option = {
...(url !== null && { url }),
...(title !== null && { title }),
...(closable !== null &&
(closable === 'true' || closable === 'false') && {
closable: closable === 'true',
}),
}
option = that.#tabOptionExtend(option)
Quicktab.#instances.get(targetID).addTab(option)
}
},
)
}
//单纯的只做删除的工作
#removeTabByUrl(url) {
//添加进最近关闭的缓存
this.#cacheRecentlyClosedByUrl(url)
//删除tab
this.#getTabByUrl(url)?.remove()
//删除面板
this.#removeTabPaneByUrl(url)
//删除缓存里的tab
this.#removeCacheTabByUrl(url)
//关闭tab的回调
this.#trigger('onTabClose', url)
//这里再增加一个事件,当所有的可关闭的tab都被关闭时会触发一个事件
if (this.#getClosableTabs().length === 0) {
this.#trigger('onTabCloseAll')
}
}
//从缓存中删除最近关闭的tab
#cacheRecentlyClosedByUrl(url) {
//添加最近删除的缓存,从tab的dom中拿到选项进行缓存
let tabEl = this.#getTabByUrl(url)
if (!tabEl) {
return
}
let tabOption = tabEl[Constants.DATAKEYS.tabOptionDataKey]
//更新时间戳为关闭时的时间戳
tabOption[Constants.DATAKEYS.tabOptionTimeKey] = Date.now()
if (this.#cacheHandle.has(this.#cacheRecentlyClosedTabsKey)) {
//先取所有的数组
let all = this.#cacheHandle.get(this.#cacheRecentlyClosedTabsKey)
while (all.length >= 10) {
all.shift()
}
all.push(tabOption)
this.#cacheHandle.set(this.#cacheRecentlyClosedTabsKey, all)
} else {
this.#cacheHandle.set(this.#cacheRecentlyClosedTabsKey, [tabOption])
}
}
// 删除面板
#removeTabPaneByUrl(url) {
//先删除iframe
this.#removeIFrameByUrl(url)
//删除tab面板最外层的容器
this.#getTabPaneByUrl(url)?.remove()
}
//根据url删除缓存里的tab
#removeCacheTabByUrl(url) {
if (this.#options.tab.remember === false) return
let tabs = this.#cacheHandle.get(this.#cacheKey)
tabs?.forEach((tab, index) => {
if (tab.url === url) {
tabs.splice(index, 1)
}
})
this.#cacheHandle.set(this.#cacheKey, tabs)
}
/**
* 激活指定url的Tab
* @param {String} url
* @param {Boolean} scrollToTab 是否滚动到Tab所在位置
* @returns
*/
activeTabByUrl(url, scrollToTab = false) {
const tabEl = this.#getTabByUrl(url)
if (!tabEl || this.#isActiveTabByUrl(url)) {
//过滤掉不存在的tab,或者已经激活的tab
return
}
const activeTabEl = this.#getActiveTab()
activeTabEl?.classList.remove(Constants.CLASSES.tabActive) //把之前激活的tab的激活状态类给删掉
if (activeTabEl && activeTabEl[Constants.DATAKEYS.tabOptionDataKey]) {
activeTabEl[Constants.DATAKEYS.tabOptionDataKey].active = false
}
//添加上激活的类,激活当前tab的dom里存的选项,并更新时间戳
tabEl?.classList.add(Constants.CLASSES.tabActive)
if (tabEl && tabEl[Constants.DATAKEYS.tabOptionDataKey]) {
tabEl[Constants.DATAKEYS.tabOptionDataKey].active = true //设置为true表示已经激活
tabEl[Constants.DATAKEYS.tabOptionDataKey][
Constants.DATAKEYS.tabOptionTimeKey
] = Date.now() //增加一个时间戳
}
//激活缓存中的tab
this.#activeCacheTabByUrl(url)
//并更新缓存里的时间戳
this.#updateCacheTabByUrl(
url,
Constants.DATAKEYS.tabOptionTimeKey,
Date.now(),
)
//判断tab面板是否已经存在,不存在则添加
if (!this.#getTabPaneByUrl(url)) {
this.#addTabPaneByUrl(url)
}
//激活面板
//把之前激活的面板给移除掉
this.#getActiveTabPane()?.classList.remove(Constants.CLASSES.tabPaneActive)
//把当前的tab面板给添加激活类
this.#getTabPaneByUrl(url)?.classList.add(Constants.CLASSES.tabPaneActive)
//激活逻辑完成调用激活事件
this.#trigger('onTabActivated', url)
if (scrollToTab === true) {
this.scrollToTabByUrl(url)
}
}
//根据url来添加面板
#addTabPaneByUrl(url) {
//添加tab面板的容器li元素
this.#tabBodyEl.insertAdjacentHTML(
'beforeEnd',
Utils.sprintf(Constants.HTML.tabBodyItem, url),
)
//加载层逻辑
this.#addLoadingByUrl(url)
//加载iframe
this.#addIFrameByUrl(url)
}
//往tab容器里插入iframe
#addIFrameByUrl(url) {
const that = this
//创建iframe
const iframe = document.createElement('iframe')
//超时逻辑
this.#iFrameTimeoutHandle(url, iframe)
iframe.src = url
iframe.onload = () => {
//销毁定时器
this.#clearIFrameTimeout(iframe)
//设置iframe状态完毕
iframe[Constants.DATAKEYS.iframeLoaded] = true
//判断是否有loading 有的话就执行过渡
this.#getTabLoadingByUrl(url)?.style.setProperty('opacity', 0)
this.#trigger('onTabLoaded', url)
this.#tabFinallyAndAll(url)
if (this.#canAccessIFrame(iframe)) {
//如果是非跨域的iframe
iframe.contentWindow.onbeforeunload = () => {
//遮罩
this.#addLoadingByUrl(url)
//清理掉iframe的状态
delete iframe[Constants.DATAKEYS.iframeLoaded]
//超时处理
this.#iFrameTimeoutHandle(url, iframe)
}
//给子页面绑定通过属性快速注册tab的事件
that.#dataAttrAddTabEventRegister(
iframe.contentDocument,
Constants.DATAKEYS.addTabParentTarget,
)
} else {
//如果是跨域的iframe,所有的逻辑执行完毕后清空onload,因为跨域的iframe,被用户点击重新加载此框架时,无法控制它
iframe.onload = null
}
}
//插入iframe
this.#getTabPaneByUrl(url)?.appendChild(iframe)
}
//iframe的超时处理逻辑
#iFrameTimeoutHandle(url, iframeEl) {
if (this.#options.tab.timeout.enable === false) return
//过滤掉某些需要超时的
let filter = this.#options.tab.timeout.filter.call(this, url)
if (!filter) return
//清除原先的定时器,否则会重复触发
this.#clearIFrameTimeout(iframeEl)
iframeEl[Constants.DATAKEYS.iframeTimeoutTimer] = setTimeout(() => {
this.#removeIFrameByUrl(url) //直接移除iframe停止加载
//如果超时的话,就应该立即移除这个loading层
this.#getTabLoadingByUrl(url)?.remove()
let timeoutHtml = Utils.sprintf(
Constants.HTML.maskWrapper,
url,
Utils.sprintf(
Constants.HTML.timeout,
this.#options.tab.timeout.text.trim() !== ''
? this.#options.tab.timeout.text
: this.#options.formatTimeoutMessage(),
),
)
if (this.#options.tab.timeout.template.trim() !== '') {
timeoutHtml = this.#options.tab.timeout.template
}
let template = Utils.sprintf(Con