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 {
buttonContainer: HTMLDivElement;
isReady: boolean;
newTabId: number;
options: TabGroupOptions;
shadow: ShadowRoot;
tabContainer: HTMLDivElement;
tabs: Array<Tab>;
viewContainer: HTMLDivElement;
constructor() { 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,15 +129,50 @@ class TabGroup extends HTMLElement {
this.emit("ready", this); this.emit("ready", this);
} }
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() { 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() {
if (!this.options.newTabButton) return;
const button = this.buttonContainer.appendChild(document.createElement("button"));
button.classList.add(`${this.options.tabClass}-button-new`);
button.innerHTML = this.options.newTabButtonText;
button.addEventListener("click", this.addTab.bind(this, undefined), false);
}
private initVisibility() {
function toggleTabsVisibility(tab: Tab, tabGroup: TabGroup) {
const visibilityThreshold = this.options.visibilityThreshold;
const el = tabGroup.tabContainer.parentElement;
if (this.tabs.length >= visibilityThreshold) {
el.classList.add("visible");
} else {
el.classList.remove("visible");
}
}
this.on("tab-added", toggleTabsVisibility);
this.on("tab-removed", toggleTabsVisibility);
}
setDefaultTab(tab: TabOptions) {
this.options.defaultTab = tab; this.options.defaultTab = tab;
} }
@ -94,9 +180,9 @@ class TabGroup extends HTMLElement {
if (typeof args === "function") { if (typeof args === "function") {
args = args(this); args = args(this);
} }
let id = this.newTabId; const id = this.newTabId;
this.newTabId++; this.newTabId++;
let tab = new Tab(this, id, args); const tab = new Tab(this, id, args);
this.tabs.push(tab); this.tabs.push(tab);
// Don't call tab.activate() before a tab is referenced in this.tabs // Don't call tab.activate() before a tab is referenced in this.tabs
if (args.active === true) { if (args.active === true) {
@ -106,7 +192,7 @@ class TabGroup extends HTMLElement {
return tab; return tab;
} }
getTab (id) { 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) {
return this.tabs[i]; return this.tabs[i];
@ -115,8 +201,8 @@ class TabGroup extends HTMLElement {
return null; return null;
} }
getTabByPosition (position) { getTabByPosition(position: number) {
let fromRight = position < 0; const fromRight = position < 0;
for (let i in this.tabs) { for (let i in this.tabs) {
if (this.tabs[i].getPosition(fromRight) === position) { if (this.tabs[i].getPosition(fromRight) === position) {
return this.tabs[i]; return this.tabs[i];
@ -125,7 +211,7 @@ class TabGroup extends HTMLElement {
return null; return null;
} }
getTabByRelPosition (position) { getTabByRelPosition(position: number) {
position = this.getActiveTab().getPosition() + position; position = this.getActiveTab().getPosition() + position;
if (position <= 0) { if (position <= 0) {
return null; return null;
@ -145,85 +231,70 @@ class TabGroup extends HTMLElement {
return this.tabs.slice(); return this.tabs.slice();
} }
eachTab (fn) { eachTab(fn: (tab: Tab) => void) {
this.getTabs().forEach(fn); this.getTabs().forEach(fn);
return this;
} }
getActiveTab() { getActiveTab() {
if (this.tabs.length === 0) return null; if (this.tabs.length === 0) return null;
return this.tabs[0]; return this.tabs[0];
} }
setActiveTab(tab: Tab) {
this.removeTab(tab);
this.tabs.unshift(tab);
this.emit("tab-active", tab, this);
} }
const TabGroupPrivate = { removeTab(tab: Tab, triggerEvent = false) {
initNewTabButton: function () { const id = tab.id;
if (!this.options.newTabButton) return; const index = this.tabs.findIndex((t: Tab) => t.id === id);
let button = this.buttonContainer.appendChild(document.createElement("button")); this.tabs.splice(index, 1);
button.classList.add(`${this.options.tabClass}-button-new`);
button.innerHTML = this.options.newTabButtonText;
button.addEventListener("click", this.addTab.bind(this, undefined), false);
},
initVisibility: function () {
function toggleTabsVisibility(tab, tabGroup) {
var visibilityThreshold = this.options.visibilityThreshold;
var el = tabGroup.tabContainer.parentNode;
if (this.tabs.length >= visibilityThreshold) {
el.classList.add("visible");
} else {
el.classList.remove("visible");
}
}
this.on("tab-added", toggleTabsVisibility);
this.on("tab-removed", toggleTabsVisibility);
},
removeTab: function (tab, triggerEvent) {
let id = tab.id;
for (let i in this.tabs) {
if (this.tabs[i].id === id) {
this.tabs.splice(i, 1);
break;
}
}
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 (this.isClosed) return;
let span = this.tabElements.title; 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;
const span = this.tabElements.title;
span.innerHTML = title; span.innerHTML = title;
span.title = title; span.title = title;
this.title = title; this.title = title;
@ -249,9 +415,9 @@ class Tab extends EventTarget {
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) {
@ -269,11 +435,11 @@ class Tab extends EventTarget {
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);
@ -291,10 +457,10 @@ class Tab extends EventTarget {
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;
@ -338,13 +504,13 @@ class Tab extends EventTarget {
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 {
@ -368,7 +534,7 @@ class Tab extends EventTarget {
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");
@ -384,11 +550,11 @@ class Tab extends EventTarget {
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
}
}