Move to typescript

This commit is contained in:
Thomas Brouard 2022-05-24 23:38:25 +02:00
parent a556590aa8
commit 29165a8f34
15 changed files with 3069 additions and 4097 deletions

View file

@ -1,4 +1,5 @@
{ {
"root": true,
"env": { "env": {
"browser": true, "browser": true,
"es2021": true "es2021": true

View file

@ -1,8 +0,0 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.css": [
"parcel-transformer-css-to-string"
]
}
}

1
declarations.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "*.css";

View file

@ -15,7 +15,7 @@
</style> </style>
</tab-group> </tab-group>
<script src="../dist/electron-tabs.sortable.js"></script> <script src="../dist/electron-tabs.js"></script>
<script> <script>
const tabGroup = document.querySelector("tab-group"); const tabGroup = document.querySelector("tab-group");
tabGroup.on("ready", () => console.info("TabGroup is ready")); tabGroup.on("ready", () => console.info("TabGroup is ready"));

0
dist/electron-tabs.d.ts vendored Normal file
View file

2868
dist/electron-tabs.js vendored

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

71
package-lock.json generated
View file

@ -617,6 +617,15 @@
"posthtml": "^0.16.4" "posthtml": "^0.16.4"
} }
}, },
"@parcel/packager-ts": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/packager-ts/-/packager-ts-2.5.0.tgz",
"integrity": "sha512-BrH2Gum5EKlWJEJ92dFrH7QTSc7A7LxyElv6c2LPc5sI3z52JDdjQsUMEHqm5Fz25D79Ca/xzVvTWQaYA7XyRA==",
"dev": true,
"requires": {
"@parcel/plugin": "2.5.0"
}
},
"@parcel/plugin": { "@parcel/plugin": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.5.0.tgz",
@ -794,6 +803,15 @@
"nullthrows": "^1.1.1" "nullthrows": "^1.1.1"
} }
}, },
"@parcel/transformer-inline-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/transformer-inline-string/-/transformer-inline-string-2.5.0.tgz",
"integrity": "sha512-nBvzbAEIQ8qTSNEbFrxm+9XyOgGTaLOm1+dmBln75+OML4yn9TRUa8w2VxPKWyImPYAwRRZ2CZXAnGKfhwr+LA==",
"dev": true,
"requires": {
"@parcel/plugin": "2.5.0"
}
},
"@parcel/transformer-js": { "@parcel/transformer-js": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.5.0.tgz",
@ -940,6 +958,28 @@
} }
} }
}, },
"@parcel/transformer-typescript-types": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.5.0.tgz",
"integrity": "sha512-O+v+vEvgQDj5U1O8C12nYeU9kYOdYaznobWgE21WYSPEV2JD9ppaJVTDoNTI5Lx58gmjc1hndY169o6N6RaV6A==",
"dev": true,
"requires": {
"@parcel/diagnostic": "2.5.0",
"@parcel/plugin": "2.5.0",
"@parcel/source-map": "^2.0.0",
"@parcel/ts-utils": "2.5.0",
"nullthrows": "^1.1.1"
}
},
"@parcel/ts-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/ts-utils/-/ts-utils-2.5.0.tgz",
"integrity": "sha512-YITx84Olg27PDxvJlXzzPVgqTtW3tEqQFh+wE2g7+Mwk4Q8vd/jL+mjDBF/5LEnGCk2WvjkcuBK/QOv7Y+YDsg==",
"dev": true,
"requires": {
"nullthrows": "^1.1.1"
}
},
"@parcel/types": { "@parcel/types": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.5.0.tgz",
@ -1036,6 +1076,12 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true "dev": true
}, },
"@types/sortablejs": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.13.0.tgz",
"integrity": "sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==",
"dev": true
},
"abortcontroller-polyfill": { "abortcontroller-polyfill": {
"version": "1.7.3", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz",
@ -2084,15 +2130,6 @@
"v8-compile-cache": "^2.0.0" "v8-compile-cache": "^2.0.0"
} }
}, },
"parcel-transformer-css-to-string": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/parcel-transformer-css-to-string/-/parcel-transformer-css-to-string-0.9.1.tgz",
"integrity": "sha512-LvN6gh60IOLR6OtkF+/cYhMCZgIyn+yssJcvm4KiSlIxWxVYzc7S/+vc9NNjuxqKEo1iYa6jTs5DHIRcTua0+A==",
"dev": true,
"requires": {
"postcss-load-config": "^3.1.0"
}
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -2206,16 +2243,6 @@
"integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
"dev": true "dev": true
}, },
"postcss-load-config": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"requires": {
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
}
},
"postcss-merge-longhand": { "postcss-merge-longhand": {
"version": "5.1.5", "version": "5.1.5",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.5.tgz", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.5.tgz",
@ -2760,6 +2787,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "dev": true
}, },
"typescript": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
"dev": true
},
"universalify": { "universalify": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",

View file

@ -3,13 +3,11 @@
"version": "1.0.0-dev", "version": "1.0.0-dev",
"description": "Simple tabs for Electron applications", "description": "Simple tabs for Electron applications",
"main": "dist/electron-tabs.js", "main": "dist/electron-tabs.js",
"sortable": "dist/electron-tabs.sortable.js", "types": "dist/electron-tabs.d.ts",
"source": "src/index.ts",
"targets": { "targets": {
"main": { "main": {
"source": "src/index.js" "includeNodeModules": true
},
"sortable": {
"source": "src/sortable.js"
} }
}, },
"repository": { "repository": {
@ -30,11 +28,15 @@
"author": "brrd", "author": "brrd",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@parcel/packager-ts": "^2.5.0",
"@parcel/transformer-inline-string": "^2.5.0",
"@parcel/transformer-typescript-types": "^2.5.0",
"@types/sortablejs": "^1.13.0",
"cssnano": "^5.1.9", "cssnano": "^5.1.9",
"electron": "^17.1.2", "electron": "^17.1.2",
"parcel": "^2.5.0", "parcel": "^2.5.0",
"parcel-transformer-css-to-string": "^0.9.1",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"sortablejs": "^1.15.0" "sortablejs": "^1.15.0",
"typescript": "^4.7.2"
} }
} }

