UNPKG

@sinm/react-chrome-tabs

Version:
576 lines (575 loc) 27.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var draggabilly_1 = __importDefault(require("draggabilly")); var util_1 = require("./utils/util"); var TAB_CONTENT_MARGIN = 9; var TAB_CONTENT_OVERLAP_DISTANCE = 1; var TAB_OVERLAP_DISTANCE = TAB_CONTENT_MARGIN * 2 + TAB_CONTENT_OVERLAP_DISTANCE; var TAB_CONTENT_MIN_WIDTH = 60; var TAB_CONTENT_MAX_WIDTH = 240; var TAB_SIZE_SMALL = 84; var TAB_SIZE_SMALLER = 60; var TAB_SIZE_MINI = 48; var noop = function (_) { }; var closest = function (value, array) { var closest = Infinity; var closestIndex = -1; array.forEach(function (v, i) { if (Math.abs(value - v) < closest) { closest = Math.abs(value - v); closestIndex = i; } }); return closestIndex; }; var tabTemplate = "\n <div class=\"chrome-tab\">\n <div class=\"chrome-tab-dividers\"></div>\n <div class=\"chrome-tab-background\">\n <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>\n </div>\n <div class=\"chrome-tab-content\">\n <div class=\"chrome-tab-favicon\"></div>\n <div class=\"chrome-tab-title\"></div>\n <div class=\"chrome-tab-drag-handle\"></div>\n <div class=\"chrome-tab-close\"></div>\n </div>\n </div>\n "; var defaultTapProperties = { title: "New tab", favicon: false, }; util_1.inRange; var instanceId = 0; var ChromeTabs = /** @class */ (function () { function ChromeTabs(_a) { var _b = _a === void 0 ? {} : _a, _c = _b.draggable, draggable = _c === void 0 ? true : _c; var _this = this; this.isMouseEnter = false; this.mouseEnterLayoutResolve = null; this.draggable = true; this.translateX = 0; this.onResize = function () { _this.cleanUpPreviouslyDraggedTabs(); _this.layoutTabs(); }; this.onMouseLeave = function () { _this.isMouseEnter = false; if (_this.mouseEnterLayoutResolve) { _this.mouseEnterLayoutResolve(); _this.mouseEnterLayoutResolve = null; } }; this.layoutPromise = null; this.draggabillies = []; this.draggable = draggable; } ChromeTabs.prototype.setDraggable = function (draggable) { this.draggable = draggable; if (draggable) { this.setupDraggabilly(); } else { this.disposeDraggabilly(); } }; ChromeTabs.prototype.init = function (el) { 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.setDraggable(this.draggable); }; ChromeTabs.prototype.emit = function (eventName, data) { this.el.dispatchEvent(new CustomEvent(eventName, { detail: data })); }; ChromeTabs.prototype.setupCustomProperties = function () { this.el.style.setProperty("--tab-content-margin", "".concat(TAB_CONTENT_MARGIN, "px")); }; ChromeTabs.prototype.setupStyleEl = function () { this.styleEl = document.createElement("style"); this.el.appendChild(this.styleEl); }; ChromeTabs.prototype.setupEvents = function () { var _this = this; if ((0, util_1.inBrowser)()) { window.addEventListener("resize", this.onResize); } // this.el.addEventListener("dblclick", (event) => { // if ([this.el, this.tabContentEl].includes(event.target as HTMLElement)) // this.addTab(); // }); this.el.addEventListener("mouseenter", function () { _this.isMouseEnter = true; }); this.el.addEventListener("mouseleave", this.onMouseLeave); // When the page visibility status changes, it is triggered immediately document.addEventListener("visibilitychange", this.onMouseLeave); this.tabEls.forEach(function (tabEl) { return _this.setTabCloseEventListener(tabEl); }); this.tabContentEl.addEventListener("wheel", function (event) { var tabsWidth = _this.getTabsWidth(); var clientWidth = _this.tabContentEl.clientWidth; if (clientWidth >= tabsWidth) { _this.translateX = 0; } else { var sign = event.deltaY > 0 ? 1 : -1; var delta = (sign * (tabsWidth - clientWidth)) / 3; _this.translateX = Math.min(0, Math.max(_this.translateX - delta, clientWidth - tabsWidth)); } _this.tabContentEl.style.transform = "translateX(".concat(_this.translateX, "px)"); }); this.el.addEventListener("activeTabChange", function (event) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.layoutPromise]; case 1: _a.sent(); // Wait for the layout to finish this.translateToView(); return [2 /*return*/]; } }); }); }); }; Object.defineProperty(ChromeTabs.prototype, "tabEls", { get: function () { return Array.prototype.slice.call(this.el.querySelectorAll(".chrome-tab")); }, enumerable: false, configurable: true }); Object.defineProperty(ChromeTabs.prototype, "tabContentEl", { get: function () { return this.el.querySelector(".chrome-tabs-content"); }, enumerable: false, configurable: true }); Object.defineProperty(ChromeTabs.prototype, "toolbarEl", { get: function () { return this.el.querySelector(".chrome-tabs-toolbar-right"); }, enumerable: false, configurable: true }); Object.defineProperty(ChromeTabs.prototype, "tabContentWidths", { get: function () { var numberOfTabs = this.tabEls.length; var tabsContentWidth = this.el.clientWidth - this.toolbarEl.clientWidth - 16; var tabsCumulativeOverlappedWidth = (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE; var targetWidth = (tabsContentWidth - 2 * TAB_CONTENT_MARGIN + tabsCumulativeOverlappedWidth) / numberOfTabs; var clampedTargetWidth = Math.max(TAB_CONTENT_MIN_WIDTH, Math.min(TAB_CONTENT_MAX_WIDTH, targetWidth)); var flooredClampedTargetWidth = Math.floor(clampedTargetWidth); var totalTabsWidthUsingTarget = flooredClampedTargetWidth * numberOfTabs + 2 * TAB_CONTENT_MARGIN - tabsCumulativeOverlappedWidth; var totalExtraWidthDueToFlooring = tabsContentWidth - totalTabsWidthUsingTarget; // TODO - Support tabs with different widths / e.g. "pinned" tabs var widths = []; var extraWidthRemaining = totalExtraWidthDueToFlooring; for (var i = 0; i < numberOfTabs; i += 1) { var extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0; widths.push(flooredClampedTargetWidth + extraWidth); if (extraWidthRemaining > 0) extraWidthRemaining -= 1; } return widths; }, enumerable: false, configurable: true }); Object.defineProperty(ChromeTabs.prototype, "tabContentPositions", { get: function () { var positions = []; var tabContentWidths = this.tabContentWidths; var position = TAB_CONTENT_MARGIN; tabContentWidths.forEach(function (width, i) { var offset = i * TAB_CONTENT_OVERLAP_DISTANCE; positions.push(position - offset); position += width; }); return positions; }, enumerable: false, configurable: true }); Object.defineProperty(ChromeTabs.prototype, "tabPositions", { get: function () { var positions = []; this.tabContentPositions.forEach(function (contentPosition) { positions.push(contentPosition - TAB_CONTENT_MARGIN); }); return positions; }, enumerable: false, configurable: true }); ChromeTabs.prototype.doLayout = function () { return __awaiter(this, void 0, void 0, function () { var tabContentWidths, styleHTML; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: tabContentWidths = this.tabContentWidths; this.tabEls.forEach(function (tabEl, i) { var contentWidth = tabContentWidths[i]; var 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", ""); }); styleHTML = ""; this.tabPositions.forEach(function (position, i) { styleHTML += "\n .chrome-tabs[data-chrome-tabs-instance-id=\"".concat(_this.instanceId, "\"] .chrome-tab:nth-child(").concat(i + 1, ") {\n transform: translate3d(").concat(position, "px, 0, 0)\n }\n "); }); this.styleEl.innerHTML = styleHTML; return [4 /*yield*/, this.justifyContentWidth()]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; ChromeTabs.prototype.layoutTabs = function () { this.layoutPromise = this.doLayout(); return this.layoutPromise; }; ChromeTabs.prototype.getTabsWidth = function () { var contentWidths = this.tabEls.map(function (tabEl) { return tabEl.querySelector(".chrome-tab-drag-handle").getBoundingClientRect() .width; }); var width = util_1.sum.apply(void 0, contentWidths); var contentWith = width + 8 - Math.max(contentWidths.length - 1, 0) * TAB_CONTENT_OVERLAP_DISTANCE; return contentWith; }; ChromeTabs.prototype.justifyContentWidth = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, util_1.requestAnimationFrameAsync)()]; case 1: _a.sent(); this.tabContentEl.style.width = this.getTabsWidth() + "px"; return [2 /*return*/]; } }); }); }; ChromeTabs.prototype.translateToView = function () { return __awaiter(this, void 0, void 0, function () { var tabsWidth, tabWidth, clientWidth, index, currentX, left, right, isInRange; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, util_1.requestAnimationFrameAsync)()]; case 1: _a.sent(); tabsWidth = this.getTabsWidth(); tabWidth = tabsWidth / this.tabEls.length; clientWidth = this.tabContentEl.clientWidth; index = this.tabEls.indexOf(this.activeTabEl); if (index === -1) return [2 /*return*/]; if (clientWidth >= tabsWidth) { this.translateX = 0; } else { currentX = index * tabWidth; left = Math.max(-currentX, clientWidth - tabsWidth); right = Math.max(-currentX + tabWidth, clientWidth - tabsWidth); isInRange = (0, util_1.inRange)(this.translateX, left, right); this.translateX = Math.min(0, isInRange ? this.translateX : (left + right) / 2); } this.tabContentEl.style.transform = "translateX(".concat(this.translateX, "px)"); return [2 /*return*/]; } }); }); }; ChromeTabs.prototype.createNewTabEl = function () { var div = document.createElement("div"); div.innerHTML = tabTemplate; return div.firstElementChild; }; ChromeTabs.prototype.addTab = function (tabProperties, _a) { var _this = this; var _b = _a === void 0 ? {} : _a, _c = _b.animate, animate = _c === void 0 ? true : _c, _d = _b.background, background = _d === void 0 ? false : _d; var tabEl = this.createNewTabEl(); tabEl.oncontextmenu = function (event) { _this.emit("contextmenu", { tabEl: tabEl, event: event }); }; tabEl.addEventListener("mousedown", function () { _this.emit("tabClick", { tabEl: tabEl }); }); if (animate) { tabEl.classList.add("chrome-tab-was-just-added"); setTimeout(function () { return tabEl.classList.remove("chrome-tab-was-just-added"); }, 500); } tabProperties = Object.assign({}, defaultTapProperties, tabProperties); var showCloseButton = tabProperties.isCloseIconVisible !== false; if (!showCloseButton) { tabEl.classList.add("chrome-tab-no-close"); } else { tabEl.classList.remove("chrome-tab-no-close"); } this.tabContentEl.appendChild(tabEl); this.setTabCloseEventListener(tabEl); this.updateTab(tabEl, tabProperties); this.emit("tabAdd", { tabEl: tabEl }); if (!background) this.setCurrentTab(tabEl); this.cleanUpPreviouslyDraggedTabs(); this.layoutTabs(); this.setDraggable(this.draggable); return tabEl; }; ChromeTabs.prototype.setTabCloseEventListener = function (tabEl) { var _this = this; tabEl.querySelector(".chrome-tab-close").addEventListener("click", function (_) { _.stopImmediatePropagation(); // this.removeTab(tabEl); _this.emit("tabClose", { tabEl: tabEl }); }); }; Object.defineProperty(ChromeTabs.prototype, "activeTabEl", { get: function () { return this.el.querySelector(".chrome-tab[active]"); }, enumerable: false, configurable: true }); ChromeTabs.prototype.hasActiveTab = function () { return !!this.activeTabEl; }; ChromeTabs.prototype.setCurrentTab = function (tabEl) { var activeTabEl = this.activeTabEl; if (activeTabEl === tabEl) return; if (activeTabEl) activeTabEl.removeAttribute("active"); tabEl.setAttribute("active", ""); this.emit("activeTabChange", { tabEl: tabEl }); }; ChromeTabs.prototype.removeTab = function (tabEl) { var _this = this; if (tabEl === this.activeTabEl) { if (tabEl.nextElementSibling) { this.setCurrentTab(tabEl.nextElementSibling); } else if (tabEl.previousElementSibling) { this.setCurrentTab(tabEl.previousElementSibling); } } tabEl.parentNode.removeChild(tabEl); this.emit("tabRemove", { tabEl: tabEl }); this.cleanUpPreviouslyDraggedTabs(); if (this.isMouseEnter) { this.justifyContentWidth(); if (!this.mouseEnterLayoutResolve) { new Promise(function (resolve) { _this.mouseEnterLayoutResolve = resolve; }) .then(function () { return _this.layoutTabs(); }) .then(function () { return _this.translateToView(); }); } } else { this.layoutTabs().then(function () { return _this.translateToView(); }); } this.setDraggable(this.draggable); }; ChromeTabs.prototype.updateTab = function (tabEl, tabProperties) { tabEl.querySelector(".chrome-tab-title").textContent = tabProperties.title; var faviconEl = tabEl.querySelector(".chrome-tab-favicon"); var favicon = tabProperties.favicon, faviconClass = tabProperties.faviconClass; faviconEl.className = "chrome-tab-favicon"; faviconEl.style.backgroundImage = ""; if (favicon || faviconClass) { if (faviconClass) { faviconEl.className = ["chrome-tab-favicon", faviconClass].join(" "); } if (favicon) { faviconEl.style.backgroundImage = "url('".concat(favicon, "')"); } faviconEl === null || faviconEl === void 0 ? void 0 : faviconEl.removeAttribute("hidden"); } else { faviconEl === null || faviconEl === void 0 ? void 0 : faviconEl.setAttribute("hidden", ""); faviconEl === null || faviconEl === void 0 ? void 0 : faviconEl.removeAttribute("style"); } if (tabProperties.id) { tabEl.setAttribute("data-tab-id", tabProperties.id); } }; ChromeTabs.prototype.cleanUpPreviouslyDraggedTabs = function () { this.tabEls.forEach(function (tabEl) { return tabEl.classList.remove("chrome-tab-was-just-dragged"); }); }; ChromeTabs.prototype.disposeDraggabilly = function () { 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(function (d) { return d.destroy(); }); this.draggabillies = []; }; ChromeTabs.prototype.setupDraggabilly = function () { var _this = this; var tabEls = this.tabEls; var tabPositions = this.tabPositions; this.disposeDraggabilly(); tabEls.forEach(function (tabEl, originalIndex) { var originalTabPositionX = tabPositions[originalIndex]; var draggabilly = new draggabilly_1.default(tabEl, { axis: "x", handle: ".chrome-tab-drag-handle", containment: false, }); _this.draggabillies.push(draggabilly); draggabilly.on("pointerDown", function (_) { _this.emit("tabClick", { tabEl: tabEl }); // this.setCurrentTab(tabEl); }); draggabilly.on("dragStart", function (_) { _this.isDragging = true; _this.draggabillyDragging = draggabilly; tabEl.classList.add("chrome-tab-is-dragging"); _this.el.classList.add("chrome-tabs-is-sorting"); _this.emit("dragStart", { tabEl: tabEl }); }); draggabilly.on("dragEnd", function (_) { _this.isDragging = false; var finalTranslateX = parseFloat(tabEl.style.left); tabEl.style.transform = "translate3d(0, 0, 0)"; _this.emit("dragEnd", { tabEl: tabEl }); // Animate dragged tab back into its place requestAnimationFrame(function (_) { tabEl.style.left = "0"; tabEl.style.transform = "translate3d(".concat(finalTranslateX, "px, 0, 0)"); requestAnimationFrame(function (_) { tabEl.classList.remove("chrome-tab-is-dragging"); _this.el.classList.remove("chrome-tabs-is-sorting"); tabEl.classList.add("chrome-tab-was-just-dragged"); requestAnimationFrame(function (_) { tabEl.style.transform = ""; _this.layoutTabs(); _this.setDraggable(_this.draggable); }); }); }); }); var handleDragMove = function (event, pointer, moveVector) { // Current index be computed within the event since it can change during the dragMove var tabEls = _this.tabEls; var currentIndex = tabEls.indexOf(tabEl); var currentTabPositionX = originalTabPositionX + moveVector.x; var tabContent = tabEl.querySelector(".chrome-tab-content"); var right = currentTabPositionX + tabContent.clientWidth; var overLeft = currentTabPositionX < -2; var overRight = right > _this.tabContentEl.clientWidth; // trick to prevent the tab from being dragged out of the tab bar // @see https://github.com/desandro/draggabilly/issues/177#issuecomment-357270225 if (overLeft || overRight) { draggabilly.off("dragMove", handleDragMove); var x = void 0; if (overLeft) { x = -originalTabPositionX; } else { var RADIUS = 8; var delta = right - _this.tabContentEl.clientWidth + RADIUS; x = moveVector.x - delta; } draggabilly._dragMove(event, pointer, { x: x, y: 0, }); draggabilly.on("dragMove", handleDragMove); return; } var destinationIndexTarget = closest(currentTabPositionX, tabPositions); var destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget)); if (currentIndex !== destinationIndex) { _this.animateTabMove(tabEl, currentIndex, destinationIndex); } }; draggabilly.on("dragMove", handleDragMove); }); }; ChromeTabs.prototype.animateTabMove = function (tabEl, originIndex, destinationIndex) { // tabEl.style.transform = `translate3d(${-this.translateX}px, 0, 0)`; if (destinationIndex < originIndex) { tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]); } else { tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1]); } this.emit("tabReorder", { tabEl: tabEl, originIndex: originIndex, destinationIndex: destinationIndex }); this.layoutTabs(); }; ChromeTabs.prototype.destroy = function () { if ((0, util_1.inBrowser)()) { window.removeEventListener("resize", this.onResize); } document.removeEventListener("visibilitychange", this.onMouseLeave); }; return ChromeTabs; }()); exports.default = ChromeTabs;