@ovine/core
Version:
Build flexible admin system with json.
326 lines (325 loc) • 13.9 kB
JavaScript
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;