118
src/index.d.ts vendored
View file

@ -1,118 +0,0 @@
declare class EventEmitter extends EventTarget {
emit(event: string, ...args: any[]): boolean;
emit(event: "tab-added", tab: ElectronTabs.Tab, tabGroup: ElectronTabs): boolean;
emit(event: "tab-removed", tab: ElectronTabs.Tab, tabGroup: ElectronTabs): boolean;
emit(event: "tab-active", tab: ElectronTabs.Tab, tabGroup: ElectronTabs): boolean;
emit(event: "webview-ready", tab: ElectronTabs.Tab): boolean;
emit(event: "webview-dom-ready", tab: ElectronTabs.Tab): boolean;
emit(event: "title-changed", title: string, tab: ElectronTabs.Tab): boolean;
emit(event: "badge-changed", badge: string, tab: ElectronTabs.Tab): boolean;
emit(event: "icon-changed", icon: string, tab: ElectronTabs.Tab): boolean;
emit(event: "active", tab: ElectronTabs.Tab): boolean;
emit(event: "inactive", tab: ElectronTabs.Tab): boolean;
emit(event: "visible", tab: ElectronTabs.Tab): boolean;
emit(event: "hidden", tab: ElectronTabs.Tab): boolean;
emit(event: "flash", tab: ElectronTabs.Tab): boolean;
emit(event: "unflash", tab: ElectronTabs.Tab): boolean;
emit(event: "close", tab: ElectronTabs.Tab, abort: () => void): boolean;
emit(event: "closing", tab: ElectronTabs.Tab, abort: () => void): boolean;
on(event: string, listener: (...args: any[]) => void): this;
on(event: "tab-added", listener: (tab: ElectronTabs.Tab, tabGroup: ElectronTabs) => void): this;
on(event: "tab-removed", listener: (tab: ElectronTabs.Tab, tabGroup: ElectronTabs) => void): this;
on(event: "tab-active", listener: (tab: ElectronTabs.Tab, tabGroup: ElectronTabs) => void): this;
on(event: "webview-ready", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "webview-dom-ready", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "title-changed", listener: (title: string, tab: ElectronTabs.Tab) => void): this;
on(event: "badge-changed", listener: (badge: string, tab: ElectronTabs.Tab) => void): this;
on(event: "icon-changed", listener: (icon: string, tab: ElectronTabs.Tab) => void): this;
on(event: "active", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "inactive", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "visible", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "hidden", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "flash", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "unflash", listener: (tab: ElectronTabs.Tab) => void): this;
on(event: "close", listener: (tab: ElectronTabs.Tab, abort: () => void) => void): this;
on(event: "closing", listener: (tab: ElectronTabs.Tab, abort: () => void) => void): this;
once(event: "webview-ready", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "webview-dom-ready", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "title-changed", listener: (title: string, tab: ElectronTabs.Tab) => void): this;
once(event: "badge-changed", listener: (badge: string, tab: ElectronTabs.Tab) => void): this;
once(event: "icon-changed", listener: (icon: string, tab: ElectronTabs.Tab) => void): this;
once(event: "active", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "inactive", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "visible", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "hidden", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "flash", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "unflash", listener: (tab: ElectronTabs.Tab) => void): this;
once(event: "close", listener: (tab: ElectronTabs.Tab, abort: () => void) => void): this;
once(event: "closing", listener: (tab: ElectronTabs.Tab, abort: () => void) => void): this;
}
declare class ElectronTabs extends EventEmitter {
constructor(options?: ElectronTabs.TabGroupOptions);
addTab(options?: ElectronTabs.TabOptions): ElectronTabs.Tab;
getTab(id: number): ElectronTabs.Tab | null;
getTabByPosition(position: number): ElectronTabs.Tab | null;
getTabByRelPosition(position: number): ElectronTabs.Tab | null;
getActiveTab(): ElectronTabs.Tab | null;
getTabs(): ElectronTabs.Tab[];
eachTab<T extends object>(
fn: (this: T, currentTab: ElectronTabs.Tab, index: number, tabs: ElectronTabs.Tab[]) => void,
thisArg?: T,
): void;
tabContainer: HTMLElement;
}
declare namespace ElectronTabs {
export interface TabGroupOptions {
tabContainerSelector?: string;
buttonsContainerSelector?: string;
viewContainerSelector?: string;
tabClass?: string;
viewClass?: string;
closeButtonText?: string;
newTabButtonText?: string;
newTab?: TabOptions | (() => TabOptions);
ready?: (tabGroup: ElectronTabs) => void;
}
export interface TabOptions {
title?: string;
src?: string;
badge?: string;
iconURL?: string;
icon?: string;
closable?: boolean;
webviewAttributes?: {[key: string]: any};
visible?: boolean;
active?: boolean;
ready?: (tab: Tab) => void;
}
export interface Tab extends EventEmitter {
id: number;
setTitle(title: string): void;
getTitle(): string;
setBadge(badge: string): void;
getBadge(): string;
setIcon(iconURL?: string, icon?: undefined | null): void;
setIcon(iconURL: undefined | null, icon: string): void;
getIcon(): string;
setPosition(position: number): Tab | null;
getPosition(fromRight?: boolean): number;
activate(): void;
show(shown?: boolean): void;
hide(): void;
flash(shown?: boolean): void;
unflash(): void;
close(force?: boolean): void;
webview: Electron.WebviewTag;
}
}
export = ElectronTabs;

