Merge pull request #151 from brrd/ts
Version 1.0.0 Breaking changes: * Switched to Web Components in order to match Electron security recommendations. * Written in TypeScript. * Built with Parcel. * CSS styles are now embedded in the module. * Sortable.js is included in the bundle. * `tab.flash()` and `tab.unflash()` methods are deprecated. * Tab position is now 0-index based (#46). * A lot of other things changed in the API (see README).
This commit is contained in:
commit
2d47be5dcf
22 changed files with 5847 additions and 1599 deletions
32
.eslintrc.json
Normal file
32
.eslintrc.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"args": "none"
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -35,3 +35,6 @@ jspm_packages
|
|||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Parcel
|
||||
.parcel-cache
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"undef": true,
|
||||
"unused": "vars",
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"eqnull": true,
|
||||
"esnext": true,
|
||||
"indent": 4
|
||||
}
|
266
README.md
266
README.md
|
@ -4,104 +4,114 @@
|
|||
|
||||
![Electron Tab Demo](screenshot.jpg)
|
||||
|
||||
## Features
|
||||
|
||||
* :electron: Compatible with Electron ≥ 17.
|
||||
* :lock: Compliant with [Electron security recommendations](https://www.electronjs.org/docs/latest/tutorial/security) (works without `nodeIntegration: true`).
|
||||
* :toolbox: Written with TypeScript and Web Components.
|
||||
* :hand: Supports drag and drop out of the box.
|
||||
* :art: Easily customizable.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ npm install --save electron-tabs
|
||||
```bash
|
||||
npm install --save electron-tabs
|
||||
```
|
||||
|
||||
## Demo
|
||||
## Getting started
|
||||
|
||||
```
|
||||
$ npm run demo
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Electron-tabs uses webviews, so you first need to use the following `webPreferences` options in the main process:
|
||||
Define the following `webPreferences` options in the main process:
|
||||
|
||||
```js
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false, // needed for Electron >= 12.x
|
||||
webviewTag: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Then add the following elements to the app page:
|
||||
Then add the following markup where you want the tabs to display:
|
||||
|
||||
```html
|
||||
<div class="etabs-tabgroup">
|
||||
<div class="etabs-tabs"></div>
|
||||
<div class="etabs-buttons"></div>
|
||||
</div>
|
||||
<div class="etabs-views"></div>
|
||||
<tab-group></tab-group>
|
||||
|
||||
<script src="node_modules/electron-tabs/dist/electron-tabs.js"></script>
|
||||
```
|
||||
|
||||
And call the module in the renderer process:
|
||||
## Options
|
||||
|
||||
```javascript
|
||||
const TabGroup = require("electron-tabs");
|
||||
```
|
||||
|
||||
Now you can initialize a tab group and add tabs to it:
|
||||
|
||||
```javascript
|
||||
let tabGroup = new TabGroup();
|
||||
let tab = tabGroup.addTab({
|
||||
title: "Electron",
|
||||
src: "http://electron.atom.io",
|
||||
visible: true
|
||||
});
|
||||
```
|
||||
|
||||
If you don't want to write your own styles, you can also insert the sample electron-tabs stylesheet in the page header:
|
||||
You can add options by setting `<tab-group>` element attributes:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="node_modules/electron-tabs/electron-tabs.css">
|
||||
<tab-group new-tab-button="true" sortable="true"></tab-group>
|
||||
```
|
||||
|
||||
### Note
|
||||
The following attributes are supported:
|
||||
|
||||
Please note, there is a known issue in some versions of Electron that prevents the process to completely shut down and it remains hanging in Background Processes (Windows 10). If you encounter that issue please use the workaround provided at https://github.com/electron/electron/issues/13939
|
||||
* `close-button-text` (string): text of the tabs "Close" button.
|
||||
* `new-tab-button` (boolean): set it to true to display the "New Tab" button.
|
||||
* `new-tab-button-text` (string): text of the "New Tab" button.
|
||||
* `sortable` (boolean): set it to true to make the tabs sortable by drag and drop.
|
||||
* `visibility-threshold` (number): the minimum number of tabs necessary for the tab bar to be displayed. 0 (default) means that it will always remain visible.
|
||||
|
||||
## API
|
||||
## Methods
|
||||
|
||||
### Tab Group
|
||||
Use `TabGroup` methods and manipulate tabs in a script after calling `electron-tabs.js`.
|
||||
|
||||
Represents the main tab container.
|
||||
```html
|
||||
<tab-group new-tab-button="true"></tab-group>
|
||||
|
||||
#### `new TabGroup(options)`
|
||||
<script src="path/to/electron-tabs.js"></script>
|
||||
|
||||
`options` must be an object. The following options are available:
|
||||
<script>
|
||||
// Select tab-group
|
||||
const tabGroup = document.querySelector("tab-group");
|
||||
|
||||
* `tabContainerSelector` (default: `".etabs-tabs"`): CSS selector to target the element where tabs are inserted.
|
||||
* `buttonsContainerSelector` (default: `".etabs-buttons"`): CSS selector to target the element where the "New Tab" button are inserted.
|
||||
* `viewContainerSelector` (default: `".etabs-views"`): CSS selector to target the element where the view are inserted.
|
||||
* `tabClass` (default: `"etabs-tab"`): class to add to tab elements.
|
||||
* `viewClass` (default: `"etabs-view"`): class to add to webview elements.
|
||||
* `closeButtonText` (default: `"✖"`): "close tab" button text.
|
||||
* `newTabButtonText` (default: `"+"`): "New Tab" button text.
|
||||
* `newTab` (default: `undefined`): arguments to use when `.addTab()` is called without parameters. It can be an object or a function which returns an object. It determines the options to use when the "New Tab" button is triggered. If you leave it undefined then the "New Tab" button won't be displayed.
|
||||
* `visibilityThreshold` (default: `0`): the minimum number of tabs necessary for the tabGroup to be displayed. `0` means tabGround will always remain visible.
|
||||
* `ready` (default: `undefined`): a callback function to call once the tab group is ready. The `TabGroup` instance is passed as the only parameter.
|
||||
// Setup the default tab which is created when the "New Tab" button is clicked
|
||||
tabGroup.setDefaultTab({
|
||||
title: "New Page",
|
||||
src: "path/to/new-page.html",
|
||||
active: true
|
||||
});
|
||||
|
||||
// Do stuff
|
||||
const tab = tabGroup.addTab({
|
||||
title: "electron-tabs on NPM",
|
||||
src: "https://www.npmjs.com/package/electron-tabs"
|
||||
});
|
||||
const pos = tab.getPosition();
|
||||
console.log("Tab position is " + pos);
|
||||
</script>
|
||||
```
|
||||
|
||||
### TabGroup
|
||||
|
||||
#### `tabGroup.addTab(options)`
|
||||
|
||||
Add a new tab to the tab group and returns a `Tab` instance.
|
||||
Add a new tab and returns the related `Tab` instance.
|
||||
|
||||
* `title`: tab title.
|
||||
* `src`: URL to the page which will be loaded into the view. This is actually the same than `options.webview.src`.
|
||||
* `badge`: optional text to put into a badge, badge will be hidden if it's falsey
|
||||
* `badge`: optional text to put into a badge, badge will be hidden if false.
|
||||
* `iconURL`: optional URL to the tab icon.
|
||||
* `icon`: optional code for a tab icon. Can be used with symbol libraries (example with Font Awesome: `icon: 'fa fa-icon-name'`). This attribute is ignored if an `iconURL` was given.
|
||||
* `closable` (default: `true`): if set to `true` the close button won't be displayed and the user won't be able to close the tab. See also `tab.close()`.
|
||||
* `webviewAttributes`: attributes to add to the webview tag. See [webview documentation](http://electron.atom.io/docs/api/web-view-tag/#tag-attributes).
|
||||
* `visible` (default: `true`): set this to `false` if you don't want to display the tab once it is loaded. If set to `false` then you will need to call `tab.show()` to display the tab.
|
||||
* `active` (default: `false`): set this to `true` if you want to activate the tab once it is loaded. Otherwise you will need to call `tab.activate()`.
|
||||
* `ready`: a callback function to call once the tab is ready. The `Tab` instance is passed as the only parameter.
|
||||
* `webviewAttributes`: attributes to add to the webview tag. See [webview documentation](http://electron.atom.io/docs/api/web-view-tag/#tag-attributes).
|
||||
|
||||
### `tabGroup.setDefaultTab(options)`
|
||||
|
||||
Define default options to use for creating the tab when the "New Tab" button is clicked or when calling `tabGroup.addTab()` with no parameter.
|
||||
|
||||
```javascript
|
||||
tabGroup.setDefaultTab({
|
||||
title: "New Page",
|
||||
src: "path/to/new-page.html",
|
||||
active: true
|
||||
});
|
||||
```
|
||||
|
||||
#### `tabGroup.getTab(id)`
|
||||
|
||||
|
@ -123,17 +133,17 @@ To get the tab in the rightmost position:
|
|||
tabGroup.getTabByPosition(-1);
|
||||
```
|
||||
|
||||
> Note: Position 0 does not contain a tab.
|
||||
|
||||
#### `tabGroup.getTabByRelPosition(position)`
|
||||
|
||||
Retrieve an instance of `Tab` from this `position` relative to the active tab (return `null` if not found).
|
||||
|
||||
`tabGroup.getNextTab()` is an alias to `tabGroup.getTabByRelPosition(1)`.
|
||||
|
||||
`tabGroup.getPreviousTab()` is an alias to `tabGroup.getTabByRelPosition(-1)`.
|
||||
|
||||
#### `tabGroup.getActiveTab()`
|
||||
|
||||
Return the currently active tab (otherwise return `null`).
|
||||
Return the active tab (return `null` if none).
|
||||
|
||||
#### `tabGroup.getTabs()`
|
||||
|
||||
|
@ -179,7 +189,7 @@ Get current tab icon URL / icon.
|
|||
|
||||
#### `tab.setPosition(newPosition)`
|
||||
|
||||
Move tab to the specified position. If `position` is 0 then `null` is returned and nothing happens. See [`tabGroup.getTabByPosition`](#tabgroupgettabbypositionposition) for information about positions.
|
||||
Move tab to the specified position. See [`tabGroup.getTabByPosition`](#tabgroupgettabbypositionposition) for information about positions.
|
||||
|
||||
#### `tab.getPosition(fromRight)`
|
||||
|
||||
|
@ -193,30 +203,17 @@ Activate this tab. The class "active" is added to the active tab.
|
|||
|
||||
Toggle the "visible" class on the tab. `tab.hide()` is an alias to `tab.show(false)`.
|
||||
|
||||
#### `tab.flash(flag)`
|
||||
|
||||
Toggle the "flash" class on the tab. `tab.unflash()` is an alias to `tab.flash(false)`.
|
||||
|
||||
#### `tab.hasClass(classname)`
|
||||
|
||||
Return `true` if the tab element has the specified classname. Useful for checking if a tab is "active", "visible" of "flash".
|
||||
Return `true` if the tab element has the specified classname. Useful for checking if a tab is "active" or "visible".
|
||||
|
||||
#### `tab.close(force)`
|
||||
|
||||
Close the tab (and activate another tab if relevant). When `force` is set to `true` the tab will be closed even if it is not `closable`.
|
||||
|
||||
### Access webview element
|
||||
## Events
|
||||
|
||||
You can access the webview element and use its methods with through the `Tab.webview` attribute. See [webview documentation](https://electronjs.org/docs/api/webview-tag#methods).
|
||||
|
||||
```javascript
|
||||
let webview = tab.webview;
|
||||
webview.loadURL("file://path/to/new/page.html");
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
The following events are available:
|
||||
The following events are emitted:
|
||||
|
||||
* `tabGroup.on("tab-added", (tab, tabGroup) => { ... });`
|
||||
* `tabGroup.on("tab-removed", (tab, tabGroup) => { ... });`
|
||||
|
@ -230,8 +227,6 @@ The following events are available:
|
|||
* `tab.on("inactive", (tab) => { ... });`
|
||||
* `tab.on("visible", (tab) => { ... });`
|
||||
* `tab.on("hidden", (tab) => { ... });`
|
||||
* `tab.on("flash", (tab) => { ... });`
|
||||
* `tab.on("unflash", (tab) => { ... });`
|
||||
* `tab.on("close", (tab) => { ... });`
|
||||
* `tab.on("closing", (tab, abort) => { ... });` (Use `abort()` function to cancel closing)
|
||||
|
||||
|
@ -240,37 +235,100 @@ You can also use `tab.once` to automatically remove the listener when invoked:
|
|||
* `tab.once("webview-ready", (tab) => { ... });`
|
||||
* `tab.once("webview-dom-ready", (tab) => { ... });`
|
||||
|
||||
## Drag and drop support
|
||||
## Access Electron webview element
|
||||
|
||||
Electron-tabs is compatible with [Dragula](https://github.com/bevacqua/dragula) so you can easily make your tabs draggable.
|
||||
|
||||
Install Dragula:
|
||||
|
||||
```
|
||||
npm install dragula --save
|
||||
```
|
||||
|
||||
Don't forget to add a link to its stylesheet in the header:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="node_modules/dragula/dist/dragula.css">
|
||||
```
|
||||
|
||||
Then call Dragula in your script once tabGroup is ready:
|
||||
You can access the webview element and use its methods with through the `Tab.webview` attribute. See [webview documentation](https://electronjs.org/docs/api/webview-tag#methods).
|
||||
|
||||
```javascript
|
||||
const TabGroup = require("electron-tabs");
|
||||
const dragula = require("dragula");
|
||||
let webview = tab.webview;
|
||||
webview.loadURL("file://path/to/new/page.html");
|
||||
```
|
||||
|
||||
var tabGroup = new TabGroup({
|
||||
ready: function (tabGroup) {
|
||||
dragula([tabGroup.tabContainer], {
|
||||
direction: "horizontal"
|
||||
});
|
||||
}
|
||||
});
|
||||
## Custom styles
|
||||
|
||||
To customize tab-group styles, set new values to [electron-tabs CSS variables](https://github.com/brrd/electron-tabs/blob/master/src/style.css) in your application stylesheet.
|
||||
|
||||
Since `TabGroup` is a Web Component you won't be able to change its styles directly from your app stylesheet. If you need more control over it then you can add a `<style>` tag inside the `<tab-group >` element:
|
||||
|
||||
```html
|
||||
<tab-group new-tab-button="true" sortable="true">
|
||||
<style>
|
||||
/* Write your own CSS rules here... */
|
||||
</style>
|
||||
</tab-group>
|
||||
```
|
||||
|
||||
This method is particularly useful when you need to define custom badges or tab styles:
|
||||
|
||||
```html
|
||||
<tab-group new-tab-button="true" sortable="true">
|
||||
<style>
|
||||
/* Add custom styles */
|
||||
.my-badge {
|
||||
background-color: orange;
|
||||
}
|
||||
.my-custom-tab {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</tab-group>
|
||||
|
||||
<script src="path/to/electron-tabs.js"></script>
|
||||
|
||||
<script>
|
||||
const tabGroup = document.querySelector("tab-group");
|
||||
|
||||
tabGroup.addTab({
|
||||
title: "Tab with custom badge",
|
||||
src: "page.html",
|
||||
badge: {
|
||||
text: "5",
|
||||
classname: "my-badge"
|
||||
}
|
||||
});
|
||||
|
||||
tabGroup.addTab({
|
||||
title: "Tab with custom style",
|
||||
src: "page.html",
|
||||
ready: function(tab) {
|
||||
tab.element.classList.add("my-custom-tab");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
`electron-tabs` uses TypeScript and Parcel under the hood.
|
||||
|
||||
### Requirements
|
||||
|
||||
Git and Node 12+.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Clone this repo
|
||||
git clone git@github.com:brrd/electron-tabs.git
|
||||
cd electron-tabs
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# ...or watch
|
||||
npm run watch
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
```bash
|
||||
npm run demo
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT) - Copyright (c) 2016 Thomas Brouard
|
||||
The MIT License (MIT) - Copyright (c) 2022 Thomas Brouard
|
||||
|
|
1
declarations.d.ts
vendored
Normal file
1
declarations.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "*.css";
|
10
demo/app.js
10
demo/app.js
|
@ -1,16 +1,14 @@
|
|||
const electron = require('electron');
|
||||
const electron = require("electron");
|
||||
const app = electron.app;
|
||||
|
||||
app.on('ready', function () {
|
||||
app.on("ready", function () {
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webviewTag: true
|
||||
}
|
||||
});
|
||||
mainWindow.loadURL('file://' + __dirname + '/electron-tabs.html');
|
||||
mainWindow.on('ready-to-show', function () {
|
||||
mainWindow.loadURL("file://" + __dirname + "/electron-tabs.html");
|
||||
mainWindow.on("ready-to-show", function () {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
|
|
@ -3,39 +3,58 @@
|
|||
|
||||
<head>
|
||||
<title>electron-tabs-demo</title>
|
||||
<link rel="stylesheet" href="../electron-tabs.css">
|
||||
</head>
|
||||
<body style="margin:0">
|
||||
|
||||
<div class="etabs-tabgroup">
|
||||
<div class="etabs-tabs"></div>
|
||||
<div class="etabs-buttons"></div>
|
||||
</div>
|
||||
<div class="etabs-views"></div>
|
||||
|
||||
<tab-group new-tab-button="true" sortable="true">
|
||||
<style>
|
||||
/* Add custom styles */
|
||||
.my-badge {
|
||||
background-color: #327BB1;
|
||||
}
|
||||
.my-custom-tab {
|
||||
color: #d135d1;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</tab-group>
|
||||
|
||||
<script src="../dist/electron-tabs.js"></script>
|
||||
<script>
|
||||
//const TabGroup = require('electron-tabs') normally but for demo :
|
||||
const TabGroup = require("../index");
|
||||
const tabGroup = document.querySelector("tab-group");
|
||||
tabGroup.on("ready", () => console.info("TabGroup is ready"));
|
||||
|
||||
let tabGroup = new TabGroup({
|
||||
newTab: {
|
||||
title: 'New Tab'
|
||||
tabGroup.setDefaultTab({
|
||||
title: "Wikipedia",
|
||||
src: "https://www.wikipedia.org/",
|
||||
active: true,
|
||||
ready: () => console.info("New Tab is ready")
|
||||
});
|
||||
|
||||
tabGroup.addTab({
|
||||
title: "electron-tabs on NPM",
|
||||
src: "https://www.npmjs.com/package/electron-tabs",
|
||||
badge: {
|
||||
text: "5",
|
||||
classname: "my-badge"
|
||||
}
|
||||
});
|
||||
|
||||
tabGroup.addTab({
|
||||
title: 'Google',
|
||||
src: 'http://google.com',
|
||||
});
|
||||
|
||||
tabGroup.addTab({
|
||||
title: "Electron",
|
||||
src: "http://electron.atom.io",
|
||||
visible: true,
|
||||
title: "electron-tabs on Github",
|
||||
src: "https://github.com/brrd/electron-tabs",
|
||||
iconURL: "mark-github.svg",
|
||||
active: true
|
||||
});
|
||||
|
||||
tabGroup.addTab({
|
||||
title: "My Custom Tab",
|
||||
src: "page.html",
|
||||
ready: function(tab) {
|
||||
tab.element.classList.add("my-custom-tab");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
1
demo/mark-github.svg
Normal file
1
demo/mark-github.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
After Width: | Height: | Size: 695 B |
13
demo/page.html
Normal file
13
demo/page.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Local Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
<p>This is a local page.</p>
|
||||
</body>
|
||||
</html>
|
96
dist/electron-tabs.d.ts
vendored
Normal file
96
dist/electron-tabs.d.ts
vendored
Normal file
|
@ -0,0 +1,96 @@
|
|||
import Sortable from "sortablejs";
|
||||
interface TabGroupOptions {
|
||||
closeButtonText: string;
|
||||
defaultTab: TabOptions | ((tabGroup: TabGroup) => TabOptions);
|
||||
newTabButton: boolean;
|
||||
newTabButtonText: string;
|
||||
sortable: boolean;
|
||||
sortableOptions?: Sortable.Options;
|
||||
visibilityThreshold: number;
|
||||
}
|
||||
interface TabOptions {
|
||||
active?: boolean;
|
||||
badge?: Badge;
|
||||
closable?: boolean;
|
||||
icon?: string;
|
||||
iconURL?: string;
|
||||
ready?: ((tab: Tab) => void);
|
||||
src?: string;
|
||||
title?: string;
|
||||
visible?: boolean;
|
||||
webviewAttributes?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
interface Badge {
|
||||
text: string;
|
||||
classname: string;
|
||||
}
|
||||
export class TabGroup extends HTMLElement {
|
||||
buttonContainer: HTMLDivElement;
|
||||
isReady: boolean;
|
||||
newTabId: number;
|
||||
options: TabGroupOptions;
|
||||
shadow: ShadowRoot;
|
||||
tabContainer: HTMLDivElement;
|
||||
tabs: Array<Tab>;
|
||||
viewContainer: HTMLDivElement;
|
||||
constructor();
|
||||
emit(type: string, ...args: any[]): void;
|
||||
on(type: string, fn: (...detail: any[]) => void): void;
|
||||
once(type: string, fn: (detail: string) => void): void;
|
||||
connectedCallback(): void;
|
||||
initSortable(): void;
|
||||
setDefaultTab(tab: TabOptions): void;
|
||||
addTab(args?: TabOptions | ((tabGroup: TabGroup) => TabOptions)): Tab;
|
||||
getTab(id: number): Tab;
|
||||
getTabByPosition(position: number): Tab;
|
||||
getTabByRelPosition(position: number): Tab;
|
||||
getNextTab(): Tab;
|
||||
getPreviousTab(): Tab;
|
||||
getTabs(): Tab[];
|
||||
eachTab(fn: (tab: Tab) => void): void;
|
||||
getActiveTab(): Tab;
|
||||
setActiveTab(tab: Tab): void;
|
||||
removeTab(tab: Tab, triggerEvent?: boolean): void;
|
||||
activateRecentTab(): void;
|
||||
}
|
||||
export class Tab extends EventTarget {
|
||||
badge: Badge;
|
||||
closable: boolean;
|
||||
element: HTMLDivElement;
|
||||
icon: string;
|
||||
iconURL: string;
|
||||
id: number;
|
||||
isClosed: boolean;
|
||||
isReady: boolean;
|
||||
spans: {
|
||||
[key: string]: HTMLSpanElement;
|
||||
};
|
||||
tabGroup: TabGroup;
|
||||
title: string;
|
||||
webview: HTMLElement;
|
||||
webviewAttributes: {
|
||||
[key: string]: any;
|
||||
};
|
||||
constructor(tabGroup: TabGroup, id: number, args: TabOptions);
|
||||
emit(type: string, ...args: any[]): void;
|
||||
on(type: string, fn: (...detail: any[]) => void): void;
|
||||
once(type: string, fn: (detail: string) => void): void;
|
||||
initWebview(): void;
|
||||
setTitle(title: string): this;
|
||||
getTitle(): string;
|
||||
setBadge(badge?: Badge): void;
|
||||
getBadge(): Badge;
|
||||
setIcon(iconURL: string, icon: string): this;
|
||||
getIcon(): string;
|
||||
setPosition(newPosition: number): this;
|
||||
getPosition(fromRight?: boolean): number;
|
||||
activate(): this;
|
||||
show(flag?: boolean): this;
|
||||
hide(): this;
|
||||
hasClass(classname: string): boolean;
|
||||
close(force: boolean): void;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=electron-tabs.d.ts.map
|
1
dist/electron-tabs.d.ts.map
vendored
Normal file
1
dist/electron-tabs.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2906
dist/electron-tabs.js
vendored
Normal file
2906
dist/electron-tabs.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
dist/electron-tabs.js.map
vendored
Normal file
1
dist/electron-tabs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,139 +0,0 @@
|
|||
.etabs-tabgroup {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background-color: #ccc;
|
||||
cursor: default;
|
||||
font: caption;
|
||||
font-size: 14px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.etabs-tabgroup.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.etabs-tabs {
|
||||
}
|
||||
|
||||
.etabs-tab {
|
||||
display: none;
|
||||
position: relative;
|
||||
color: #333;
|
||||
height: 22px;
|
||||
padding: 6px 8px 4px;
|
||||
border: 1px solid #aaa;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
background: linear-gradient(to bottom, rgba(234,234,234,1) 0%,rgba(204,204,204,1) 100%);
|
||||
font: caption;
|
||||
font-size: 14px;
|
||||
background-color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Dragula */
|
||||
.etabs-tab.gu-mirror {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.etabs-tab:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.etabs-tab.visible {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.etabs-tab.active {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.etabs-tab.flash {
|
||||
background: linear-gradient(to bottom, rgba(255,243,170,1) 0%,rgba(255,227,37,1) 100%);
|
||||
}
|
||||
|
||||
.etabs-buttons {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.etabs-buttons button {
|
||||
float: left;
|
||||
color: #333;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.etabs-buttons button:hover {
|
||||
color: #eee;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.etabs-tab-badge {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -7px;
|
||||
background: red;
|
||||
border-radius: 100%;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.etabs-tab-badge.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.etabs-tab-icon {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.etabs-tab-icon img {
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
.etabs-tab-title {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.etabs-tab-buttons {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.etabs-tab-buttons button {
|
||||
display: inline-block;
|
||||
color: #333;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.etabs-tab-buttons button:hover {
|
||||
color: #eee;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.etabs-views {
|
||||
position: relative;
|
||||
border-top: 1px solid #aaa;
|
||||
height: calc(100vh - 33px);
|
||||
}
|
||||
|
||||
.etab-view {
|
||||
position: relative;
|
||||
}
|
118
index.d.ts
vendored
118
index.d.ts
vendored
|
@ -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;
|
472
index.js
472
index.js
|
@ -1,472 +0,0 @@
|
|||
if (!document) {
|
||||
throw Error("electron-tabs module must be called in renderer process");
|
||||
}
|
||||
|
||||
// Inject styles
|
||||
(function () {
|
||||
const styles = `
|
||||
webview {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
webview.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
let styleTag = document.createElement("style");
|
||||
styleTag.innerHTML = styles;
|
||||
document.getElementsByTagName("head")[0].appendChild(styleTag);
|
||||
})();
|
||||
|
||||
/**
|
||||
* This makes the browser EventTarget API work similar to EventEmitter
|
||||
*/
|
||||
class EventEmitter extends EventTarget {
|
||||
emit (type, ...args) {
|
||||
this.dispatchEvent(new CustomEvent(type, { detail: args }));
|
||||
}
|
||||
|
||||
on (type, fn) {
|
||||
this.addEventListener(type, ({ detail }) => fn.apply(this, detail));
|
||||
}
|
||||
|
||||
once (type, fn) {
|
||||
this.addEventListener(type, ({ detail }) => fn.apply(this, detail), { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
class TabGroup extends EventEmitter {
|
||||
constructor (args = {}) {
|
||||
super();
|
||||
let options = this.options = {
|
||||
tabContainerSelector: args.tabContainerSelector || ".etabs-tabs",
|
||||
buttonsContainerSelector: args.buttonsContainerSelector || ".etabs-buttons",
|
||||
viewContainerSelector: args.viewContainerSelector || ".etabs-views",
|
||||
tabClass: args.tabClass || "etabs-tab",
|
||||
viewClass: args.viewClass || "etabs-view",
|
||||
closeButtonText: args.closeButtonText || "×",
|
||||
newTab: args.newTab,
|
||||
newTabButtonText: args.newTabButtonText || "+",
|
||||
visibilityThreshold: args.visibilityThreshold || 0,
|
||||
ready: args.ready
|
||||
};
|
||||
this.tabContainer = document.querySelector(options.tabContainerSelector);
|
||||
this.viewContainer = document.querySelector(options.viewContainerSelector);
|
||||
this.tabs = [];
|
||||
this.newTabId = 0;
|
||||
TabGroupPrivate.initNewTabButton.bind(this)();
|
||||
TabGroupPrivate.initVisibility.bind(this)();
|
||||
if (typeof this.options.ready === "function") {
|
||||
this.options.ready(this);
|
||||
}
|
||||
}
|
||||
|
||||
addTab (args = this.options.newTab) {
|
||||
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.newTab) return;
|
||||
let container = document.querySelector(this.options.buttonsContainerSelector);
|
||||
let button = container.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);
|
||||
},
|
||||
|
||||
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) {
|
||||
this.emit("tab-removed", tab, this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
setActiveTab: function (tab) {
|
||||
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) {
|
||||
this.tabs[0].activate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
class Tab extends EventEmitter {
|
||||
constructor (tabGroup, id, args) {
|
||||
super();
|
||||
this.tabGroup = tabGroup;
|
||||
this.id = id;
|
||||
this.title = args.title;
|
||||
this.badge = args.badge;
|
||||
this.iconURL = args.iconURL;
|
||||
this.icon = args.icon;
|
||||
this.closable = args.closable === false ? false : true;
|
||||
this.webviewAttributes = args.webviewAttributes || {};
|
||||
this.webviewAttributes.src = args.src;
|
||||
this.tabElements = {};
|
||||
TabPrivate.initTab.bind(this)();
|
||||
TabPrivate.initWebview.bind(this)();
|
||||
if (args.visible !== false) {
|
||||
this.show();
|
||||
}
|
||||
if (typeof args.ready === "function") {
|
||||
args.ready(this);
|
||||
}
|
||||
}
|
||||
|
||||
setTitle (title) {
|
||||
if (this.isClosed) return;
|
||||
let span = this.tabElements.title;
|
||||
span.innerHTML = title;
|
||||
span.title = title;
|
||||
this.title = title;
|
||||
this.emit("title-changed", title, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
getTitle () {
|
||||
if (this.isClosed) return;
|
||||
return this.title;
|
||||
}
|
||||
|
||||
setBadge (badge) {
|
||||
if (this.isClosed) return;
|
||||
let span = this.tabElements.badge;
|
||||
this.badge = badge;
|
||||
|
||||
if (badge) {
|
||||
span.innerHTML = badge;
|
||||
span.classList.remove('hidden');
|
||||
} else {
|
||||
span.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.emit("badge-changed", badge, this);
|
||||
}
|
||||
|
||||
getBadge () {
|
||||
if (this.isClosed) return;
|
||||
return this.badge;
|
||||
}
|
||||
|
||||
setIcon (iconURL, icon) {
|
||||
if (this.isClosed) return;
|
||||
this.iconURL = iconURL;
|
||||
this.icon = icon;
|
||||
let span = this.tabElements.icon;
|
||||
if (iconURL) {
|
||||
span.innerHTML = `<img src="${iconURL}" />`;
|
||||
this.emit("icon-changed", iconURL, this);
|
||||
} else if (icon) {
|
||||
span.innerHTML = `<i class="${icon}"></i>`;
|
||||
this.emit("icon-changed", icon, this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getIcon () {
|
||||
if (this.isClosed) return;
|
||||
if (this.iconURL) return this.iconURL;
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
setPosition (newPosition) {
|
||||
let tabContainer = this.tabGroup.tabContainer;
|
||||
let tabs = tabContainer.children;
|
||||
let oldPosition = this.getPosition() - 1;
|
||||
|
||||
if (newPosition < 0) {
|
||||
newPosition += tabContainer.childElementCount;
|
||||
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0;
|
||||
}
|
||||
} else {
|
||||
if (newPosition > tabContainer.childElementCount) {
|
||||
newPosition = tabContainer.childElementCount;
|
||||
}
|
||||
|
||||
// Make 1 be leftmost position
|
||||
newPosition--;
|
||||
}
|
||||
|
||||
if (newPosition > oldPosition) {
|
||||
newPosition++;
|
||||
}
|
||||
|
||||
tabContainer.insertBefore(tabs[oldPosition], tabs[newPosition]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getPosition (fromRight) {
|
||||
let position = 0;
|
||||
let tab = this.tab;
|
||||
while ((tab = tab.previousSibling) != null) position++;
|
||||
|
||||
if (fromRight === true) {
|
||||
position -= this.tabGroup.tabContainer.childElementCount;
|
||||
}
|
||||
|
||||
if (position >= 0) {
|
||||
position++;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
activate () {
|
||||
if (this.isClosed) return;
|
||||
let activeTab = this.tabGroup.getActiveTab();
|
||||
if (activeTab) {
|
||||
activeTab.tab.classList.remove("active");
|
||||
activeTab.webview.classList.remove("visible");
|
||||
activeTab.emit("inactive", activeTab);
|
||||
}
|
||||
TabGroupPrivate.setActiveTab.bind(this.tabGroup)(this);
|
||||
this.tab.classList.add("active");
|
||||
this.webview.classList.add("visible");
|
||||
this.webview.focus();
|
||||
this.emit("active", this);
|
||||
return this;
|
||||
}
|
||||
|
||||
show (flag) {
|
||||
if (this.isClosed) return;
|
||||
if (flag !== false) {
|
||||
this.tab.classList.add("visible");
|
||||
this.emit("visible", this);
|
||||
} else {
|
||||
this.tab.classList.remove("visible");
|
||||
this.emit("hidden", this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hide () {
|
||||
return this.show(false);
|
||||
}
|
||||
|
||||
flash (flag) {
|
||||
if (this.isClosed) return;
|
||||
if (flag !== false) {
|
||||
this.tab.classList.add("flash");
|
||||
this.emit("flash", this);
|
||||
} else {
|
||||
this.tab.classList.remove("flash");
|
||||
this.emit("unflash", this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unflash () {
|
||||
return this.flash(false);
|
||||
}
|
||||
|
||||
hasClass (classname) {
|
||||
return this.tab.classList.contains(classname);
|
||||
}
|
||||
|
||||
close (force) {
|
||||
const abortController = new AbortController();
|
||||
const abort = () => abortController.abort();
|
||||
this.emit("closing", this, abort);
|
||||
|
||||
const abortSignal = abortController.signal;
|
||||
if (this.isClosed || (!this.closable && !force) || abortSignal.aborted) return;
|
||||
|
||||
this.isClosed = true;
|
||||
let tabGroup = this.tabGroup;
|
||||
tabGroup.tabContainer.removeChild(this.tab);
|
||||
tabGroup.viewContainer.removeChild(this.webview);
|
||||
let activeTab = this.tabGroup.getActiveTab();
|
||||
TabGroupPrivate.removeTab.bind(tabGroup)(this, true);
|
||||
|
||||
this.emit("close", this);
|
||||
|
||||
if (activeTab.id === this.id) {
|
||||
TabGroupPrivate.activateRecentTab.bind(tabGroup)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TabGroup;
|
||||
|
2513
package-lock.json
generated
2513
package-lock.json
generated
File diff suppressed because it is too large
Load diff
24
package.json
24
package.json
|
@ -1,15 +1,23 @@
|
|||
{
|
||||
"name": "electron-tabs",
|
||||
"version": "0.17.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple tabs for Electron applications",
|
||||
"main": "index.js",
|
||||
"main": "dist/electron-tabs.js",
|
||||
"types": "dist/electron-tabs.d.ts",
|
||||
"source": "src/index.ts",
|
||||
"targets": {
|
||||
"main": {
|
||||
"includeNodeModules": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/brrd/electron-tabs"
|
||||
},
|
||||
"scripts": {
|
||||
"demo": "electron ./demo",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"watch": "parcel watch",
|
||||
"build": "parcel build"
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
|
@ -20,6 +28,14 @@
|
|||
"author": "brrd",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^17.1.2"
|
||||
"@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",
|
||||
"electron": "^17.1.2",
|
||||
"parcel": "^2.5.0",
|
||||
"postcss": "^8.4.14",
|
||||
"sortablejs": "^1.15.0",
|
||||
"typescript": "^4.7.2"
|
||||
}
|
||||
}
|
||||
|
|
BIN
screenshot.jpg
BIN
screenshot.jpg
Binary file not shown.
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 155 KiB |
566
src/index.ts
Normal file
566
src/index.ts
Normal file
|
@ -0,0 +1,566 @@
|
|||
import Sortable from "sortablejs";
|
||||
// @ts-ignore
|
||||
import styles from "bundle-text:./style.css";
|
||||
|
||||
if (!document) {
|
||||
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
|
||||
visibilityThreshold: number,
|
||||
}
|
||||
|
||||
interface TabOptions {
|
||||
active?: boolean;
|
||||
badge?: Badge;
|
||||
closable?: boolean;
|
||||
icon?: string;
|
||||
iconURL?: string;
|
||||
ready?: ((tab: Tab) => void);
|
||||
src?: string;
|
||||
title?: string;
|
||||
visible?: boolean;
|
||||
webviewAttributes?: { [key: string]: any };
|
||||
}
|
||||
|
||||
interface Badge {
|
||||
text: string,
|
||||
classname: string
|
||||
}
|
||||
|
||||
const CLASSNAMES = {
|
||||
ROOT: "etabs",
|
||||
NAV: "nav",
|
||||
TABS: "tabs",
|
||||
TAB: "tab",
|
||||
BUTTONS: "buttons",
|
||||
VIEWS: "views",
|
||||
VIEW: "view"
|
||||
}
|
||||
|
||||
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 {
|
||||
buttonContainer: HTMLDivElement;
|
||||
isReady: boolean;
|
||||
newTabId: number;
|
||||
options: TabGroupOptions;
|
||||
shadow: ShadowRoot;
|
||||
tabContainer: HTMLDivElement;
|
||||
tabs: Array<Tab>;
|
||||
viewContainer: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.isReady = false;
|
||||
|
||||
// Options
|
||||
this.options = {
|
||||
closeButtonText: this.getAttribute("close-button-text") || "×",
|
||||
defaultTab: { title: "New Tab", active: true },
|
||||
newTabButton: !!this.getAttribute("new-tab-button") === true || false,
|
||||
newTabButtonText: this.getAttribute("new-tab-button-text") || "+",
|
||||
sortable: !!this.getAttribute("sortable") === true || false,
|
||||
visibilityThreshold: Number(this.getAttribute("visibility-threshold")) || 0
|
||||
};
|
||||
|
||||
this.tabs = [];
|
||||
this.newTabId = 0;
|
||||
|
||||
this.createComponent();
|
||||
this.initVisibility();
|
||||
if (this.options.sortable) {
|
||||
this.initSortable();
|
||||
}
|
||||
|
||||
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() {
|
||||
// Support custom styles
|
||||
const style = this.querySelector("style");
|
||||
if (style) {
|
||||
this.shadow.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
private createComponent() {
|
||||
const shadow = this.attachShadow({mode: "open"});
|
||||
this.shadow = shadow;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.setAttribute("class", CLASSNAMES.ROOT);
|
||||
|
||||
const tabgroup = document.createElement("nav");
|
||||
tabgroup.setAttribute("class", CLASSNAMES.NAV);
|
||||
wrapper.appendChild(tabgroup);
|
||||
|
||||
const tabContainer = document.createElement("div");
|
||||
tabContainer.setAttribute("class", CLASSNAMES.TABS);
|
||||
tabgroup.appendChild(tabContainer);
|
||||
this.tabContainer = tabContainer;
|
||||
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.setAttribute("class", CLASSNAMES.BUTTONS);
|
||||
tabgroup.appendChild(buttonContainer);
|
||||
this.buttonContainer = buttonContainer;
|
||||
|
||||
if (this.options.newTabButton) {
|
||||
const button = this.buttonContainer.appendChild(document.createElement("button"));
|
||||
button.innerHTML = this.options.newTabButtonText;
|
||||
button.addEventListener("click", this.addTab.bind(this, undefined), false);
|
||||
}
|
||||
|
||||
const viewContainer = document.createElement("div");
|
||||
viewContainer.setAttribute("class", CLASSNAMES.VIEWS);
|
||||
wrapper.appendChild(viewContainer);
|
||||
this.viewContainer = viewContainer;
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = styles;
|
||||
|
||||
shadow.appendChild(style);
|
||||
shadow.appendChild(wrapper);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
const createNewSortable = () => {
|
||||
const options = Object.assign({
|
||||
direction: "horizontal",
|
||||
animation: 150,
|
||||
swapThreshold: 0.20
|
||||
}, this.options.sortableOptions);
|
||||
new Sortable(this.tabContainer, options);
|
||||
};
|
||||
|
||||
if (Sortable) {
|
||||
createNewSortable();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", createNewSortable);
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultTab(tab: TabOptions) {
|
||||
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) {
|
||||
if (this.tabs[i].id === id) {
|
||||
return this.tabs[i];
|
||||
}
|
||||
}
|
||||
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) {
|
||||
this.emit("tab-removed", tab, this);
|
||||
}
|
||||
}
|
||||
|
||||
activateRecentTab() {
|
||||
if (this.tabs.length > 0) {
|
||||
this.tabs[0].activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Tab extends EventTarget {
|
||||
badge: Badge;
|
||||
closable: boolean;
|
||||
element: HTMLDivElement;
|
||||
icon: string;
|
||||
iconURL: string;
|
||||
id: number;
|
||||
isClosed: boolean;
|
||||
isReady: boolean;
|
||||
spans: { [key: string]: HTMLSpanElement };
|
||||
tabGroup: TabGroup;
|
||||
title: string;
|
||||
webview: HTMLElement;
|
||||
webviewAttributes: { [key: string]: any };
|
||||
|
||||
constructor(tabGroup: TabGroup, id: number, args: TabOptions) {
|
||||
super();
|
||||
this.badge = args.badge;
|
||||
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.spans = {};
|
||||
this.tabGroup = tabGroup;
|
||||
this.title = args.title;
|
||||
this.webviewAttributes = args.webviewAttributes || {};
|
||||
this.webviewAttributes.src = args.src;
|
||||
|
||||
this.initTab();
|
||||
this.initWebview();
|
||||
|
||||
if (args.visible !== false) {
|
||||
this.show();
|
||||
}
|
||||
if (typeof args.ready === "function") {
|
||||
args.ready(this);
|
||||
} else {
|
||||
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 });
|
||||
}
|
||||
|
||||
private initTab() {
|
||||
const tab = this.element = document.createElement("div");
|
||||
tab.classList.add(CLASSNAMES.TAB);
|
||||
for (let el of ["icon", "title", "badge", "close"]) {
|
||||
const span = tab.appendChild(document.createElement("span"));
|
||||
span.classList.add(`${CLASSNAMES.TAB}-${el}`);
|
||||
this.spans[el] = span;
|
||||
}
|
||||
|
||||
this.setTitle(this.title);
|
||||
this.setBadge(this.badge);
|
||||
this.setIcon(this.iconURL, this.icon);
|
||||
this.initTabCloseButton();
|
||||
this.initTabClickHandler();
|
||||
|
||||
this.tabGroup.tabContainer.appendChild(this.element);
|
||||
}
|
||||
|
||||
private initTabCloseButton() {
|
||||
const container = this.spans.close;
|
||||
if (this.closable) {
|
||||
const button = container.appendChild(document.createElement("button"));
|
||||
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.element.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.element.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(CLASSNAMES.VIEW);
|
||||
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.spans.title;
|
||||
span.innerHTML = title;
|
||||
span.title = title;
|
||||
this.title = title;
|
||||
this.emit("title-changed", title, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
if (this.isClosed) return;
|
||||
return this.title;
|
||||
}
|
||||
|
||||
setBadge(badge?: Badge) {
|
||||
if (this.isClosed) return;
|
||||
const span = this.spans.badge;
|
||||
this.badge = badge;
|
||||
|
||||
if (badge) {
|
||||
span.innerHTML = badge.text;
|
||||
span.classList.add(badge.classname);
|
||||
span.classList.remove("hidden");
|
||||
} else {
|
||||
span.classList.add("hidden");
|
||||
}
|
||||
|
||||
this.emit("badge-changed", badge, this);
|
||||
}
|
||||
|
||||
getBadge() {
|
||||
if (this.isClosed) return;
|
||||
return this.badge;
|
||||
}
|
||||
|
||||
setIcon(iconURL: string, icon: string) {
|
||||
if (this.isClosed) return;
|
||||
this.iconURL = iconURL;
|
||||
this.icon = icon;
|
||||
const span = this.spans.icon;
|
||||
if (iconURL) {
|
||||
span.innerHTML = `<img src="${iconURL}" />`;
|
||||
this.emit("icon-changed", iconURL, this);
|
||||
} else if (icon) {
|
||||
span.innerHTML = `<i class="${icon}"></i>`;
|
||||
this.emit("icon-changed", icon, this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
if (this.isClosed) return;
|
||||
if (this.iconURL) return this.iconURL;
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
setPosition(newPosition: number) {
|
||||
const tabContainer = this.tabGroup.tabContainer;
|
||||
const length = tabContainer.childElementCount;
|
||||
const thisPosition = this.getPosition();
|
||||
const tabs = Array.from(tabContainer.children)
|
||||
tabs.splice(thisPosition, 1);
|
||||
|
||||
if (newPosition < 0) {
|
||||
newPosition += length;
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPosition < length) {
|
||||
tabContainer.insertBefore(this.element, tabs[newPosition]);
|
||||
} else {
|
||||
tabContainer.appendChild(this.element);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getPosition(fromRight = false) {
|
||||
let position = 0;
|
||||
let tab = this.element;
|
||||
while ((tab = tab.previousSibling as HTMLDivElement) != null) position++;
|
||||
|
||||
if (fromRight === true) {
|
||||
position -= this.tabGroup.tabContainer.childElementCount;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
activate() {
|
||||
if (this.isClosed) return;
|
||||
const activeTab = this.tabGroup.getActiveTab();
|
||||
if (activeTab) {
|
||||
activeTab.element.classList.remove("active");
|
||||
activeTab.webview.classList.remove("visible");
|
||||
activeTab.emit("inactive", activeTab);
|
||||
}
|
||||
this.tabGroup.setActiveTab(this);
|
||||
this.element.classList.add("active");
|
||||
this.webview.classList.add("visible");
|
||||
this.webview.focus();
|
||||
this.emit("active", this);
|
||||
return this;
|
||||
}
|
||||
|
||||
show(flag = true) {
|
||||
if (this.isClosed) return;
|
||||
if (flag) {
|
||||
this.element.classList.add("visible");
|
||||
this.emit("visible", this);
|
||||
} else {
|
||||
this.element.classList.remove("visible");
|
||||
this.emit("hidden", this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hide() {
|
||||
return this.show(false);
|
||||
}
|
||||
|
||||
hasClass(classname: string) {
|
||||
return this.element.classList.contains(classname);
|
||||
}
|
||||
|
||||
close(force: boolean) {
|
||||
const abortController = new AbortController();
|
||||
const abort = () => abortController.abort();
|
||||
this.emit("closing", this, abort);
|
||||
|
||||
const abortSignal = abortController.signal;
|
||||
if (this.isClosed || (!this.closable && !force) || abortSignal.aborted) return;
|
||||
|
||||
this.isClosed = true;
|
||||
const tabGroup = this.tabGroup;
|
||||
tabGroup.tabContainer.removeChild(this.element);
|
||||
tabGroup.viewContainer.removeChild(this.webview);
|
||||
const activeTab = this.tabGroup.getActiveTab();
|
||||
tabGroup.removeTab(this, true);
|
||||
|
||||
this.emit("close", this);
|
||||
|
||||
if (activeTab.id === this.id) {
|
||||
tabGroup.activateRecentTab();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("tab-group", TabGroup);
|
||||
|
||||
export type { TabGroup, Tab };
|
208
src/style.css
Normal file
208
src/style.css
Normal file
|
@ -0,0 +1,208 @@
|
|||
/* CUSTOM PROPERTIES */
|
||||
/* ================= */
|
||||
|
||||
:host {
|
||||
--tabgroup-background: #E7EAED;
|
||||
--tab-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--tab-font-size: 13px;
|
||||
--tab-background: #E7EAED;
|
||||
--tab-color: #696A6C;
|
||||
--tab-border-color: #DADCE0;
|
||||
--tab-transition: background-color 200ms ease-out, color 200ms ease-out;
|
||||
--tab-cursor: pointer;
|
||||
--tab-active-color: currentcolor;
|
||||
--tab-active-background: #FFF;
|
||||
--tag-hover-color: currentcolor;
|
||||
--tag-hover-background: #F1F3F4;
|
||||
--button-font-size: 15px;
|
||||
--button-background: none;
|
||||
--button-color: #696A6C;
|
||||
--button-hover-background: #DADCE0;
|
||||
--button-hover-color: #383a3e;
|
||||
--button-border-radius: 50%;
|
||||
--button-cursor: pointer;
|
||||
--badge-background: #383a3e;
|
||||
--badge-color: #fff;
|
||||
--close-button-visibility: visible;
|
||||
}
|
||||
|
||||
/* STYLES */
|
||||
/* ====== */
|
||||
|
||||
webview {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
webview.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.etabs {
|
||||
font-family: var(--tab-font-family);
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: 'liga', 'clig', 'kern';
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: var(--tabgroup-background);
|
||||
box-shadow: inset 0 -1px var(--tab-border-color);
|
||||
border-top: 1px solid var(--tab-border-color);
|
||||
font-size: var(--tab-font-size);
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nav.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: var(--tab-background);
|
||||
box-shadow: inset 0 -1px var(--tab-border-color);
|
||||
color: var(--tab-color);
|
||||
cursor: var(--tab-cursor);
|
||||
font-size: var(--tab-font-size);
|
||||
transition: var(--tab-transition);
|
||||
display: none;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 5px 9px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.tab.visible {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--tab-active-color);
|
||||
background: var(--tab-active-background);
|
||||
border-left: 1px solid var(--tab-border-color);
|
||||
border-right: 1px solid var(--tab-border-color);
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tab.active:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.tab.visible:not(.active)+.tab.visible:not(.active) {
|
||||
border-left: 1px solid var(--tab-border-color);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.tab:not(.active):hover {
|
||||
background: var(--tab-hover-background);
|
||||
color: var(--tab-hover-color);
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
background: var(--badge-background);
|
||||
color: var(--badge-color);
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
margin-left: 5px;
|
||||
padding: 1px 4px;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tab-badge.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tab-icon img {
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tab-close button {
|
||||
background: var(--button-background);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-color);
|
||||
cursor: var(--button-cursor);
|
||||
font-size: var(--button-font-size);
|
||||
display: inline-block;
|
||||
border: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
padding: 1px 0 0 0;
|
||||
visibility: var(--close-button-visibility);
|
||||
}
|
||||
|
||||
.tab.active .tab-close button {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tab-close button:hover {
|
||||
color: var(--button-hover-color);
|
||||
background: var(--button-hover-background);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
border-left: 1px solid var(--tab-border-color);
|
||||
}
|
||||
|
||||
.buttons button {
|
||||
color: var(--button-color);
|
||||
background: var(--button-background);
|
||||
border-radius: var(--button-border-radius);
|
||||
cursor: var(--button-cursor);
|
||||
font-size: var(--button-font-size);
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
margin: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
padding: 1px 0 0 0;
|
||||
}
|
||||
|
||||
.buttons button:hover {
|
||||
color: var(--button-hover-color);
|
||||
background: var(--button-hover-background);
|
||||
}
|
||||
|
||||
.views {
|
||||
position: relative;
|
||||
height: calc(100vh - 33px);
|
||||
}
|
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"noImplicitAny": true,
|
||||
"noUnusedParameters": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue