Files
Andrew Zambazos 553ab6537a Added MacOS SDK
2026-06-11 14:04:52 +12:00

273 lines
8.4 KiB
JavaScript

(function(){
const isNodeContext = typeof module !== 'undefined' && typeof module.exports !== 'undefined'
if (isNodeContext) {
Draggabilly = require('draggabilly')
}
const tabTemplate = `
<div class="chrome-tab">
<div class="chrome-tab-background">
</div>
<div class="chrome-tab-favicon"></div>
<div class="chrome-tab-spinner"></div>
<div class="chrome-tab-title"></div>
<div class="chrome-tab-close"></div>
</div>
`
const defaultTabProperties = {
title: '',
favicon: ''
}
let instanceId = 0
let tabId = 0
class ChromeTabs {
constructor() {
this.draggabillyInstances = []
}
init(el, options) {
this.el = el
this.options = options
this.instanceId = instanceId
this.el.setAttribute('data-chrome-tabs-instance-id', this.instanceId)
instanceId += 1
this.tabId = tabId
this.setupStyleEl()
this.setupEvents()
this.layoutTabs()
this.fixZIndexes()
this.setupDraggabilly()
}
emit(eventName, data) {
this.el.dispatchEvent(new CustomEvent(eventName, { detail: data }))
}
setupStyleEl() {
this.animationStyleEl = document.createElement('style')
this.el.appendChild(this.animationStyleEl)
}
setupEvents() {
window.addEventListener('resize', event => this.layoutTabs())
document.body.querySelector('#chrome-tabs-add-tab').addEventListener('click', event => this.emit('requestNewTab'))
this.el.addEventListener('click', ({target}) => {
if (target.classList.contains('chrome-tab')) {
this.setCurrentTab(target)
} else if (target.classList.contains('chrome-tab-close')) {
this.emit('requestTabClose', {tabEl: target.parentNode})
} else if (target.classList.contains('chrome-tab-title') || target.classList.contains('chrome-tab-favicon')) {
this.setCurrentTab(target.parentNode)
}
})
}
get tabEls() {
return Array.prototype.slice.call(this.el.querySelectorAll('.chrome-tab'))
}
get tabContentEl() {
return this.el.querySelector('.chrome-tabs-content')
}
get tabWidth() {
const tabsContentWidth = this.tabContentEl.clientWidth - this.options.tabOverlapDistance
const width = (tabsContentWidth / this.tabEls.length) + this.options.tabOverlapDistance
return Math.max(this.options.minWidth, Math.min(this.options.maxWidth, width))
}
get tabEffectiveWidth() {
return this.tabWidth - this.options.tabOverlapDistance
}
get tabPositions() {
const tabEffectiveWidth = this.tabEffectiveWidth
let left = 0
let positions = []
this.tabEls.forEach((tabEl, i) => {
positions.push(left)
left += tabEffectiveWidth
})
return positions
}
layoutTabs() {
const tabWidth = this.tabWidth
this.cleanUpPreviouslyDraggedTabs()
this.tabEls.forEach((tabEl) => tabEl.style.width = tabWidth + 'px')
requestAnimationFrame(() => {
let styleHTML = ''
this.tabPositions.forEach((left, i) => {
styleHTML += `
.chrome-tabs[data-chrome-tabs-instance-id="${ this.instanceId }"] .chrome-tab:nth-child(${ i + 1}) {
transform: translate3d(${ left }px, 0, 0)
}
`
})
this.animationStyleEl.innerHTML = styleHTML
})
}
fixZIndexes() {
const bottomBarEl = this.el.querySelector('.chrome-tabs-bottom-bar')
const tabEls = this.tabEls
tabEls.forEach((tabEl, i) => {
let zIndex = tabEls.length - i
if (tabEl.classList.contains('chrome-tab-current')) {
bottomBarEl.style.zIndex = tabEls.length + 1
zIndex = tabEls.length + 2
}
tabEl.style.zIndex = zIndex
})
}
createNewTabEl() {
const div = document.createElement('div')
div.innerHTML = tabTemplate
return div.firstElementChild
}
addTab(tabProperties) {
const tabEl = this.createNewTabEl()
tabEl.setAttribute('data-tab-id', tabProperties.id)
tabEl.classList.add('chrome-tab-just-added')
setTimeout(() => tabEl.classList.remove('chrome-tab-just-added'), 500)
tabProperties = Object.assign({}, defaultTabProperties, tabProperties)
this.tabContentEl.appendChild(tabEl)
this.updateTab(tabEl, tabProperties)
this.emit('tabAdd', { tabEl })
this.setCurrentTab(tabEl)
this.layoutTabs()
this.fixZIndexes()
this.setupDraggabilly()
}
setCurrentTab(tabEl) {
const currentTab = this.el.querySelector('.chrome-tab-current')
if (currentTab) currentTab.classList.remove('chrome-tab-current')
tabEl.classList.add('chrome-tab-current')
this.fixZIndexes()
this.emit('activeTabChange', { tabEl })
}
removeTab(tabEl) {
if (tabEl.classList.contains('chrome-tab-current')) {
if (tabEl.previousElementSibling) {
this.setCurrentTab(tabEl.previousElementSibling)
} else if (tabEl.nextElementSibling) {
this.setCurrentTab(tabEl.nextElementSibling)
}
}
tabEl.parentNode.removeChild(tabEl)
this.emit('tabRemove', { tabEl })
this.layoutTabs()
this.fixZIndexes()
this.setupDraggabilly()
}
updateTab(tabEl, tabProperties) {
tabEl.querySelector('.chrome-tab-title').textContent = tabProperties.title
tabEl.querySelector('.chrome-tab-favicon').style.backgroundImage = `url('${tabProperties.favicon}')`
tabEl.querySelector('.chrome-tab-favicon').style.display = tabProperties.loading ? 'none' : 'inline-block'
tabEl.querySelector('.chrome-tab-spinner').style.display = tabProperties.loading ? 'inline-block' : 'none'
}
cleanUpPreviouslyDraggedTabs() {
this.tabEls.forEach((tabEl) => tabEl.classList.remove('chrome-tab-just-dragged'))
}
setupDraggabilly() {
const tabEls = this.tabEls
const tabEffectiveWidth = this.tabEffectiveWidth
const tabPositions = this.tabPositions
this.draggabillyInstances.forEach(draggabillyInstance => draggabillyInstance.destroy())
tabEls.forEach((tabEl, originalIndex) => {
const originalTabPositionX = tabPositions[originalIndex]
const draggabillyInstance = new Draggabilly(tabEl, {
axis: 'x',
containment: this.tabContentEl
})
this.draggabillyInstances.push(draggabillyInstance)
draggabillyInstance.on('dragStart', () => {
this.cleanUpPreviouslyDraggedTabs()
tabEl.classList.add('chrome-tab-currently-dragged')
this.el.classList.add('chrome-tabs-sorting')
this.fixZIndexes()
})
draggabillyInstance.on('dragEnd', () => {
const finalTranslateX = parseFloat(tabEl.style.left, 10)
tabEl.style.transform = `translate3d(0, 0, 0)`
// Animate dragged tab back into its place
requestAnimationFrame(() => {
tabEl.style.left = '0'
tabEl.style.transform = `translate3d(${ finalTranslateX }px, 0, 0)`
requestAnimationFrame(() => {
tabEl.classList.remove('chrome-tab-currently-dragged')
this.el.classList.remove('chrome-tabs-sorting')
this.setCurrentTab(tabEl)
tabEl.classList.add('chrome-tab-just-dragged')
requestAnimationFrame(() => {
tabEl.style.transform = ''
this.setupDraggabilly()
})
})
})
})
draggabillyInstance.on('dragMove', (event, pointer, moveVector) => {
// Current index be computed within the event since it can change during the dragMove
const tabEls = this.tabEls
const currentIndex = tabEls.indexOf(tabEl)
const currentTabPositionX = originalTabPositionX + moveVector.x
const destinationIndex = Math.max(0, Math.min(tabEls.length, Math.floor((currentTabPositionX + (tabEffectiveWidth / 2)) / tabEffectiveWidth)))
if (currentIndex !== destinationIndex) {
this.animateTabMove(tabEl, currentIndex, destinationIndex)
}
})
})
}
animateTabMove(tabEl, originIndex, destinationIndex) {
if (destinationIndex < originIndex) {
tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex])
} else {
tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1])
}
}
}
if (isNodeContext) {
module.exports = ChromeTabs
} else {
window.ChromeTabs = ChromeTabs
}
})()