273 lines
8.4 KiB
JavaScript
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
|
|
}
|
|
})()
|