trilium/libraries/chrome-tabs/chrome-tabs.js

396 lines
26 KiB
JavaScript
Raw Normal View History

2019-05-01 04:31:12 +08:00
/*!
* Draggabilly PACKAGED v2.2.0
* Make that shiz draggable
* https://draggabilly.desandro.com
* MIT license
*/
!function(i,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(t){return e(i,t)}):"object"==typeof module&&module.exports?module.exports=e(i,require("jquery")):i.jQueryBridget=e(i,i.jQuery)}(window,function(t,i){"use strict";var c=Array.prototype.slice,e=t.console,p=void 0===e?function(){}:function(t){e.error(t)};function n(d,o,u){(u=u||i||t.jQuery)&&(o.prototype.option||(o.prototype.option=function(t){u.isPlainObject(t)&&(this.options=u.extend(!0,this.options,t))}),u.fn[d]=function(t){if("string"==typeof t){var i=c.call(arguments,1);return s=i,a="$()."+d+'("'+(r=t)+'")',(e=this).each(function(t,i){var e=u.data(i,d);if(e){var n=e[r];if(n&&"_"!=r.charAt(0)){var o=n.apply(e,s);h=void 0===h?o:h}else p(a+" is not a valid method")}else p(d+" not initialized. Cannot call methods, i.e. "+a)}),void 0!==h?h:e}var e,r,s,h,a,n;return n=t,this.each(function(t,i){var e=u.data(i,d);e?(e.option(n),e._init()):(e=new o(i,n),u.data(i,d,e))}),this},r(u))}function r(t){!t||t&&t.bridget||(t.bridget=n)}return r(i||t.jQuery),n}),function(t,i){"use strict";"function"==typeof define&&define.amd?define("get-size/get-size",[],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():t.getSize=i()}(window,function(){"use strict";function m(t){var i=parseFloat(t);return-1==t.indexOf("%")&&!isNaN(i)&&i}var e="undefined"==typeof console?function(){}:function(t){console.error(t)},y=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],b=y.length;function E(t){var i=getComputedStyle(t);return i||e("Style returned "+i+". Are you running this code in a hidden iframe on Firefox? See http://bit.ly/getsizebug1"),i}var _,x=!1;function P(t){if(function(){if(!x){x=!0;var t=document.createElement("div");t.style.width="200px",t.style.padding="1px 2px 3px 4px",t.style.borderStyle="solid",t.style.borderWidth="1px 2px 3px 4px",t.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(t);var e=E(t);P.isBoxSizeOuter=_=200==m(e.width),i.removeChild(t)}}(),"string"==typeof t&&(t=document.querySelector(t)),t&&"object"==typeof t&&t.nodeType){var i=E(t);if("none"==i.display)return function(){for(var t={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},i=0;i<b;i++)t[y[i]]=0;return t}();var e={};e.width=t.offsetWidth,e.height=t.offsetHeight;for(var n=e.isBorderBox="border-box"==i.boxSizing,o=0;o<b;o++){var r=y[o],s=i[r],h=parseFloat(s);e[r]=isNaN(h)?0:h}var a=e.paddingLeft+e.paddingRight,d=e.paddingTop+e.paddingBottom,u=e.marginLeft+e.marginRight,c=e.marginTop+e.marginBottom,p=e.borderLeftWidth+e.borderRightWidth,f=e.borderTopWidth+e.borderBottomWidth,g=n&&_,l=m(i.width);!1!==l&&(e.width=l+(g?0:a+p));var v=m(i.height);return!1!==v&&(e.height=v+(g?0:d+f)),e.innerWidth=e.width-(a+p),e.innerHeight=e.height-(d+f),e.outerWidth=e.width+u,e.outerHeight=e.height+c,e}}return P}),function(t,i){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",i):"object"==typeof module&&module.exports?module.exports=i():t.EvEmitter=i()}("undefined"!=typeof window?window:this,function(){function t(){}var i=t.prototype;return i.on=function(t,i){if(t&&i){var e=this._events=this._events||{},n=e[t]=e[t]||[];return-1==n.indexOf(i)&&n.push(i),this}},i.once=function(t,i){if(t&&i){this.on(t,i);var e=this._onceEvents=this._onceEvents||{};return(e[t]=e[t]||{})[i]=!0,this}},i.off=function(t,i){var e=this._events&&this._events[t];if(e&&e.length){var n=e.indexOf(i);return-1!=n&&e.splice(n,1),this}},i.emitEvent=function(t,i){var e=this._events&&this._events[t];if(e&&e.length){e=e.slice(0),i=i||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;o<e.length;o++){var r=e[o];n&&n[r]&&(this.off(t,r),delete n[r]),r.apply(this,i)}return this}},i.allOff=function(){delete this._events,delete this._onceEvents},t}),function(i,e){"function"==typeof define&&define.amd?define("unipointer/unipointer",["ev-emitter/ev-emitter"],function(t){return e(i,t
(function(){
2019-05-12 01:27:33 +08:00
const Draggabilly = window.Draggabilly;
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
const TAB_CONTENT_MARGIN = 0;
const TAB_CONTENT_OVERLAP_DISTANCE = 1;
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
const TAB_CONTENT_MIN_WIDTH = 24;
const TAB_CONTENT_MAX_WIDTH = 240;
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
const TAB_SIZE_SMALL = 84;
const TAB_SIZE_SMALLER = 60;
const TAB_SIZE_MINI = 48;
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
const noop = _ => {};
2019-05-01 04:31:12 +08:00
const closest = (value, array) => {
2019-05-12 01:27:33 +08:00
let closest = Infinity;
let closestIndex = -1;
2019-05-01 04:31:12 +08:00
array.forEach((v, i) => {
if (Math.abs(value - v) < closest) {
2019-05-12 01:27:33 +08:00
closest = Math.abs(value - v);
2019-05-01 04:31:12 +08:00
closestIndex = i
}
2019-05-12 01:27:33 +08:00
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
return closestIndex;
};
2019-05-01 04:31:12 +08:00
const tabTemplate = `
<div class="chrome-tab">
<div class="chrome-tab-content">
<div class="chrome-tab-title"></div>
<div class="chrome-tab-drag-handle"></div>
<div class="chrome-tab-close"><span>×</span></div>
2019-05-01 04:31:12 +08:00
</div>
</div>
2019-05-12 01:27:33 +08:00
`;
2019-05-01 04:31:12 +08:00
const defaultTapProperties = {
2019-05-05 16:59:34 +08:00
title: 'New tab'
2019-05-12 01:27:33 +08:00
};
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
let instanceId = 0;
2019-05-01 04:31:12 +08:00
class ChromeTabs {
constructor() {
this.draggabillies = []
}
init(el) {
2019-05-12 01:27:33 +08:00
this.el = el;
this.instanceId = instanceId;
this.el.setAttribute('data-chrome-tabs-instance-id', this.instanceId);
instanceId += 1;
this.setupCustomProperties();
this.setupStyleEl();
this.setupEvents();
this.layoutTabs();
this.setupDraggabilly();
this.setVisibility();
2019-05-01 04:31:12 +08:00
}
emit(eventName, data) {
2019-05-12 01:27:33 +08:00
this.el.dispatchEvent(new CustomEvent(eventName, { detail: data }));
2019-05-01 04:31:12 +08:00
}
setupCustomProperties() {
2019-05-12 01:27:33 +08:00
this.el.style.setProperty('--tab-content-margin', `${ TAB_CONTENT_MARGIN }px`);
2019-05-01 04:31:12 +08:00
}
setupStyleEl() {
2019-05-12 01:27:33 +08:00
this.styleEl = document.createElement('style');
this.el.appendChild(this.styleEl);
2019-05-01 04:31:12 +08:00
}
setupEvents() {
window.addEventListener('resize', _ => {
2019-05-12 01:27:33 +08:00
this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs();
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
this.tabEls.forEach((tabEl) => this.setTabCloseEventListener(tabEl));
2019-05-01 04:31:12 +08:00
}
setVisibility() {
this.el.style.display = this.tabEls.length > 1 ? "block" : "none";
}
2019-05-01 04:31:12 +08:00
get tabEls() {
2019-05-12 01:27:33 +08:00
return Array.prototype.slice.call(this.el.querySelectorAll('.chrome-tab'));
2019-05-01 04:31:12 +08:00
}
get tabContentEl() {
2019-05-12 01:27:33 +08:00
return this.el.querySelector('.chrome-tabs-content');
2019-05-01 04:31:12 +08:00
}
get tabContentWidths() {
2019-05-12 01:27:33 +08:00
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;
const widths = [];
let extraWidthRemaining = totalExtraWidthDueToFlooring;
2019-05-01 04:31:12 +08:00
for (let i = 0; i < numberOfTabs; i += 1) {
2019-05-12 01:27:33 +08:00
const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0;
widths.push(flooredClampedTargetWidth + extraWidth);
if (extraWidthRemaining > 0) extraWidthRemaining -= 1;
2019-05-01 04:31:12 +08:00
}
return widths
}
get tabContentPositions() {
2019-05-12 01:27:33 +08:00
const positions = [];
const tabContentWidths = this.tabContentWidths;
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
let position = TAB_CONTENT_MARGIN;
2019-05-01 04:31:12 +08:00
tabContentWidths.forEach((width, i) => {
2019-05-12 01:27:33 +08:00
const offset = i * TAB_CONTENT_OVERLAP_DISTANCE;
positions.push(position - offset);
position += width;
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
return positions;
2019-05-01 04:31:12 +08:00
}
get tabPositions() {
2019-05-12 01:27:33 +08:00
const positions = [];
2019-05-01 04:31:12 +08:00
this.tabContentPositions.forEach((contentPosition) => {
2019-05-12 01:27:33 +08:00
positions.push(contentPosition - TAB_CONTENT_MARGIN);
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
return positions;
2019-05-01 04:31:12 +08:00
}
layoutTabs() {
2019-05-12 01:27:33 +08:00
const tabContentWidths = this.tabContentWidths;
2019-05-01 04:31:12 +08:00
this.tabEls.forEach((tabEl, i) => {
2019-05-12 01:27:33 +08:00
const contentWidth = tabContentWidths[i];
const width = contentWidth + (2 * TAB_CONTENT_MARGIN);
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
tabEl.style.width = width + 'px';
tabEl.removeAttribute('is-small');
tabEl.removeAttribute('is-smaller');
tabEl.removeAttribute('is-mini');
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
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', '');
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
let styleHTML = '';
2019-05-01 04:31:12 +08:00
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)
}
`
2019-05-12 01:27:33 +08:00
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
this.styleEl.innerHTML = styleHTML;
2019-05-01 04:31:12 +08:00
}
addTab(tabProperties, { animate = true, background = false } = {}) {
2019-05-12 01:27:33 +08:00
const div = document.createElement('div');
div.innerHTML = tabTemplate;
const tabEl = div.firstElementChild;
2019-05-01 04:31:12 +08:00
if (animate) {
2019-05-12 01:27:33 +08:00
tabEl.classList.add('chrome-tab-was-just-added');
setTimeout(() => tabEl.classList.remove('chrome-tab-was-just-added'), 500);
2019-05-01 04:31:12 +08:00
}
2019-05-12 01:27:33 +08:00
tabProperties = Object.assign({}, defaultTapProperties, tabProperties);
this.tabContentEl.appendChild(tabEl);
this.setVisibility();
this.setTabCloseEventListener(tabEl);
this.updateTab(tabEl, tabProperties);
this.emit('tabAdd', { tabEl });
if (!background) this.setCurrentTab(tabEl);
this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs();
this.setupDraggabilly();
return tabEl;
2019-05-01 04:31:12 +08:00
}
setTabCloseEventListener(tabEl) {
2019-05-12 01:27:33 +08:00
tabEl.querySelector('.chrome-tab-close').addEventListener('click', _ => this.removeTab(tabEl));
2019-05-01 04:31:12 +08:00
}
get activeTabEl() {
2019-05-12 01:27:33 +08:00
return this.el.querySelector('.chrome-tab[active]');
2019-05-01 04:31:12 +08:00
}
get previousTabEl() {
const tabEls = this.tabEls;
if (tabEls.length <= 1) {
return null;
}
let prev = tabEls[tabEls.length - 1];
for (const tabEl of tabEls) {
if (tabEl.hasAttribute("active")) {
return prev;
}
prev = tabEl;
}
return null;
}
get nextTabEl() {
const tabEls = this.tabEls;
if (tabEls.length <= 1) {
return null;
}
let prev = tabEls[tabEls.length - 1];
for (const tabEl of tabEls) {
if (prev.hasAttribute("active")) {
return tabEl;
}
prev = tabEl;
}
return null;
}
2019-05-01 04:31:12 +08:00
hasActiveTab() {
2019-05-12 01:27:33 +08:00
return !!this.activeTabEl;
2019-05-01 04:31:12 +08:00
}
setCurrentTab(tabEl) {
2019-05-12 01:27:33 +08:00
const activeTabEl = this.activeTabEl;
if (activeTabEl === tabEl) return;
if (activeTabEl) activeTabEl.removeAttribute('active');
tabEl.setAttribute('active', '');
this.emit('activeTabChange', { tabEl });
2019-05-01 04:31:12 +08:00
}
removeTab(tabEl) {
if (tabEl === this.activeTabEl) {
if (tabEl.nextElementSibling) {
this.setCurrentTab(tabEl.nextElementSibling)
} else if (tabEl.previousElementSibling) {
this.setCurrentTab(tabEl.previousElementSibling)
}
}
2019-05-12 01:27:33 +08:00
tabEl.parentNode.removeChild(tabEl);
this.emit('tabRemove', { tabEl });
this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs();
this.setupDraggabilly();
this.setVisibility();
2019-05-01 04:31:12 +08:00
}
removeAllTabsExceptForThis(remainingTabEl) {
for (const tabEl of this.tabEls) {
if (remainingTabEl !== tabEl) {
this.removeTab(tabEl);
}
}
}
2019-05-01 04:31:12 +08:00
updateTab(tabEl, tabProperties) {
2019-05-12 01:27:33 +08:00
tabEl.querySelector('.chrome-tab-title').textContent = tabProperties.title;
2019-05-01 04:31:12 +08:00
if (tabProperties.id) {
2019-05-12 01:27:33 +08:00
tabEl.setAttribute('data-tab-id', tabProperties.id);
2019-05-01 04:31:12 +08:00
}
}
cleanUpPreviouslyDraggedTabs() {
2019-05-12 01:27:33 +08:00
this.tabEls.forEach((tabEl) => tabEl.classList.remove('chrome-tab-was-just-dragged'));
2019-05-01 04:31:12 +08:00
}
setupDraggabilly() {
2019-05-12 01:27:33 +08:00
const tabEls = this.tabEls;
const tabPositions = this.tabPositions;
2019-05-01 04:31:12 +08:00
if (this.isDragging) {
2019-05-12 01:27:33 +08:00
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;
2019-05-01 04:31:12 +08:00
}
2019-05-12 01:27:33 +08:00
this.draggabillies.forEach(d => d.destroy());
2019-05-01 04:31:12 +08:00
tabEls.forEach((tabEl, originalIndex) => {
2019-05-12 01:27:33 +08:00
const originalTabPositionX = tabPositions[originalIndex];
2019-05-01 04:31:12 +08:00
const draggabilly = new Draggabilly(tabEl, {
axis: 'x',
handle: '.chrome-tab-drag-handle',
containment: this.tabContentEl
2019-05-12 01:27:33 +08:00
});
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
this.draggabillies.push(draggabilly);
2019-05-01 04:31:12 +08:00
draggabilly.on('pointerDown', _ => {
this.setCurrentTab(tabEl)
2019-05-12 01:27:33 +08:00
});
2019-05-01 04:31:12 +08:00
draggabilly.on('dragStart', _ => {
2019-05-12 01:27:33 +08:00
this.isDragging = true;
this.draggabillyDragging = draggabilly;
tabEl.classList.add('chrome-tab-is-dragging');
this.el.classList.add('chrome-tabs-is-sorting');
});
2019-05-01 04:31:12 +08:00
draggabilly.on('dragEnd', _ => {
2019-05-12 01:27:33 +08:00
this.isDragging = false;
const finalTranslateX = parseFloat(tabEl.style.left, 10);
tabEl.style.transform = `translate3d(0, 0, 0)`;
2019-05-01 04:31:12 +08:00
// Animate dragged tab back into its place
requestAnimationFrame(_ => {
2019-05-12 01:27:33 +08:00
tabEl.style.left = '0';
tabEl.style.transform = `translate3d(${ finalTranslateX }px, 0, 0)`;
2019-05-01 04:31:12 +08:00
requestAnimationFrame(_ => {
2019-05-12 01:27:33 +08:00
tabEl.classList.remove('chrome-tab-is-dragging');
this.el.classList.remove('chrome-tabs-is-sorting');
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
tabEl.classList.add('chrome-tab-was-just-dragged');
2019-05-01 04:31:12 +08:00
requestAnimationFrame(_ => {
2019-05-12 01:27:33 +08:00
tabEl.style.transform = '';
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
this.layoutTabs();
this.setupDraggabilly();
2019-05-01 04:31:12 +08:00
})
})
})
2019-05-12 01:27:33 +08:00
});
2019-05-01 04:31:12 +08:00
draggabilly.on('dragMove', (event, pointer, moveVector) => {
// Current index be computed within the event since it can change during the dragMove
2019-05-12 01:27:33 +08:00
const tabEls = this.tabEls;
const currentIndex = tabEls.indexOf(tabEl);
2019-05-01 04:31:12 +08:00
2019-05-12 01:27:33 +08:00
const currentTabPositionX = originalTabPositionX + moveVector.x;
const destinationIndexTarget = closest(currentTabPositionX, tabPositions);
const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget));
2019-05-01 04:31:12 +08:00
if (currentIndex !== destinationIndex) {
2019-05-12 01:27:33 +08:00
this.animateTabMove(tabEl, currentIndex, destinationIndex);
2019-05-01 04:31:12 +08:00
}
})
})
}
animateTabMove(tabEl, originIndex, destinationIndex) {
if (destinationIndex < originIndex) {
2019-05-12 01:27:33 +08:00
tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]);
2019-05-01 04:31:12 +08:00
} else {
2019-05-12 01:27:33 +08:00
tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1]);
2019-05-01 04:31:12 +08:00
}
2019-05-12 01:27:33 +08:00
this.emit('tabReorder', { tabEl, originIndex, destinationIndex });
this.layoutTabs();
2019-05-01 04:31:12 +08:00
}
}
2019-05-12 01:27:33 +08:00
window.ChromeTabs = ChromeTabs;
})();