UNPKG

@ovine/core

Version:

Build flexible admin system with json.

326 lines (325 loc) 13.9 kB
import Draggabilly from "../../assets/scripts/draggabilly"; import { message } from "../../constants"; import { publish } from "../../utils/message"; const TAB_CONTENT_MARGIN = 9; const TAB_CONTENT_OVERLAP_DISTANCE = 1; // const TAB_OVERLAP_DISTANCE = TAB_CONTENT_MARGIN * 2 + TAB_CONTENT_OVERLAP_DISTANCE const TAB_CONTENT_MIN_WIDTH = 24; const TAB_CONTENT_MAX_WIDTH = 240; const TAB_SIZE_SMALL = 84; const TAB_SIZE_SMALLER = 60; const TAB_SIZE_MINI = 48; const noop = (_) => { }; const closest = (value, array) => { let temp = Infinity; let closestIndex = -1; array.forEach((v, i) => { if (Math.abs(value - v) < temp) { temp = Math.abs(value - v); closestIndex = i; } }); return closestIndex; }; const tabTemplate = ` <div class="chrome-tab"> <div class="chrome-tab-dividers"></div> <div class="chrome-tab-background"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="chrome-tab-geometry-left" viewBox="0 0 214 36"><path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"/></symbol><symbol id="chrome-tab-geometry-right" viewBox="0 0 214 36"><use xlink:href="#chrome-tab-geometry-left"/></symbol><clipPath id="crop"><rect class="mask" width="100%" height="100%" x="0"/></clipPath></defs><svg width="52%" height="100%"><use xlink:href="#chrome-tab-geometry-left" width="214" height="36" class="chrome-tab-geometry"/></svg><g transform="scale(-1, 1)"><svg width="52%" height="100%" x="-100%" y="0"><use xlink:href="#chrome-tab-geometry-right" width="214" height="36" class="chrome-tab-geometry"/></svg></g></svg> </div> <div class="chrome-tab-content"> <div class="chrome-tab-favicon"></div> <div class="chrome-tab-title"></div> <div class="chrome-tab-drag-handle"></div> <div class="chrome-tab-close"> <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2352" xmlnsXlink="http://www.w3.org/1999/xlink" width="200" height="200" > <path d="M518.5815877 469.42879156L240.02576609 190.87364889a34.76142882 34.76142882 0 1 0-49.17520097 49.12971238l278.55446374 278.57822643L190.85056508 797.15913522a34.76142882 34.76142882 0 1 0 49.15279619 49.15211725l278.57822651-278.55446378 278.57822634 278.55446378a34.76142882 34.76142882 0 1 0 49.15211728-49.12903345l-278.55446374-278.60063124 278.55446374-278.55514271a34.76142882 34.76142882 0 1 0-49.15211728-49.15279622L518.60467145 469.42879156z" p-id="2353" /> </svg> </div> </div> </div> `; const defaultTapProperties = { title: 'New tab', favicon: false, }; let instanceId = 0; class ChromeTabs { constructor() { this.draggabillies = []; } init(el) { this.el = el; this.instanceId = instanceId; this.el.setAttribute('data-chrome-tabs-instance-id', this.instanceId); instanceId += 1; this.setupStyleEl(); this.setupEvents(); this.layoutTabs(); this.setupDraggabilly(); } emit(eventName, data) { this.el.dispatchEvent(new CustomEvent(eventName, { detail: data })); } setupStyleEl() { this.styleEl = document.createElement('style'); this.el.appendChild(this.styleEl); } setupEvents() { window.addEventListener('resize', (_) => { this.cleanUpPreviouslyDraggedTabs(); this.layoutTabs(); }); this.tabEls.forEach((tabEl) => this.setTabCloseEventListener(tabEl)); } get tabEls() { return Array.prototype.slice.call(this.el.querySelectorAll('.chrome-tab')); } get tabContentEl() { return this.el.querySelector('.chrome-tabs-content'); } get tabContentWidths() { const numberOfTabs = this.tabEls.length; const tabsContentWidth = this.tabContentEl.clientWidth; const tabsCumulativeOverlappedWidth = (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE; const targetWidth = (tabsContentWidth - 2 * TAB_CONTENT_MARGIN + tabsCumulativeOverlappedWidth) / numberOfTabs; const clampedTargetWidth = Math.max(TAB_CONTENT_MIN_WIDTH, Math.min(TAB_CONTENT_MAX_WIDTH, targetWidth)); const flooredClampedTargetWidth = Math.floor(clampedTargetWidth); const totalTabsWidthUsingTarget = flooredClampedTargetWidth * numberOfTabs + 2 * TAB_CONTENT_MARGIN - tabsCumulativeOverlappedWidth; const totalExtraWidthDueToFlooring = tabsContentWidth - totalTabsWidthUsingTarget; // TODO - Support tabs with different widths / e.g. "pinned" tabs const widths = []; let extraWidthRemaining = totalExtraWidthDueToFlooring; for (let i = 0; i < numberOfTabs; i += 1) { const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0; widths.push(flooredClampedTargetWidth + extraWidth); if (extraWidthRemaining > 0) { extraWidthRemaining -= 1; } } return widths; } get tabContentPositions() { const positions = []; const { tabContentWidths } = this; let position = TAB_CONTENT_MARGIN; tabContentWidths.forEach((width, i) => { const offset = i * TAB_CONTENT_OVERLAP_DISTANCE; positions.push(position - offset); position += width; }); return positions; } get tabPositions() { const positions = []; this.tabContentPositions.forEach((contentPosition) => { positions.push(contentPosition - TAB_CONTENT_MARGIN); }); return positions; } layoutTabs() { const { tabContentWidths } = this; this.tabEls.forEach((tabEl, i) => { const contentWidth = tabContentWidths[i]; const width = contentWidth + 2 * TAB_CONTENT_MARGIN; tabEl.style.width = `${width}px`; tabEl.removeAttribute('is-small'); tabEl.removeAttribute('is-smaller'); tabEl.removeAttribute('is-mini'); if (contentWidth < TAB_SIZE_SMALL) { tabEl.setAttribute('is-small', ''); } if (contentWidth < TAB_SIZE_SMALLER) { tabEl.setAttribute('is-smaller', ''); } if (contentWidth < TAB_SIZE_MINI) { tabEl.setAttribute('is-mini', ''); } }); let styleHTML = ''; this.tabPositions.forEach((position, i) => { styleHTML += ` .chrome-tabs[data-chrome-tabs-instance-id="${this.instanceId}"] .chrome-tab:nth-child(${i + 1}) { transform: translate3d(${position}px, 0, 0) } `; }); this.styleEl.innerHTML = styleHTML; } createNewTabEl() { const div = document.createElement('div'); div.innerHTML = tabTemplate; return div.firstElementChild; } addTab(tabProperties, { animate = false, background = false } = {}) { const tabEl = this.createNewTabEl(); if (animate) { tabEl.classList.add('chrome-tab-was-just-added'); setTimeout(() => tabEl.classList.remove('chrome-tab-was-just-added'), 500); } tabProperties = Object.assign(Object.assign({ isAdd: true }, defaultTapProperties), tabProperties); this.tabContentEl.appendChild(tabEl); this.setTabCloseEventListener(tabEl); this.updateTab(tabEl, tabProperties); this.emit('tabAdd', { tabEl, tabProperties }); if (!background) { this.setCurrentTab(tabEl, tabProperties); } this.cleanUpPreviouslyDraggedTabs(); this.layoutTabs(); this.setupDraggabilly(); } setTabCloseEventListener(tabEl) { tabEl.querySelector('.chrome-tab-close').addEventListener('click', (_) => { this.emit('onTabRemove', { tabEl }); }); } get activeTabEl() { return this.el.querySelector('.chrome-tab[active]'); } hasActiveTab() { return !!this.activeTabEl; } setCurrentTab(tabEl, tabProperties = {}) { const { activeTabEl } = this; if (activeTabEl === tabEl) { return; } if (activeTabEl) { activeTabEl.removeAttribute('active'); } tabEl.setAttribute('active', ''); this.emit('activeTabChange', { tabEl, tabProperties: Object.assign({ changeRoute: true }, tabProperties), }); } removeTab(tabEl, tabProperties = {}) { const { autoActive = true } = tabProperties; if (autoActive && tabEl === this.activeTabEl) { if (tabEl.nextElementSibling) { this.setCurrentTab(tabEl.nextElementSibling, tabProperties); } else if (tabEl.previousElementSibling) { this.setCurrentTab(tabEl.previousElementSibling, tabProperties); } } tabEl.parentNode.removeChild(tabEl); this.emit('tabRemove', { tabEl }); this.cleanUpPreviouslyDraggedTabs(); this.layoutTabs(); this.setupDraggabilly(); } updateTab(tabEl, tabProperties) { tabEl.querySelector('.chrome-tab-title').textContent = tabProperties.label; const faviconEl = tabEl.querySelector('.chrome-tab-favicon'); if (tabProperties.favicon) { faviconEl.style.backgroundImage = `url('${tabProperties.favicon}')`; faviconEl.removeAttribute('hidden', ''); } else { faviconEl.setAttribute('hidden', ''); faviconEl.removeAttribute('style'); } if (tabProperties.id) { tabEl.setAttribute('data-tab-id', tabProperties.id); } } cleanUpPreviouslyDraggedTabs() { this.tabEls.forEach((tabEl) => tabEl.classList.remove('chrome-tab-was-just-dragged')); } setupDraggabilly() { const { tabEls } = this; const { tabPositions } = this; if (this.isDragging) { this.isDragging = false; this.el.classList.remove('chrome-tabs-is-sorting'); this.draggabillyDragging.element.classList.remove('chrome-tab-is-dragging'); this.draggabillyDragging.element.style.transform = ''; this.draggabillyDragging.dragEnd(); this.draggabillyDragging.isDragging = false; this.draggabillyDragging.positionDrag = noop; // Prevent Draggabilly from updating tabEl.style.transform in later frames this.draggabillyDragging.destroy(); this.draggabillyDragging = null; } this.draggabillies.forEach((d) => d.destroy()); tabEls.forEach((tabEl, originalIndex) => { const originalTabPositionX = tabPositions[originalIndex]; const draggabilly = new Draggabilly(tabEl, { axis: 'x', handle: '.chrome-tab-drag-handle', containment: this.tabContentEl, }); this.draggabillies.push(draggabilly); draggabilly.on('pointerDown', (event) => { if (event.button === 2) { return; } // this.emit('onTabChange') publish(message.routeTabChange); this.setCurrentTab(tabEl); }); draggabilly.on('dragStart', (_) => { this.isDragging = true; this.draggabillyDragging = draggabilly; tabEl.classList.add('chrome-tab-is-dragging'); this.el.classList.add('chrome-tabs-is-sorting'); }); draggabilly.on('dragEnd', () => { this.isDragging = false; 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-is-dragging'); this.el.classList.remove('chrome-tabs-is-sorting'); tabEl.classList.add('chrome-tab-was-just-dragged'); requestAnimationFrame(() => { tabEl.style.transform = ''; this.layoutTabs(); this.setupDraggabilly(); }); }); }); }); draggabilly.on('dragMove', (event, pointer, moveVector) => { // Current index be computed within the event since it can change during the dragMove const currentIndex = this.tabEls.indexOf(tabEl); const currentTabPositionX = originalTabPositionX + moveVector.x; const destinationIndexTarget = closest(currentTabPositionX, tabPositions); const destinationIndex = Math.max(0, Math.min(this.tabEls.length, destinationIndexTarget)); 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]); } this.emit('tabReorder', { tabEl, originIndex, destinationIndex }); this.layoutTabs(); } } export default ChromeTabs;