mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-27 01:41:44 +02:00
Added plugin context view
- Added plugin context view - Moved plugin type definition to separate file - Added wip request forwarder
This commit is contained in:
26
src/mod/plugins/forwarder.go
Normal file
26
src/mod/plugins/forwarder.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Forwarder.go
|
||||||
|
|
||||||
|
This file handles the dynamic proxy routing forwarding
|
||||||
|
request to plugin capture path that handles the matching
|
||||||
|
request path registered when the plugin started
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (m *Manager) GetHandlerPlugins(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetHandlerPluginsSubsets(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) HandlePluginRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//Find the plugin that matches the request path
|
||||||
|
//If no plugin found, return 404
|
||||||
|
//If found, forward the request to the plugin
|
||||||
|
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
@ -18,6 +19,11 @@ func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Sort the plugin by its name
|
||||||
|
sort.Slice(plugins, func(i, j int) bool {
|
||||||
|
return plugins[i].Spec.Name < plugins[j].Spec.Name
|
||||||
|
})
|
||||||
|
|
||||||
js, err := json.Marshal(plugins)
|
js, err := json.Marshal(plugins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -1,49 +1,23 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zoraxy Plugin Manager
|
||||||
|
|
||||||
|
This module is responsible for managing plugins
|
||||||
|
loading plugins from the disk
|
||||||
|
enable / disable plugins
|
||||||
|
and forwarding traffic to plugins
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
|
||||||
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Plugin struct {
|
|
||||||
RootDir string //The root directory of the plugin
|
|
||||||
Spec *zoraxyPlugin.IntroSpect //The plugin specification
|
|
||||||
Enabled bool //Whether the plugin is enabled
|
|
||||||
|
|
||||||
//Runtime
|
|
||||||
AssignedPort int //The assigned port for the plugin
|
|
||||||
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
|
|
||||||
process *exec.Cmd //The process of the plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
type ManagerOptions struct {
|
|
||||||
PluginDir string
|
|
||||||
SystemConst *zoraxyPlugin.RuntimeConstantValue
|
|
||||||
Database *database.Database
|
|
||||||
Logger *logger.Logger
|
|
||||||
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
LoadedPlugins sync.Map //Storing *Plugin
|
|
||||||
Options *ManagerOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed no_img.png
|
|
||||||
var noImg []byte
|
|
||||||
|
|
||||||
// NewPluginManager creates a new plugin manager
|
// NewPluginManager creates a new plugin manager
|
||||||
func NewPluginManager(options *ManagerOptions) *Manager {
|
func NewPluginManager(options *ManagerOptions) *Manager {
|
||||||
//Create plugin directory if not exists
|
//Create plugin directory if not exists
|
||||||
|
40
src/mod/plugins/typdef.go
Normal file
40
src/mod/plugins/typdef.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed no_img.png
|
||||||
|
var noImg []byte
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
RootDir string //The root directory of the plugin
|
||||||
|
Spec *zoraxyPlugin.IntroSpect //The plugin specification
|
||||||
|
Enabled bool //Whether the plugin is enabled
|
||||||
|
|
||||||
|
//Runtime
|
||||||
|
AssignedPort int //The assigned port for the plugin
|
||||||
|
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
|
||||||
|
process *exec.Cmd //The process of the plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagerOptions struct {
|
||||||
|
PluginDir string
|
||||||
|
SystemConst *zoraxyPlugin.RuntimeConstantValue
|
||||||
|
Database *database.Database
|
||||||
|
Logger *logger.Logger
|
||||||
|
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
LoadedPlugins sync.Map //Storing *Plugin
|
||||||
|
Options *ManagerOptions
|
||||||
|
}
|
@ -79,9 +79,8 @@ type IntroSpect struct {
|
|||||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||||
This captures the whole traffic of Zoraxy
|
This captures the whole traffic of Zoraxy
|
||||||
|
|
||||||
Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule
|
|
||||||
*/
|
*/
|
||||||
GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -90,20 +89,9 @@ type IntroSpect struct {
|
|||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||||
these always applies
|
these always applies
|
||||||
*/
|
*/
|
||||||
AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler)
|
||||||
|
|
||||||
/*
|
|
||||||
Dynamic Capture Settings
|
|
||||||
|
|
||||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
|
||||||
the plugin can capture the request and decided if the request
|
|
||||||
shall be handled by itself or let it pass through
|
|
||||||
|
|
||||||
*/
|
|
||||||
DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture)
|
|
||||||
DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler)
|
|
||||||
|
|
||||||
/* UI Path for your plugin */
|
/* UI Path for your plugin */
|
||||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
<h2>Global Area Network</h2>
|
<h2>Global Area Network</h2>
|
||||||
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
|
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui yellow message">
|
||||||
|
<b>Deprecation Notice</b>
|
||||||
|
<p>Global Area Network will be deprecating in v3.2.x and moved to Plugin</p>
|
||||||
|
</div>
|
||||||
<div class="gansnetworks">
|
<div class="gansnetworks">
|
||||||
<div class="ganstats ui basic segment">
|
<div class="ganstats ui basic segment">
|
||||||
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
|
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
|
||||||
|
52
src/web/components/plugincontext.html
Normal file
52
src/web/components/plugincontext.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<div class="">
|
||||||
|
<iframe id="pluginContextLoader" src="" style="width: 100%; border: none;">
|
||||||
|
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function initPluginUIView(){
|
||||||
|
let pluginID = getPluginIDFromWindowHash();
|
||||||
|
console.log("Launching plugin UI for plugin with ID:", pluginID);
|
||||||
|
loadPluginContext(pluginID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPluginContext(pluginID){
|
||||||
|
let pluginContextURL = `/plugin.ui/${pluginID}/`;
|
||||||
|
$("#pluginContextLoader").attr("src", pluginContextURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginIDFromWindowHash(){
|
||||||
|
let tabID = window.location.hash.substr(1);
|
||||||
|
let pluginID = "";
|
||||||
|
if (tabID.startsWith("{")) {
|
||||||
|
tabID = decodeURIComponent(tabID);
|
||||||
|
try {
|
||||||
|
let parsedData = JSON.parse(tabID);
|
||||||
|
if (typeof(parsedData.pluginID) != "undefined"){
|
||||||
|
pluginID = parsedData.pluginID;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid JSON data:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pluginID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeIframe() {
|
||||||
|
let iframe = document.getElementById('pluginContextLoader');
|
||||||
|
let mainMenuHeight = document.getElementById('mainmenu').offsetHeight;
|
||||||
|
iframe.style.height = mainMenuHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).on("resize", function(){
|
||||||
|
resizeIframe();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Bind event to tab switch
|
||||||
|
tabSwitchEventBind["pluginContextWindow"] = function(){
|
||||||
|
//On switch over to this page, load info
|
||||||
|
resizeIframe();
|
||||||
|
}
|
||||||
|
|
||||||
|
initPluginUIView();
|
||||||
|
</script>
|
@ -1,7 +1,11 @@
|
|||||||
<div class="standardContainer">
|
<div class="standardContainer">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h2>Plugins</h2>
|
<h2>Plugins</h2>
|
||||||
<p>Custom features on Zoraxy</p>
|
<p>Add custom features to your Zoraxy!</p>
|
||||||
|
</div>
|
||||||
|
<div class="ui yellow message">
|
||||||
|
<div class="header">Experimental Feature</div>
|
||||||
|
<p>This feature is experimental and may not work as expected. Use with caution.</p>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui basic celled table">
|
<table class="ui basic celled table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -19,6 +23,29 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
function initPluginSideMenu(){
|
||||||
|
$("#pluginMenu").html("");
|
||||||
|
$.get(`/api/plugins/list`, function(data){
|
||||||
|
data.forEach(plugin => {
|
||||||
|
$("#pluginMenu").append(`
|
||||||
|
<a class="item" tag="pluginContextWindow" pluginid="${plugin.Spec.id}" onclick="loadPluginUIContextIfAvailable();">
|
||||||
|
<img style="width: 20px;" class="ui mini right spaced image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}"> ${plugin.Spec.name}
|
||||||
|
</a>
|
||||||
|
`);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initPluginSideMenu();
|
||||||
|
|
||||||
|
function loadPluginUIContextIfAvailable(){
|
||||||
|
if(typeof(initPluginUIView) != "undefined"){
|
||||||
|
initPluginUIView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function initiatePluginList(){
|
function initiatePluginList(){
|
||||||
$.get(`/api/plugins/list`, function(data){
|
$.get(`/api/plugins/list`, function(data){
|
||||||
const tbody = $("#pluginTable");
|
const tbody = $("#pluginTable");
|
||||||
@ -49,7 +76,6 @@ function initiatePluginList(){
|
|||||||
<div class="ui toggle checkbox">
|
<div class="ui toggle checkbox">
|
||||||
<input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}>
|
<input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic small circular icon button" onclick="openPluginUI('${plugin.Spec.id}');"><i class="ui edit icon"></i></button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@ -59,10 +85,6 @@ function initiatePluginList(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPluginUI(pluginid){
|
|
||||||
showSideWrapper(`/plugin.ui/${pluginid}/`, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
initiatePluginList();
|
initiatePluginList();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -78,9 +78,6 @@
|
|||||||
<i class="simplistic user circle icon"></i> SSO / Oauth
|
<i class="simplistic user circle icon"></i> SSO / Oauth
|
||||||
</a>
|
</a>
|
||||||
<div class="ui divider menudivider">Others</div>
|
<div class="ui divider menudivider">Others</div>
|
||||||
<a class="item" tag="plugins">
|
|
||||||
<i class="simplistic puzzle piece icon"></i> Plugins
|
|
||||||
</a>
|
|
||||||
<a class="item" tag="webserv">
|
<a class="item" tag="webserv">
|
||||||
<i class="simplistic globe icon"></i> Static Web Server
|
<i class="simplistic globe icon"></i> Static Web Server
|
||||||
</a>
|
</a>
|
||||||
@ -96,6 +93,15 @@
|
|||||||
<a class="item" tag="utils">
|
<a class="item" tag="utils">
|
||||||
<i class="simplistic paperclip icon"></i> Utilities
|
<i class="simplistic paperclip icon"></i> Utilities
|
||||||
</a>
|
</a>
|
||||||
|
<a class="item" tag="plugins">
|
||||||
|
<i class="simplistic puzzle piece icon"></i> Plugins Manager
|
||||||
|
</a>
|
||||||
|
<div class="ui divider menudivider">Plugins</div>
|
||||||
|
<cx id="pluginMenu"></container>
|
||||||
|
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
|
||||||
|
<i class="green circle check icon"></i> No Installed Plugins
|
||||||
|
</a>
|
||||||
|
</cx>
|
||||||
<!-- Add more components here -->
|
<!-- Add more components here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -155,6 +161,12 @@
|
|||||||
|
|
||||||
<!-- Utilities -->
|
<!-- Utilities -->
|
||||||
<div id="utils" class="functiontab" target="utils.html"></div>
|
<div id="utils" class="functiontab" target="utils.html"></div>
|
||||||
|
|
||||||
|
<!-- Plugin Context Menu -->
|
||||||
|
<div id="pluginContextWindow" class="functiontab" target="plugincontext.html"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -246,7 +258,26 @@
|
|||||||
|
|
||||||
if (window.location.hash.length > 1){
|
if (window.location.hash.length > 1){
|
||||||
let tabID = window.location.hash.substr(1);
|
let tabID = window.location.hash.substr(1);
|
||||||
openTabById(tabID);
|
if (tabID.startsWith("{")) {
|
||||||
|
tabID = decodeURIComponent(tabID);
|
||||||
|
//Zoraxy v3.2.x plugin context window
|
||||||
|
try {
|
||||||
|
let parsedData = JSON.parse(tabID);
|
||||||
|
tabID = parsedData.tabID;
|
||||||
|
|
||||||
|
//Open the plugin context window
|
||||||
|
if (tabID == "pluginContextWindow"){
|
||||||
|
let pluginID = parsedData.pluginID;
|
||||||
|
let button = $("#pluginMenu").find(`[pluginid="${pluginID}"]`);
|
||||||
|
openTabById(tabID, button);
|
||||||
|
loadPluginUIContextIfAvailable();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid JSON data:", e);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
openTabById(tabID);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
openTabById("status");
|
openTabById("status");
|
||||||
}
|
}
|
||||||
@ -257,7 +288,7 @@
|
|||||||
$("#mainmenu").find(".item").each(function(){
|
$("#mainmenu").find(".item").each(function(){
|
||||||
$(this).on("click", function(event){
|
$(this).on("click", function(event){
|
||||||
let tabid = $(this).attr("tag");
|
let tabid = $(this).attr("tag");
|
||||||
openTabById(tabid);
|
openTabById(tabid, $(this));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -282,13 +313,19 @@
|
|||||||
if ($(".sideWrapper").is(":visible")){
|
if ($(".sideWrapper").is(":visible")){
|
||||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
|
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
setDarkTheme(true);
|
setDarkTheme(true);
|
||||||
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
||||||
if ($(".sideWrapper").is(":visible")){
|
if ($(".sideWrapper").is(":visible")){
|
||||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||||
}
|
}
|
||||||
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
|
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,8 +344,12 @@
|
|||||||
|
|
||||||
//Select and open a tab by its tag id
|
//Select and open a tab by its tag id
|
||||||
let tabSwitchEventBind = {}; //Bind event to tab switch by tabid
|
let tabSwitchEventBind = {}; //Bind event to tab switch by tabid
|
||||||
function openTabById(tabID){
|
function openTabById(tabID, object=undefined){
|
||||||
let targetBtn = getTabButtonById(tabID);
|
let targetBtn = object;
|
||||||
|
if (object == undefined){
|
||||||
|
//Search tab by its tap id
|
||||||
|
targetBtn = getTabButtonById(tabID);
|
||||||
|
}
|
||||||
if (targetBtn == undefined){
|
if (targetBtn == undefined){
|
||||||
alert("Invalid tabid given");
|
alert("Invalid tabid given");
|
||||||
return;
|
return;
|
||||||
@ -329,7 +370,19 @@
|
|||||||
},100)
|
},100)
|
||||||
});
|
});
|
||||||
$('html,body').animate({scrollTop: 0}, 'fast');
|
$('html,body').animate({scrollTop: 0}, 'fast');
|
||||||
window.location.hash = tabID;
|
|
||||||
|
if (tabID == "pluginContextWindow"){
|
||||||
|
let statePayload = {
|
||||||
|
tabID: tabID,
|
||||||
|
pluginID: $(targetBtn).attr("pluginid")
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.hash = JSON.stringify(statePayload);
|
||||||
|
loadPluginUIContextIfAvailable();
|
||||||
|
}else{
|
||||||
|
window.location.hash = tabID;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(window).on("resize", function(){
|
$(window).on("resize", function(){
|
||||||
|
Reference in New Issue
Block a user