Files
zn-ai/packages/chrome-ui/tabs.js
2025-11-15 22:41:50 +08:00

154 lines
3.6 KiB
JavaScript

const { EventEmitter } = require('events')
const { WebContentsView } = require('electron')
const toolbarHeight = 64
class Tab {
constructor(parentWindow, wcvOpts = {}) {
this.invalidateLayout = this.invalidateLayout.bind(this)
// Delete undefined properties which cause WebContentsView constructor to
// throw. This should probably be fixed in Electron upstream.
if (wcvOpts.hasOwnProperty('webContents') && !wcvOpts.webContents) delete wcvOpts.webContents
if (wcvOpts.hasOwnProperty('webPreferences') && !wcvOpts.webPreferences)
delete wcvOpts.webPreferences
this.view = new WebContentsView(wcvOpts)
this.id = this.view.webContents.id
this.window = parentWindow
this.webContents = this.view.webContents
this.window.contentView.addChildView(this.view)
}
destroy() {
if (this.destroyed) return
this.destroyed = true
this.hide()
this.window.contentView.removeChildView(this.view)
this.window = undefined
if (!this.webContents.isDestroyed()) {
if (this.webContents.isDevToolsOpened()) {
this.webContents.closeDevTools()
}
// TODO: why is this no longer called?
this.webContents.emit('destroyed')
this.webContents.destroy()
}
this.webContents = undefined
this.view = undefined
}
loadURL(url) {
return this.view.webContents.loadURL(url)
}
show() {
this.invalidateLayout()
this.startResizeListener()
this.view.setVisible(true)
}
hide() {
this.stopResizeListener()
this.view.setVisible(false)
}
reload() {
this.view.webContents.reload()
}
invalidateLayout() {
const [width, height] = this.window.getSize()
const padding = 4
this.view.setBounds({
x: padding,
y: toolbarHeight,
width: width - padding * 2,
height: height - toolbarHeight - padding,
})
this.view.setBorderRadius(8)
}
// Replacement for BrowserView.setAutoResize. This could probably be better...
startResizeListener() {
this.stopResizeListener()
this.window.on('resize', this.invalidateLayout)
}
stopResizeListener() {
this.window.off('resize', this.invalidateLayout)
}
}
class Tabs extends EventEmitter {
tabList = []
selected = null
constructor(browserWindow) {
super()
this.window = browserWindow
}
destroy() {
this.tabList.forEach((tab) => tab.destroy())
this.tabList = []
this.selected = undefined
if (this.window) {
this.window.destroy()
this.window = undefined
}
}
get(tabId) {
return this.tabList.find((tab) => tab.id === tabId)
}
create(webContentsViewOptions) {
const tab = new Tab(this.window, webContentsViewOptions)
this.tabList.push(tab)
if (!this.selected) this.selected = tab
tab.show() // must be attached to window
this.emit('tab-created', tab)
this.select(tab.id)
return tab
}
remove(tabId) {
const tabIndex = this.tabList.findIndex((tab) => tab.id === tabId)
if (tabIndex < 0) {
throw new Error(`Tabs.remove: unable to find tab.id = ${tabId}`)
}
const tab = this.tabList[tabIndex]
this.tabList.splice(tabIndex, 1)
tab.destroy()
if (this.selected === tab) {
this.selected = undefined
const nextTab = this.tabList[tabIndex] || this.tabList[tabIndex - 1]
if (nextTab) this.select(nextTab.id)
}
this.emit('tab-destroyed', tab)
if (this.tabList.length === 0) {
this.destroy()
}
}
select(tabId) {
const tab = this.get(tabId)
if (!tab) return
if (this.selected) this.selected.hide()
tab.show()
this.selected = tab
this.emit('tab-selected', tab)
}
}
exports.Tabs = Tabs