View file

@ -1,23 +1,74 @@
import styles from "./style.css"; import Sortable from "sortablejs";
import styles from "bundle-text:./style.css";
if (!document) { if (!document) {
throw Error("electron-tabs module must be called in renderer process"); throw Error("electron-tabs module must be called in renderer process");
} }
interface TabGroupOptions {
closeButtonText: string,
defaultTab: TabOptions | ((tabGroup: TabGroup) => TabOptions),
newTabButton: boolean,
newTabButtonText: string,
sortable: boolean,
sortableOptions?: Sortable.Options
tabClass: string,
viewClass: string,
visibilityThreshold: number,
}
interface TabOptions {
active?: boolean;
badge?: string;
closable?: boolean;
icon?: string;
iconURL?: string;
ready?: ((tab: Tab) => void);
src?: string;
title?: string;
visible?: boolean;
webviewAttributes?: { [key: string]: any };
}
function emit(emitter: TabGroup | Tab, type: string, args: any[]) {
if (type === "ready") {
emitter.isReady = true;
}
emitter.dispatchEvent(new CustomEvent(type, { detail: args }));
}
function on(emitter: TabGroup | Tab, type: string, fn: (detail: string) => void, options?: { [key: string]: any }) {
if (type === "ready" && emitter.isReady === true) {
fn.apply(emitter, [emitter]);
}
emitter.addEventListener(type, ((e: CustomEvent) => fn.apply(emitter, e.detail)) as EventListener, options);
}
class TabGroup extends HTMLElement { class TabGroup extends HTMLElement {
constructor () { buttonContainer: HTMLDivElement;
isReady: boolean;
newTabId: number;
options: TabGroupOptions;
shadow: ShadowRoot;
tabContainer: HTMLDivElement;
tabs: Array<Tab>;
viewContainer: HTMLDivElement;
constructor() {
super(); super();
this.isReady = false;
// Options // Options
this.options = { this.options = {
closeButtonText: this.getAttribute("close-button-text") || "&#215;", closeButtonText: this.getAttribute("close-button-text") || "&#215;",
defaultTab: { title: "New Tab", active: true },
newTabButton: !!this.getAttribute("new-tab-button") === true || false,
newTabButtonText: this.getAttribute("new-tab-button-text") || "&#65291;", newTabButtonText: this.getAttribute("new-tab-button-text") || "&#65291;",
visibilityThreshold: this.getAttribute("visibility-threshold") || 0, sortable: !!this.getAttribute("sortable") === true || false,
tabClass: this.getAttribute("tab-class") || "etabs-tab", tabClass: this.getAttribute("tab-class") || "etabs-tab",
viewClass: this.getAttribute("view-class") || "etabs-view", viewClass: this.getAttribute("view-class") || "etabs-view",
newTabButton: this.getAttribute("new-tab-button") || false, visibilityThreshold: Number(this.getAttribute("visibility-threshold")) || 0
defaultTab: { title: "New Tab", active: true },
sortable: this.getAttribute("sortable") || false
}; };
// Create custom element // Create custom element
@ -54,8 +105,8 @@ class TabGroup extends HTMLElement {
this.tabs = []; this.tabs = [];
this.newTabId = 0; this.newTabId = 0;
TabGroupPrivate.initNewTabButton.bind(this)(); this.initNewTabButton();
TabGroupPrivate.initVisibility.bind(this)(); this.initVisibility();
// Init sortable tabs // Init sortable tabs
if (this.options.sortable) { if (this.options.sortable) {
@ -65,10 +116,10 @@ class TabGroup extends HTMLElement {
animation: 150, animation: 150,
swapThreshold: 0.20 swapThreshold: 0.20
}, this.options.sortableOptions); }, this.options.sortableOptions);
new window.Sortable(this.tabContainer, options); new Sortable(this.tabContainer, options);
}; };
if (window.Sortable) { if (Sortable) {
initSortable(); initSortable();
} else { } else {
document.addEventListener("DOMContentLoaded", initSortable); document.addEventListener("DOMContentLoaded", initSortable);
@ -78,97 +129,38 @@ class TabGroup extends HTMLElement {
this.emit("ready", this); this.emit("ready", this);
} }
connectedCallback () { emit(type: string, ...args: any[]) {
return emit(this, type, args);
}
on(type: string, fn: (...detail: any[]) => void) {
return on(this, type, fn);
}
once(type: string, fn: (detail: string) => void) {
return on(this, type, fn, { once: true });
}
connectedCallback() {
const style = this.querySelector("style"); const style = this.querySelector("style");
if (style) { if (style) {
const clone = style.cloneNode(this); const clone = style.cloneNode(true);
this.shadow.appendChild(clone); this.shadow.appendChild(clone);
} }
} }
setDefaultTab (tab) { private initNewTabButton() {
this.options.defaultTab = tab;
}
addTab (args = this.options.defaultTab) {
if (typeof args === "function") {
args = args(this);
}
let id = this.newTabId;
this.newTabId++;
let tab = new Tab(this, id, args);
this.tabs.push(tab);
// Don't call tab.activate() before a tab is referenced in this.tabs
if (args.active === true) {
tab.activate();
}
this.emit("tab-added", tab, this);
return tab;
}
getTab (id) {
for (let i in this.tabs) {
if (this.tabs[i].id === id) {
return this.tabs[i];
}
}
return null;
}
getTabByPosition (position) {
let fromRight = position < 0;
for (let i in this.tabs) {
if (this.tabs[i].getPosition(fromRight) === position) {
return this.tabs[i];
}
}
return null;
}
getTabByRelPosition (position) {
position = this.getActiveTab().getPosition() + position;
if (position <= 0) {
return null;
}
return this.getTabByPosition(position);
}
getNextTab () {
return this.getTabByRelPosition(1);
}
getPreviousTab () {
return this.getTabByRelPosition(-1);
}
getTabs () {
return this.tabs.slice();
}
eachTab (fn) {
this.getTabs().forEach(fn);
return this;
}
getActiveTab () {
if (this.tabs.length === 0) return null;
return this.tabs[0];
}
}
const TabGroupPrivate = {
initNewTabButton: function () {
if (!this.options.newTabButton) return; if (!this.options.newTabButton) return;
let button = this.buttonContainer.appendChild(document.createElement("button")); const button = this.buttonContainer.appendChild(document.createElement("button"));
button.classList.add(`${this.options.tabClass}-button-new`); button.classList.add(`${this.options.tabClass}-button-new`);
button.innerHTML = this.options.newTabButtonText; button.innerHTML = this.options.newTabButtonText;
button.addEventListener("click", this.addTab.bind(this, undefined), false); button.addEventListener("click", this.addTab.bind(this, undefined), false);
}, }
initVisibility: function () { private initVisibility() {
function toggleTabsVisibility(tab, tabGroup) { function toggleTabsVisibility(tab: Tab, tabGroup: TabGroup) {
var visibilityThreshold = this.options.visibilityThreshold; const visibilityThreshold = this.options.visibilityThreshold;
var el = tabGroup.tabContainer.parentNode; const el = tabGroup.tabContainer.parentElement;
if (this.tabs.length >= visibilityThreshold) { if (this.tabs.length >= visibilityThreshold) {
el.classList.add("visible"); el.classList.add("visible");
} else { } else {
@ -178,52 +170,131 @@ const TabGroupPrivate = {
this.on("tab-added", toggleTabsVisibility); this.on("tab-added", toggleTabsVisibility);
this.on("tab-removed", toggleTabsVisibility); this.on("tab-removed", toggleTabsVisibility);
}, }
removeTab: function (tab, triggerEvent) { setDefaultTab(tab: TabOptions) {
let id = tab.id; this.options.defaultTab = tab;
}
addTab(args = this.options.defaultTab) {
if (typeof args === "function") {
args = args(this);
}
const id = this.newTabId;
this.newTabId++;
const tab = new Tab(this, id, args);
this.tabs.push(tab);
// Don't call tab.activate() before a tab is referenced in this.tabs
if (args.active === true) {
tab.activate();
}
this.emit("tab-added", tab, this);
return tab;
}
getTab(id: number) {
for (let i in this.tabs) { for (let i in this.tabs) {
if (this.tabs[i].id === id) { if (this.tabs[i].id === id) {
this.tabs.splice(i, 1); return this.tabs[i];
break;
} }
} }
return null;
}
getTabByPosition(position: number) {
const fromRight = position < 0;
for (let i in this.tabs) {
if (this.tabs[i].getPosition(fromRight) === position) {
return this.tabs[i];
}
}
return null;
}
getTabByRelPosition(position: number) {
position = this.getActiveTab().getPosition() + position;
if (position <= 0) {
return null;
}
return this.getTabByPosition(position);
}
getNextTab() {
return this.getTabByRelPosition(1);
}
getPreviousTab() {
return this.getTabByRelPosition(-1);
}
getTabs() {
return this.tabs.slice();
}
eachTab(fn: (tab: Tab) => void) {
this.getTabs().forEach(fn);
}
getActiveTab() {
if (this.tabs.length === 0) return null;
return this.tabs[0];
}
setActiveTab(tab: Tab) {
this.removeTab(tab);
this.tabs.unshift(tab);
this.emit("tab-active", tab, this);
}
removeTab(tab: Tab, triggerEvent = false) {
const id = tab.id;
const index = this.tabs.findIndex((t: Tab) => t.id === id);
this.tabs.splice(index, 1);
if (triggerEvent) { if (triggerEvent) {
this.emit("tab-removed", tab, this); this.emit("tab-removed", tab, this);
} }
return this; }
},
setActiveTab: function (tab) { activateRecentTab() {
TabGroupPrivate.removeTab.bind(this)(tab);
this.tabs.unshift(tab);
this.emit("tab-active", tab, this);
return this;
},
activateRecentTab: function (tab) {
if (this.tabs.length > 0) { if (this.tabs.length > 0) {
this.tabs[0].activate(); this.tabs[0].activate();
} }
return this;
} }
}; }
class Tab extends EventTarget { class Tab extends EventTarget {
constructor (tabGroup, id, args) { badge: string;
closable: boolean;
icon: string;
iconURL: string;
id: number;
isClosed: boolean;
isReady: boolean;
tab: HTMLDivElement;
tabElements: { [key: string]: HTMLSpanElement };
tabGroup: TabGroup;
title: string;
webview: HTMLElement;
webviewAttributes: { [key: string]: any };
constructor(tabGroup: TabGroup, id: number, args: TabOptions) {
super(); super();
this.tabGroup = tabGroup;
this.id = id;
this.title = args.title;
this.badge = args.badge; this.badge = args.badge;
this.iconURL = args.iconURL;
this.icon = args.icon;
this.closable = args.closable === false ? false : true; this.closable = args.closable === false ? false : true;
this.icon = args.icon;
this.iconURL = args.iconURL;
this.id = id;
this.isClosed = false;
this.isReady = false;
this.tabElements = {};
this.tabGroup = tabGroup;
this.title = args.title;
this.webviewAttributes = args.webviewAttributes || {}; this.webviewAttributes = args.webviewAttributes || {};
this.webviewAttributes.src = args.src; this.webviewAttributes.src = args.src;
this.tabElements = {};
TabPrivate.initTab.bind(this)(); this.initTab();
TabPrivate.initWebview.bind(this)(); this.initWebview();
if (args.visible !== false) { if (args.visible !== false) {
this.show(); this.show();
} }
@ -234,9 +305,104 @@ class Tab extends EventTarget {
} }
} }
setTitle (title) { emit(type: string, ...args: any[]) {
return emit(this, type, args);
}
on(type: string, fn: (...detail: any[]) => void) {
return on(this, type, fn);
}
once(type: string, fn: (detail: string) => void) {
return on(this, type, fn, { once: true });
}
private initTab() {
const tabClass = this.tabGroup.options.tabClass;
// Create tab element
const tab = this.tab = document.createElement("div");
tab.classList.add(tabClass);
for (let el of ["icon", "title", "buttons", "badge"]) {
const span = tab.appendChild(document.createElement("span"));
span.classList.add(`${tabClass}-${el}`);
this.tabElements[el] = span;
}
this.setTitle(this.title);
this.setBadge(this.badge);
this.setIcon(this.iconURL, this.icon);
this.initTabButtons();
this.initTabClickHandler();
this.tabGroup.tabContainer.appendChild(this.tab);
}
private initTabButtons() {
const container = this.tabElements.buttons;
const tabClass = this.tabGroup.options.tabClass;
if (this.closable) {
const button = container.appendChild(document.createElement("button"));
button.classList.add(`${tabClass}-button-close`);
button.innerHTML = this.tabGroup.options.closeButtonText;
button.addEventListener("click", this.close.bind(this, false), false);
}
}
private initTabClickHandler() {
// Mouse up
const tabClickHandler = function(e: KeyboardEvent) {
if (this.isClosed) return;
if (e.which === 2) {
this.close();
}
};
this.tab.addEventListener("mouseup", tabClickHandler.bind(this), false);
// Mouse down
const tabMouseDownHandler = function(e: KeyboardEvent) {
if (this.isClosed) return;
if (e.which === 1) {
if ((e.target as HTMLElement).matches("button")) return;
this.activate();
}
};
this.tab.addEventListener("mousedown", tabMouseDownHandler.bind(this), false);
}
initWebview() {
const webview = this.webview = document.createElement("webview");
const tabWebviewDidFinishLoadHandler = function(e: Event) {
this.emit("webview-ready", this);
};
this.webview.addEventListener("did-finish-load", tabWebviewDidFinishLoadHandler.bind(this), false);
const tabWebviewDomReadyHandler = function(e: Event) {
// Remove this once https://github.com/electron/electron/issues/14474 is fixed
webview.blur();
webview.focus();
this.emit("webview-dom-ready", this);
};
this.webview.addEventListener("dom-ready", tabWebviewDomReadyHandler.bind(this), false);
this.webview.classList.add(this.tabGroup.options.viewClass);
if (this.webviewAttributes) {
const attrs = this.webviewAttributes;
for (let key in attrs) {
const attr = attrs[key];
if (attr === false) continue;
this.webview.setAttribute(key, attr);
}
}
this.tabGroup.viewContainer.appendChild(this.webview);
}
setTitle(title: string) {
if (this.isClosed) return; if (this.isClosed) return;
let span = this.tabElements.title; const span = this.tabElements.title;
span.innerHTML = title; span.innerHTML = title;
span.title = title; span.title = title;
this.title = title; this.title = title;
@ -244,14 +410,14 @@ class Tab extends EventTarget {
return this; return this;
} }
getTitle () { getTitle() {
if (this.isClosed) return; if (this.isClosed) return;
return this.title; return this.title;
} }
setBadge (badge) { setBadge(badge: string) {
if (this.isClosed) return; if (this.isClosed) return;
let span = this.tabElements.badge; const span = this.tabElements.badge;
this.badge = badge; this.badge = badge;
if (badge) { if (badge) {
@ -264,16 +430,16 @@ class Tab extends EventTarget {
this.emit("badge-changed", badge, this); this.emit("badge-changed", badge, this);
} }
getBadge () { getBadge() {
if (this.isClosed) return; if (this.isClosed) return;
return this.badge; return this.badge;
} }
setIcon (iconURL, icon) { setIcon(iconURL: string, icon: string) {
if (this.isClosed) return; if (this.isClosed) return;
this.iconURL = iconURL; this.iconURL = iconURL;
this.icon = icon; this.icon = icon;
let span = this.tabElements.icon; const span = this.tabElements.icon;
if (iconURL) { if (iconURL) {
span.innerHTML = `<img src="${iconURL}" />`; span.innerHTML = `<img src="${iconURL}" />`;
this.emit("icon-changed", iconURL, this); this.emit("icon-changed", iconURL, this);
@ -285,16 +451,16 @@ class Tab extends EventTarget {
return this; return this;
} }
getIcon () { getIcon() {
if (this.isClosed) return; if (this.isClosed) return;
if (this.iconURL) return this.iconURL; if (this.iconURL) return this.iconURL;
return this.icon; return this.icon;
} }
setPosition (newPosition) { setPosition(newPosition: number) {
let tabContainer = this.tabGroup.tabContainer; const tabContainer = this.tabGroup.tabContainer;
let tabs = tabContainer.children; const tabs = tabContainer.children;
let oldPosition = this.getPosition() - 1; const oldPosition = this.getPosition() - 1;
if (newPosition < 0) { if (newPosition < 0) {
newPosition += tabContainer.childElementCount; newPosition += tabContainer.childElementCount;
@ -320,10 +486,10 @@ class Tab extends EventTarget {
return this; return this;
} }
getPosition (fromRight) { getPosition(fromRight = false) {
let position = 0; let position = 0;
let tab = this.tab; let tab = this.tab;
while ((tab = tab.previousSibling) != null) position++; while ((tab = tab.previousSibling as HTMLDivElement) != null) position++;
if (fromRight === true) { if (fromRight === true) {
position -= this.tabGroup.tabContainer.childElementCount; position -= this.tabGroup.tabContainer.childElementCount;
@ -336,15 +502,15 @@ class Tab extends EventTarget {
return position; return position;
} }
activate () { activate() {
if (this.isClosed) return; if (this.isClosed) return;
let activeTab = this.tabGroup.getActiveTab(); const activeTab = this.tabGroup.getActiveTab();
if (activeTab) { if (activeTab) {
activeTab.tab.classList.remove("active"); activeTab.tab.classList.remove("active");
activeTab.webview.classList.remove("visible"); activeTab.webview.classList.remove("visible");
activeTab.emit("inactive", activeTab); activeTab.emit("inactive", activeTab);
} }
TabGroupPrivate.setActiveTab.bind(this.tabGroup)(this); this.tabGroup.setActiveTab(this);
this.tab.classList.add("active"); this.tab.classList.add("active");
this.webview.classList.add("visible"); this.webview.classList.add("visible");
this.webview.focus(); this.webview.focus();
@ -352,9 +518,9 @@ class Tab extends EventTarget {
return this; return this;
} }
show (flag) { show(flag = true) {
if (this.isClosed) return; if (this.isClosed) return;
if (flag !== false) { if (flag) {
this.tab.classList.add("visible"); this.tab.classList.add("visible");
this.emit("visible", this); this.emit("visible", this);
} else { } else {
@ -364,11 +530,11 @@ class Tab extends EventTarget {
return this; return this;
} }
hide () { hide() {
return this.show(false); return this.show(false);
} }
flash (flag) { flash(flag = true) {
if (this.isClosed) return; if (this.isClosed) return;
if (flag !== false) { if (flag !== false) {
this.tab.classList.add("flash"); this.tab.classList.add("flash");
@ -380,15 +546,15 @@ class Tab extends EventTarget {
return this; return this;
} }
unflash () { unflash() {
return this.flash(false); return this.flash(false);
} }
hasClass (classname) { hasClass(classname: string) {
return this.tab.classList.contains(classname); return this.tab.classList.contains(classname);
} }
close (force) { close(force: boolean) {
const abortController = new AbortController(); const abortController = new AbortController();
const abort = () => abortController.abort(); const abort = () => abortController.abort();
this.emit("closing", this, abort); this.emit("closing", this, abort);
@ -397,132 +563,18 @@ class Tab extends EventTarget {
if (this.isClosed || (!this.closable && !force) || abortSignal.aborted) return; if (this.isClosed || (!this.closable && !force) || abortSignal.aborted) return;
this.isClosed = true; this.isClosed = true;
let tabGroup = this.tabGroup; const tabGroup = this.tabGroup;
tabGroup.tabContainer.removeChild(this.tab); tabGroup.tabContainer.removeChild(this.tab);
tabGroup.viewContainer.removeChild(this.webview); tabGroup.viewContainer.removeChild(this.webview);
let activeTab = this.tabGroup.getActiveTab(); const activeTab = this.tabGroup.getActiveTab();
TabGroupPrivate.removeTab.bind(tabGroup)(this, true); tabGroup.removeTab(this, true);
this.emit("close", this); this.emit("close", this);
if (activeTab.id === this.id) { if (activeTab.id === this.id) {
TabGroupPrivate.activateRecentTab.bind(tabGroup)(); tabGroup.activateRecentTab();
} }
} }
} }
const TabPrivate = {
initTab: function () {
let tabClass = this.tabGroup.options.tabClass;
// Create tab element
let tab = this.tab = document.createElement("div");
tab.classList.add(tabClass);
for (let el of ["icon", "title", "buttons", "badge"]) {
let span = tab.appendChild(document.createElement("span"));
span.classList.add(`${tabClass}-${el}`);
this.tabElements[el] = span;
}
this.setTitle(this.title);
this.setBadge(this.badge);
this.setIcon(this.iconURL, this.icon);
TabPrivate.initTabButtons.bind(this)();
TabPrivate.initTabClickHandler.bind(this)();
this.tabGroup.tabContainer.appendChild(this.tab);
},
initTabButtons: function () {
let container = this.tabElements.buttons;
let tabClass = this.tabGroup.options.tabClass;
if (this.closable) {
let button = container.appendChild(document.createElement("button"));
button.classList.add(`${tabClass}-button-close`);
button.innerHTML = this.tabGroup.options.closeButtonText;
button.addEventListener("click", this.close.bind(this, false), false);
}
},
initTabClickHandler: function () {
// Mouse up
const tabClickHandler = function (e) {
if (this.isClosed) return;
if (e.which === 2) {
this.close();
}
};
this.tab.addEventListener("mouseup", tabClickHandler.bind(this), false);
// Mouse down
const tabMouseDownHandler = function (e) {
if (this.isClosed) return;
if (e.which === 1) {
if (e.target.matches("button")) return;
this.activate();
}
};
this.tab.addEventListener("mousedown", tabMouseDownHandler.bind(this), false);
},
initWebview: function () {
const webview = this.webview = document.createElement("webview");
const tabWebviewDidFinishLoadHandler = function (e) {
this.emit("webview-ready", this);
};
this.webview.addEventListener("did-finish-load", tabWebviewDidFinishLoadHandler.bind(this), false);
const tabWebviewDomReadyHandler = function (e) {
// Remove this once https://github.com/electron/electron/issues/14474 is fixed
webview.blur();
webview.focus();
this.emit("webview-dom-ready", this);
};
this.webview.addEventListener("dom-ready", tabWebviewDomReadyHandler.bind(this), false);
this.webview.classList.add(this.tabGroup.options.viewClass);
if (this.webviewAttributes) {
let attrs = this.webviewAttributes;
for (let key in attrs) {
const attr = attrs[key];
if (attr === false) continue;
this.webview.setAttribute(key, attr);
}
}
this.tabGroup.viewContainer.appendChild(this.webview);
}
};
/**
* This makes the browser EventTarget API work similar to EventEmitter
*/
const eventEmitterMixin = {
emit (type, ...args) {
if (type === "ready") {
this.isReady = true;
}
this.dispatchEvent(new CustomEvent(type, { detail: args }));
},
on (type, fn) {
if (type === "ready" && this.isReady === true) {
fn.apply(this, [this]);
}
this.addEventListener(type, ({ detail }) => fn.apply(this, detail));
},
once (type, fn) {
if (type === "ready" && this.isReady === true) {
fn.apply(this, [this]);
}
this.addEventListener(type, ({ detail }) => fn.apply(this, detail), { once: true });
}
};
Object.assign(TabGroup.prototype, eventEmitterMixin);
Object.assign(Tab.prototype, eventEmitterMixin);
customElements.define("tab-group", TabGroup); customElements.define("tab-group", TabGroup);

View file

@ -1,4 +0,0 @@
import Sortable from "sortablejs";
import "./index.js";
window.Sortable = Sortable;

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es2021",
"noImplicitAny": true,
"noUnusedParameters": false,
"allowSyntheticDefaultImports": true
}
}