+
+ ×
+
+
+ Save File
+
+
+ Choose how to save your file:
+
+
+
+
+ Save locally
+
+
Cancel
@@ -238,7 +284,7 @@
Open File
// Dispatch pending loads
for (var i = 0; i < q.length; i = (i + 1) | 0) q[i].send();
};
- ret.src = "/c.html";
+ ret.src = "c.html";
ret.width = "0px";
ret.height = "0px";
ret.style.border = "0px";
@@ -251,24 +297,35 @@ Open File
document.body.appendChild(IFrameProxyDownloader.iframe);
});
IFrameProxyDownloader.intervalId = setInterval(function() {
- IFrameProxyDownloader.iframe.src = "/c.html";
+ IFrameProxyDownloader.iframe.src = "c.html";
}, 10000);
};
document.addEventListener(
"DOMContentLoaded",
function() {
+ // Register service worker for both standalone and iframe contexts
registerServiceWorker();
},
false
);
- document.addEventListener(
- "DOMContentLoaded",
- function() {
+
+ // Start ImageJ with a robust approach
+ function startImageJWhenReady() {
+ if (typeof startImageJ === 'function') {
startImageJ();
- },
- false
- );
+ } else {
+ // If startImageJ is not ready, try again in 100ms
+ setTimeout(startImageJWhenReady, 100);
+ }
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener("DOMContentLoaded", startImageJWhenReady, false);
+ } else {
+ // DOM is already loaded (common in iframes)
+ startImageJWhenReady();
+ }
+
diff --git a/src/index.js b/src/index.js
index d0016bc..551571b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -334,19 +334,52 @@ window.openFileDialogJS = async (title, initPath, selectionMode, promise) => {
cjCall(promise, "resolve", "");
}
fileDialog.hide();
- };
+ };
};
+// window.saveFileDialogJS = async (title, initPath, selectionMode, promise) => {
+// // by pass the selection
+// const savePath = prompt(title || "Saving file as ", initPath);
+// if (savePath) {
+// downloadQueue[savePath] = 1;
+// loader.style.display = "block";
+// await cjCall(promise, "resolve", "/files/" + savePath);
+// } else {
+// await cjCall(promise, "reject", "cancelled");
+// }
+// };
+
+// Get the save dialog element
+const saveEl = document.getElementById("save-file-dialog");
+const saveDialog = new A11yDialog(saveEl);
+
window.saveFileDialogJS = async (title, initPath, selectionMode, promise) => {
- // by pass the selection
- const savePath = prompt(title || "Saving file as ", initPath);
- if (savePath) {
- downloadQueue[savePath] = 1;
- loader.style.display = "block";
- await cjCall(promise, "resolve", "/files/" + savePath);
- } else {
- await cjCall(promise, "reject", "cancelled");
- }
+ const fmt = title.split("Save as ")[1].toLowerCase();
+ document.getElementById("save-dialog-title").innerHTML = "Save File";
+ saveDialog.show();
+ let closed = false;
+
+ // Handle dialog close (both overlay click and close button)
+ saveDialog.on("hide", function(dialogEl, event) {
+ if (!closed) {
+ closed = true;
+ cjCall(promise, "reject", "cancelled");
+ }
+ });
+
+ // Download to computer option
+ document.getElementById("save-file-download").onclick = async () => {
+ if (!closed) {
+ const savePath = prompt("Enter filename:", initPath || "image.png");
+ if (savePath) {
+ downloadQueue[savePath] = 1;
+ loader.style.display = "block";
+ closed = true;
+ saveDialog.hide();
+ await cjCall(promise, "resolve", "/files/" + savePath);
+ }
+ }
+ };
};
window.onFileOpened = (path, error) => {
@@ -1123,22 +1156,48 @@ window.registerServiceWorker = function() {
window.addEventListener("load", function() {
navigator.serviceWorker.register("/service-worker.js").then(
function(registration) {
- // Registration was successful
- console.log(
- "ServiceWorker registration successful with scope: ",
- registration.scope
- );
-
- const currentVersion = localStorage.getItem("service-worker-version");
- if (!currentVersion || currentVersion != version) {
- console.log("Upgrading ServiceWorker to " + version);
- registration.update();
- localStorage.setItem("service-worker-version", version);
+ console.log("[ServiceWorker] Registration successful:", registration.scope);
+
+ // Check for updates periodically (every page load)
+ registration.update();
+
+ // Handle new service worker waiting
+ if (registration.waiting) {
+ // There's a new service worker waiting, prompt user or auto-update
+ console.log("[ServiceWorker] New version waiting, activating...");
+ registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
+
+ // Handle future updates
+ registration.addEventListener('updatefound', function() {
+ const newWorker = registration.installing;
+ console.log("[ServiceWorker] Update found, installing...");
+
+ newWorker.addEventListener('statechange', function() {
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+ // New service worker installed, activate it
+ newWorker.postMessage({ type: 'SKIP_WAITING' });
+
+ // Optional: Show a notification to the user
+ console.log("[ServiceWorker] New version available! Page will refresh...");
+
+ // Auto-refresh when new service worker takes control
+ let refreshing = false;
+ navigator.serviceWorker.addEventListener('controllerchange', function() {
+ if (!refreshing) {
+ refreshing = true;
+ window.location.reload();
+ }
+ });
+ }
+ });
+ });
+
+ // Store version for tracking
+ localStorage.setItem("service-worker-version", version);
},
function(err) {
- // registration failed :(
- console.log("ServiceWorker registration failed: ", err);
+ console.error("[ServiceWorker] Registration failed:", err);
}
);
});
@@ -1692,16 +1751,12 @@ window.onImageJInitialized = async () => {
addMenuItem
);
}
- // if inside an iframe, setup ImJoy
- if (window.self !== window.top) {
- setAPI(null);
- } else {
- await setupImJoyApp(setAPI);
- const titleBar = document.querySelector(".titleBar");
- const elem = document.getElementById("imjoy-menu");
- window._imjoy_menu_element = elem;
- titleBar.parentNode.insertBefore(elem, titleBar.nextSibling);
- }
+ // Always setup the ImJoy App UI (show menu/plugins) regardless of iframe
+ await setupImJoyApp(setAPI);
+ const titleBar = document.querySelector(".titleBar");
+ const elem = document.getElementById("imjoy-menu");
+ window._imjoy_menu_element = elem;
+ titleBar.parentNode.insertBefore(elem, titleBar.nextSibling);
processUrlParameters(imagej);
diff --git a/src/service-worker.js b/src/service-worker.js
index 07472af..b164432 100644
--- a/src/service-worker.js
+++ b/src/service-worker.js
@@ -1,901 +1,187 @@
-(function e(t, n, r) {
- function s(o, u) {
- if (!n[o]) {
- if (!t[o]) {
- var a = typeof require == "function" && require;
- if (!u && a) return a(o, !0);
- if (i) return i(o, !0);
- var f = new Error("Cannot find module '" + o + "'");
- throw ((f.code = "MODULE_NOT_FOUND"), f);
- }
- var l = (n[o] = { exports: {} });
- t[o][0].call(
- l.exports,
- function(e) {
- var n = t[o][1][e];
- return s(n ? n : e);
- },
- l,
- l.exports,
- e,
- t,
- n,
- r
+/**
+ * ImageJ.js Service Worker
+ * Version: 0.6.0
+ *
+ * Simple, upgrade-friendly service worker that:
+ * - Caches essential static assets for performance
+ * - Always fetches HTML fresh (network-first)
+ * - Automatically cleans up old caches
+ * - Works the same in both standalone and iframe contexts
+ */
+
+const CACHE_VERSION = 'imagej-v0.6.0';
+const PREVIOUS_CACHES = [
+ 'imagej-v0.5.9',
+ 'imagej-v0.5.8',
+ 'offline' // Old cache name
+];
+
+// Assets that are safe to cache and rarely change
+const STATIC_ASSETS = [
+ '/style.css',
+ '/assets/img/imagej-js-splash.jpg',
+ '/assets/img/imagej-js-loading.gif',
+ '/assets/icons/chrome/chrome-favicon-16-16.png',
+ '/manifest.webmanifest'
+];
+
+// File types that are safe to cache
+const CACHEABLE_TYPES = [
+ '.css',
+ '.js',
+ '.jpg',
+ '.jpeg',
+ '.png',
+ '.gif',
+ '.svg',
+ '.woff',
+ '.woff2',
+ '.ttf'
+];
+
+// Install event - cache essential assets
+self.addEventListener('install', function(event) {
+ console.log('[ServiceWorker] Installing version:', CACHE_VERSION);
+
+ event.waitUntil(
+ caches.open(CACHE_VERSION).then(function(cache) {
+ console.log('[ServiceWorker] Caching essential assets');
+ // Only cache the most essential assets on install
+ return cache.addAll(STATIC_ASSETS).catch(function(err) {
+ console.warn('[ServiceWorker] Failed to cache some assets:', err);
+ // Don't fail installation if some assets can't be cached
+ return Promise.resolve();
+ });
+ }).then(function() {
+ // Skip waiting to become active immediately
+ return self.skipWaiting();
+ })
+ );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', function(event) {
+ console.log('[ServiceWorker] Activating version:', CACHE_VERSION);
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNames) {
+ return Promise.all(
+ cacheNames.map(function(cacheName) {
+ // Delete old caches and any cache not matching current version
+ if (cacheName !== CACHE_VERSION) {
+ console.log('[ServiceWorker] Deleting old cache:', cacheName);
+ return caches.delete(cacheName);
+ }
+ })
);
- }
- return n[o].exports;
+ }).then(function() {
+ // Take control of all pages immediately
+ return self.clients.claim();
+ })
+ );
+});
+
+// Fetch event - smart caching strategy
+self.addEventListener('fetch', function(event) {
+ const request = event.request;
+ const url = new URL(request.url);
+
+ // Skip non-GET requests
+ if (request.method !== 'GET') {
+ return;
}
- var i = typeof require == "function" && require;
- for (var o = 0; o < r.length; o++) s(r[o]);
- return s;
-})(
- {
- 1: [
- function(require, module, exports) {
- var sww = require("./lib/sww.js");
-
- self.ServiceWorkerWare = sww.ServiceWorkerWare;
- self.StaticCacher = sww.StaticCacher;
- self.SimpleOfflineCache = sww.SimpleOfflineCache;
- },
- { "./lib/sww.js": 5 }
- ],
- 2: [
- function(require, module, exports) {
- "use strict";
-
- // Inspired by expressjs and shed (https://github.com/wibblymat/shed)
- function Router(options) {
- this.options = options;
- this.stack = [];
- }
-
- Router.prototype.ALL_METHODS = "all";
- Router.prototype.methods = [
- "get",
- "post",
- "put",
- "delete",
- "head",
- Router.prototype.ALL_METHODS
- ];
-
- /**
- * Add a new route to the stack.
- * @param method (String) http verb to handle the request
- * @param path (Regexp) string or regexp to match urls
- * @param handler (Function) payload to be executed if url matches.
- */
- Router.prototype.add = function r_add(method, path, handler) {
- var pathRegexAndTags, pathRegex, namedPlaceholders;
-
- method = this._sanitizeMethod(method);
-
- // Parse simle string path into regular expression for path matching
- pathRegexAndTags = this._parseSimplePath(path);
- pathRegex = pathRegexAndTags.regexp;
- namedPlaceholders = pathRegexAndTags.tags;
-
- this.stack.push({
- method: method,
- path: pathRegex,
- namedPlaceholders: namedPlaceholders,
- handler: handler
- });
- };
-
- /**
- * Create the utility methods .get .post ... etc.
- */
- Router.prototype.methods.forEach(function(method) {
- Router.prototype[method] = function(path, handler) {
- return this.add(method, path, handler);
- };
- });
-
- Router.prototype.proxyMethods = function r_proxyPrototype(obj) {
- var self = this;
- this.methods.forEach(function(method) {
- obj[method] = function(path, mw) {
- if (
- typeof mw.onFetch !== "function" &&
- typeof mw !== "function"
- ) {
- throw new Error("This middleware cannot handle fetch request");
- }
- var handler =
- typeof mw.onFetch !== "undefined" ? mw.onFetch.bind(mw) : mw;
- self.add(method, path, handler);
- };
- });
- };
-
- /**
- * Matches the given url and methods with the routes stored in
- * the stack.
- */
- Router.prototype.match = function r_match(method, url) {
- method = this._sanitizeMethod(method);
- var matches = [];
-
- var _this = this;
- this.stack.forEach(function eachRoute(route) {
- if (
- !(method === route.method || route.method === _this.ALL_METHODS)
- ) {
- return;
- }
-
- var groups = _this._routeMatch(url, route);
- if (groups) {
- route.handler.__params = groups;
- matches.push(route.handler);
- }
- });
-
- return matches;
- };
-
- /**
- * Performs a matching test for url against a route.
- * @param {String} the url to test
- * @param {route} the route to match against
- * @return {Object} an object with the portions of the url matching the named
- * placeholders or null if there is no match.
- */
- Router.prototype._routeMatch = function(url, route) {
- var groups = url.match(route.path);
- if (!groups) {
- return null;
- }
- return this._mapParameters(groups, route.namedPlaceholders);
- };
-
- /**
- * Assign names from named placeholders in a route to the matching groups
- * for an URL against that route.
- * @param {Array} groups from a successful match
- * @param {Array} names for those groups
- * @return a map of names of named placeholders and values for those matches.
- */
- Router.prototype._mapParameters = function(groups, placeholderNames) {
- return placeholderNames.reduce(function(params, name, index) {
- params[name] = groups[index + 1];
- return params;
- }, Object.create(null));
- };
-
- Router.prototype._sanitizeMethod = function(method) {
- var sanitizedMethod = method.toLowerCase().trim();
- if (this.methods.indexOf(sanitizedMethod) === -1) {
- throw new Error('Method "' + method + '" is not supported');
- }
- return sanitizedMethod;
- };
-
- /**
- * Simple path-to-regex translation based on the Express "string-based path"
- * syntax.
- */
- Router.prototype._parseSimplePath = function(path) {
- // Check for named placeholder crowding
- if (/\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+/g.test(path)) {
- throw new Error("Invalid usage of named placeholders");
- }
-
- // Check for mixed placeholder crowdings
- var mixedPlaceHolders = /(\*\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\*)/g;
- if (mixedPlaceHolders.test(path.replace(/\\\*/g, ""))) {
- throw new Error("Invalid usage of named placeholders");
- }
-
- // Try parsing the string and converting special characters into regex
- try {
- // Parsing anonymous placeholders with simple backslash-escapes
- path = path.replace(/(.|^)[*]+/g, function(m, escape) {
- return escape === "\\" ? "\\*" : escape + "(?:.*?)";
- });
-
- // Parsing named placeholders with backslash-escapes
- var tags = [];
- path = path.replace(/(.|^)\:([a-zA-Z0-9]+)/g, function(
- m,
- escape,
- tag
- ) {
- if (escape === "\\") {
- return ":" + tag;
- }
- tags.push(tag);
- return escape + "(.+?)";
- });
-
- return { regexp: RegExp(path + "$"), tags: tags };
- } catch (ex) {
- // Failed to parse final path as a RegExp
- throw new Error("Invalid path specified");
- }
- };
-
- module.exports = Router;
- },
- {}
- ],
- 3: [
- function(require, module, exports) {
- /* global Promise, caches */
- "use strict";
-
- // Default Match options, not exposed.
- var DEFAULT_MATCH_OPTIONS = {
- ignoreSearch: false,
- ignoreMethod: false,
- ignoreVary: false
- };
- var DEFAULT_MISS_POLICY = "fetch";
- // List of different policies
- var MISS_POLICIES = [DEFAULT_MISS_POLICY];
-
- var DEFAULT_CACHE_NAME = "offline";
-
- /**
- * Constructor for the middleware that serves the content of a
- * cache specified by it's name.
- * @param {string} cacheName Name of the cache that will be serving the content
- * @param {object} [options] Object use to setup the cache matching alternatives
- * @param {string} [missPolicy] Name of the policy to follow if a request miss
- * when hitting the cache.
- */
- function SimpleOfflineCache(cacheName, options, missPolicy) {
- this.cacheName = cacheName || DEFAULT_CACHE_NAME;
- this.options = options || DEFAULT_MATCH_OPTIONS;
- this.missPolicy = missPolicy || DEFAULT_MISS_POLICY;
- if (MISS_POLICIES.indexOf(this.missPolicy) === -1) {
- console.warn("Policy " + missPolicy + " not supported");
- this.missPolicy = DEFAULT_MISS_POLICY;
- }
- }
- const swScriptUrl = new URL(self.location).href;
- const tmp = swScriptUrl.split("/");
- const baseURL = tmp.slice(0, tmp.length - 1).join("/");
- SimpleOfflineCache.prototype.onFetch = function soc_onFetch(
- request,
- response
- ) {
- // If another middleware layer already have a response, the simple cache
- // just pass through the response and does nothing.
- if (response) {
- return Promise.resolve(response);
- }
-
- var _this = this;
-
- // Enable cache only for this site and leaningtech
- if (
- !request.url.startsWith(baseURL) &&
- !request.url.startsWith("https://cjrtnc.leaningtech.com/")
- ) {
- return fetch(request);
- }
- // since we cannot just cache the HEAD requests
- // here we make a copy of it and change it to a GET request
- if (request.method === "HEAD") {
- return _this.ensureHeadCache().then(function(cache) {
- const opt = Object.assign({}, _this.options);
- {
- opt.ignoreMethod = true;
- }
- return cache.match(request, opt).then(function(res) {
- if (res && res.ok) {
- return res;
- } else {
- return _this
- .ensureCache()
- .then(function(cache) {
- return cache.match(request, _this.options);
- })
- .then(function(res) {
- if (res) {
- return res;
- } else {
- return _this.fetchAndCache(
- modifyRequest(request),
- cache
- );
- }
- });
- }
- });
- });
- } else {
- return _this.ensureCache().then(function(cache) {
- return cache.match(request, _this.options).then(function(res) {
- if (res && res.ok) {
- // try to fetch anyway
- if (!request.url.startsWith(baseURL)) {
- _this.fetchAndCache(request, cache);
- }
- return res;
- }
- // So far we just support one policy
- return _this.fetchAndCache(request, cache);
- });
- });
- }
- };
-
- function modifyRequest(urlOrReq) {
- // make sure we deal with a Request object even if we got a URL string
- const req =
- urlOrReq instanceof Request ? urlOrReq : new Request(urlOrReq);
-
- const {
- cache,
- credentials,
- headers,
- integrity,
- method,
- mode,
- redirect,
- referrer,
- referrerPolicy,
- url,
- body
- } = req;
-
- let met = method;
- if (method === "HEAD") met = "GET";
-
- return new Request(url, {
- cache,
- credentials,
- headers,
- integrity,
- method: met,
- mode,
- redirect,
- referrer,
- referrerPolicy,
- body
- });
- }
-
- SimpleOfflineCache.prototype.fetchAndCache = function sc_fetchAndCache(
- request,
- cache
- ) {
- return fetch(request.clone()).then(function(response) {
- // if (parseInt(response.status) < 400) {
- cache.put(request.clone(), response.clone());
- // }
- return response;
- });
- };
-
- SimpleOfflineCache.prototype.ensureCache = function soc_ensureCache() {
- if (!this.cacheRequest) {
- this.cacheRequest = caches.open(this.cacheName);
- }
- return this.cacheRequest;
- };
-
- SimpleOfflineCache.prototype.ensureHeadCache = function soc_ensureHeadCache() {
- if (!this.cacheHeadRequest) {
- this.cacheHeadRequest = caches.open(this.cacheName + "_head");
- }
- return this.cacheHeadRequest;
- };
-
- module.exports = SimpleOfflineCache;
- },
- {}
- ],
- 4: [
- function(require, module, exports) {
- /* globals caches, Promise, Request */
- "use strict";
-
- function StaticCacher(fileList) {
- if (!Array.isArray(fileList) || fileList.length === 0) {
- throw new Error("Invalid file list");
- }
- this.files = fileList;
- }
-
- StaticCacher.prototype.onInstall = function sc_onInstall() {
- var self = this;
- return this.getDefaultCache().then(function(cache) {
- return self.addAll(cache, self.files);
- });
- };
-
- StaticCacher.prototype.getDefaultCache = function sc_getDefaultCache() {
- if (!this.cacheRequest) {
- this.cacheRequest = caches.open("offline");
- }
- return this.cacheRequest;
- };
-
- StaticCacher.prototype.addAll = function(cache, urls) {
- if (!cache) {
- throw new Error("Need a cache to store things");
- }
- // Polyfill until chrome implements it
- if (typeof cache.addAll !== "undefined") {
- return cache.addAll(urls);
- }
-
- var promises = [];
- var self = this;
- urls.forEach(function(url) {
- promises.push(self.fetchAndCache(new Request(url), cache));
- });
-
- return Promise.all(promises);
- };
-
- StaticCacher.prototype.fetchAndCache = function sc_fetchAndCache(
- request,
- cache
- ) {
- return fetch(request.clone()).then(function(response) {
- if (parseInt(response.status) < 400) {
- cache.put(request.clone(), response.clone());
- }
- return response;
+
+ // Skip external requests
+ if (url.origin !== location.origin) {
+ return;
+ }
+
+ // Skip service worker updates (important!)
+ if (url.pathname === '/service-worker.js') {
+ return;
+ }
+
+ // Determine caching strategy based on request type
+ if (url.pathname === '/' || url.pathname.endsWith('.html')) {
+ // HTML: Network first, fall back to cache
+ event.respondWith(
+ fetch(request).then(function(response) {
+ // Update cache with fresh HTML
+ if (response.ok) {
+ const responseToCache = response.clone();
+ caches.open(CACHE_VERSION).then(function(cache) {
+ cache.put(request, responseToCache);
});
- };
-
- module.exports = StaticCacher;
- },
- {}
- ],
- 5: [
- function(require, module, exports) {
- /* global fetch, BroadcastChannel, clients, Promise, Request, Response */
- "use strict";
-
- var debug = function() {};
-
- var StaticCacher = require("./staticcacher.js");
- var SimpleOfflineCache = require("./simpleofflinecache.js");
- var Router = require("./router.js");
-
- var ERROR = "error";
- var CONTINUE = "continue";
- var TERMINATE = "terminate";
- var TERMINATION_TOKEN = {};
-
- function DEFAULT_FALLBACK_MW(request) {
- return fetch(request);
}
-
- function ServiceWorkerWare(options) {
- options = options || {};
- if (typeof options === "function" || options.onFetch) {
- options = { fallbackMiddleware: options };
- }
- options.autoClaim = "autoClaim" in options ? options.autoClaim : true;
- this.middleware = [];
- this.router = new Router({});
- this.router.proxyMethods(this);
-
- this.fallbackMw = options.fallbackMiddleware || DEFAULT_FALLBACK_MW;
- this.autoClaim = options.autoClaim;
- }
-
- ServiceWorkerWare.prototype.init = function sww_init() {
- // lifecycle events
- addEventListener("install", this);
- addEventListener("activate", this);
- addEventListener("beforeevicted", this);
- addEventListener("evicted", this);
-
- // network events
- addEventListener("fetch", this);
-
- // misc events
- addEventListener("message", this);
-
- // push notifications
- addEventListener("push", this);
-
- // XXX: Add default configuration
- };
-
- /**
- * Handle and forward all events related to SW
- */
- ServiceWorkerWare.prototype.handleEvent = function sww_handleEvent(
- evt
- ) {
- debug("Event received: " + evt.type);
- switch (evt.type) {
- case "install":
- this.onInstall(evt);
- break;
- case "fetch":
- this.onFetch(evt);
- break;
- case "activate":
- this.onActivate(evt);
- break;
- case "push":
- case "message":
- case "beforeevicted":
- case "evicted":
- this.forwardEvent(evt);
- break;
- default:
- debug("Unhandled event " + evt.type);
- }
- };
-
- ServiceWorkerWare.prototype.onFetch = function sww_onFetch(evt) {
- var steps = this.router.match(evt.request.method, evt.request.url);
-
- // Push the fallback middleware at the end of the list.
- // XXX bug 1165860: Decorating fallback MW with `stopIfResponse` until
- // 1165860 lands
- steps.push(
- function(req, res) {
- if (res) {
- return Promise.resolve(res);
- }
- return this.fallbackMw(req, res);
- }.bind(this)
- );
-
- evt.respondWith(this.executeMiddleware(steps, evt.request));
- };
-
- /**
- * Run the middleware pipeline and inform if errors preventing respondWith()
- * to swallow the error.
- *
- * @param {Array} the middleware pipeline
- * @param {Request} the request for the middleware
- */
- ServiceWorkerWare.prototype.executeMiddleware = function(
- middleware,
- request
- ) {
- var response = this.runMiddleware(middleware, 0, request, null);
- response.catch(function(error) {
- console.error(error);
- });
+ return response;
+ }).catch(function() {
+ // If network fails, try cache
+ return caches.match(request);
+ })
+ );
+ } else if (shouldCache(url.pathname)) {
+ // Static assets: Cache first, fall back to network
+ event.respondWith(
+ caches.match(request).then(function(response) {
+ if (response) {
+ // Found in cache
return response;
- };
-
- /**
- * Pass through the middleware pipeline, executing each middleware in a
- * sequence according to the result from each execution.
- *
- * Each middleware will be passed with the request and response from the
- * previous one in the pipeline. The response from the latest one will be
- * used to answer from the service worker. The middleware will receive,
- * as the last parameter, a function to stop the pipeline and answer
- * immediately.
- *
- * A middleware run can lead to continuing execution, interruption of the
- * pipeline or error. The next action to be performed is calculated according
- * the conditions of the middleware execution and its return value.
- * See normalizeMwAnswer() for details.
- *
- * @param {Array} middleware pipeline.
- * @param {Number} middleware to execute in the pipeline.
- * @param {Request} the request for the middleware.
- * @param {Response} the response for the middleware.
- */
- ServiceWorkerWare.prototype.runMiddleware = function(
- middleware,
- current,
- request,
- response
- ) {
- if (current >= middleware.length) {
- return Promise.resolve(response);
- }
-
- var mw = middleware[current];
- if (request) {
- request.parameters = mw.__params;
- }
- var endWith = ServiceWorkerWare.endWith;
- var answer = mw(request, response, endWith);
- var normalized = ServiceWorkerWare.normalizeMwAnswer(
- answer,
- request,
- response
- );
-
- return normalized.then(
- function(info) {
- switch (info.nextAction) {
- case TERMINATE:
- return Promise.resolve(info.response);
-
- case ERROR:
- return Promise.reject(info.error);
-
- case CONTINUE:
- var next = current + 1;
- var request = info.request;
- var response = info.response;
- return this.runMiddleware(
- middleware,
- next,
- request,
- response
- );
- }
- }.bind(this)
- );
- };
-
- /**
- * A function to force interruption of the pipeline.
- *
- * @param {Response} the response object that will be used to answer from the
- * service worker.
- */
- ServiceWorkerWare.endWith = function(response) {
- if (arguments.length === 0) {
- throw new Error(
- "Type error: endWith() must be called with a value."
- );
- }
- return [TERMINATION_TOKEN, response];
- };
-
- /**
- * A middleware is supposed to return a promise resolving in a pair of request
- * and response for the next one or to indicate that it wants to answer
- * immediately.
- *
- * To allow flexibility, the middleware is allowed to return other values
- * rather than the promise. For instance, it is allowed to return only a
- * request meaning the next middleware will be passed that request but the
- * previous response untouched.
- *
- * The function takes into account all the scenarios to compute the request
- * and response for the next middleware or the intention to terminate
- * immediately.
- *
- * @param {Any} non normalized answer from the middleware.
- * @param {Request} request passed as parameter to the middleware.
- * @param {Response} response passed as parameter to the middleware.
- */
- ServiceWorkerWare.normalizeMwAnswer = function(
- answer,
- request,
- response
- ) {
- if (!answer || !answer.then) {
- answer = Promise.resolve(answer);
- }
- return answer.then(function(value) {
- var nextAction = CONTINUE;
- var error, nextRequest, nextResponse;
- var isArray = Array.isArray(value);
-
- if (isArray && value[0] === TERMINATION_TOKEN) {
- nextAction = TERMINATE;
- nextRequest = request;
- nextResponse = value[1] || response;
- } else if (value === null) {
- nextRequest = request;
- nextResponse = null;
- } else if (isArray && value.length === 2) {
- nextRequest = value[0];
- nextResponse = value[1];
- } else if (value instanceof Response) {
- nextRequest = request;
- nextResponse = value;
- } else if (value instanceof Request) {
- nextRequest = value;
- nextResponse = response;
- } else {
- var msg =
- "Type error: middleware must return a Response, " +
- "a Request, a pair [Response, Request] or a Promise " +
- "resolving to one of these types.";
- nextAction = ERROR;
- error = new Error(msg);
- }
-
- return {
- nextAction: nextAction,
- request: nextRequest,
- response: nextResponse,
- error: error
- };
- });
- };
-
- /**
- * Walk all the middleware installed asking if they have prerequisites
- * (on the way of a promise to be resolved) when installing the SW.
- */
- ServiceWorkerWare.prototype.onInstall = function sww_oninstall(evt) {
- var installation = this.getFromMiddleware("onInstall");
- evt.waitUntil(installation);
- };
-
- /**
- * Walk all the installed middleware asking if they have prerequisites
- * (on the way of a promise to be resolved) when SW activates.
- */
- ServiceWorkerWare.prototype.onActivate = function sww_activate(evt) {
- var activation = this.getFromMiddleware("onActivate");
- if (this.autoClaim) {
- activation = activation.then(function claim() {
- return self.clients.claim();
- });
- }
- evt.waitUntil(activation);
- };
-
- /**
- * Returns a promise gathering the results for executing the same method for
- * all the middleware.
- * @param {Function} the method to be executed.
- * @param {Promise} a promise resolving once all the results have been gathered.
- */
- ServiceWorkerWare.prototype.getFromMiddleware = function sww_getFromMiddleware(
- method
- ) {
- var tasks = this.middleware.reduce(function(tasks, mw) {
- if (typeof mw[method] === "function") {
- tasks.push(mw[method]());
- }
- return tasks;
- }, []);
- return Promise.all(tasks);
- };
-
- /**
- * Register a new middleware layer, they will treat the request in
- * the order that this layers have been defined.
- * A middleware layer can behave in the ServiceWorker in two ways:
- * - Listening to SW lifecycle events (install, activate, message).
- * - Handle a request.
- * To handle each case (or both) the middleware object should provide
- * the following methods:
- * - on: for listening to SW lifeciclye events
- * - onFetch: for handling fetch urls
- */
- ServiceWorkerWare.prototype.use = function sww_use() {
- // If the first parameter is not a function we will understand that
- // is the path to handle, and the handler will be the second parameter
- if (arguments.length === 0) {
- throw new Error("No arguments given");
- }
- var mw = arguments[0];
- var path = "*";
- var method = this.router.ALL_METHODS;
- if (typeof mw === "string") {
- path = arguments[0];
- mw = arguments[1];
- var kind = typeof mw;
- if (!mw || !(kind === "object" || kind === "function")) {
- throw new Error("No middleware specified");
- }
- if (Router.prototype.methods.indexOf(arguments[2]) !== -1) {
- method = arguments[2];
- }
- }
-
- this.middleware.push(mw);
- // Add to the router just if middleware object is able to handle onFetch
- // or if we have a simple function
- var handler = null;
- if (typeof mw.onFetch === "function") {
- handler = mw.onFetch.bind(mw);
- } else if (typeof mw === "function") {
- handler = mw;
- }
- if (handler) {
- this.router.add(method, path, handler);
- }
- // XXX: Attaching the broadcastMessage to mw that implements onMessage.
- // We should provide a way to get a reference to the SWW object and do
- // the broadcast from there
- if (typeof mw.onMessage === "function") {
- mw.broadcastMessage = this.broadcastMessage;
- }
- };
-
- /**
- * Forward the event received to any middleware layer that has a 'on'
- * handler
- */
- ServiceWorkerWare.prototype.forwardEvent = function sww_forwardEvent(
- evt
- ) {
- this.middleware.forEach(function(mw) {
- var handlerName =
- "on" +
- evt.type.replace(/^[a-z]/, function(m) {
- return m.toUpperCase();
- });
- if (typeof mw[handlerName] !== "undefined") {
- mw[handlerName].call(mw, evt);
- }
- });
- };
-
- /**
- * Broadcast a message to all worker clients
- * @param msg Object the message
- * @param channel String (Used just in Firefox Nightly) using broadcastchannel
- * api to deliver the message, this parameter can be undefined as we listen for
- * a channel undefined in the client.
- */
- ServiceWorkerWare.prototype.broadcastMessage = function sww_broadcastMessage(
- msg,
- channel
- ) {
- // XXX: Until https://bugzilla.mozilla.org/show_bug.cgi?id=1130685 is fixed
- // we can use BroadcastChannel API in Firefox Nightly
- if (typeof BroadcastChannel === "function") {
- var bc = new BroadcastChannel(channel);
- bc.postMessage(msg);
- bc.close();
- return Promise.resolve();
- } else {
- // This is suppose to be the way of broadcasting a message, unfortunately
- // it's not working yet in Chrome Canary
- return clients.matchAll().then(function(consumers) {
- consumers.forEach(function(client) {
- client.postMessage(msg);
- });
+ }
+
+ // Not in cache, fetch from network
+ return fetch(request).then(function(response) {
+ // Cache successful responses
+ if (response.ok) {
+ const responseToCache = response.clone();
+ caches.open(CACHE_VERSION).then(function(cache) {
+ cache.put(request, responseToCache);
});
}
- };
-
- ServiceWorkerWare.decorators = {
- ifNoResponse: function(mw) {
- return function(req, res, endWith) {
- if (res) {
- return [req, res];
- }
- return mw(req, res, endWith);
- };
- },
-
- stopAfter: function(mw) {
- return function(req, res, endWith) {
- var answer = mw(req, res, endWith);
- var normalized = ServiceWorkerWare.normalizeMwAnswer(
- answer,
- req,
- res
- );
+ return response;
+ });
+ })
+ );
+ } else {
+ // Everything else: Just fetch from network
+ event.respondWith(fetch(request));
+ }
+});
- return normalized.then(function(info) {
- if (info.nextAction === "error") {
- return Promise.reject(info.error);
- }
- return endWith(info.response);
- });
- };
- }
- };
+// Listen for messages from the main thread
+self.addEventListener('message', function(event) {
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ console.log('[ServiceWorker] Received SKIP_WAITING message');
+ self.skipWaiting();
+ }
+
+ if (event.data && event.data.type === 'CLAIM_CLIENTS') {
+ console.log('[ServiceWorker] Received CLAIM_CLIENTS message');
+ self.clients.claim();
+ }
+});
- module.exports = {
- ServiceWorkerWare: ServiceWorkerWare,
- StaticCacher: StaticCacher,
- SimpleOfflineCache: SimpleOfflineCache
- };
- },
- { "./router.js": 2, "./simpleofflinecache.js": 3, "./staticcacher.js": 4 }
- ]
- },
- {},
- [1]
-);
+// Helper function to determine if a resource should be cached
+function shouldCache(pathname) {
+ // Check if it's a known static asset
+ if (STATIC_ASSETS.includes(pathname)) {
+ return true;
+ }
+
+ // Check file extension
+ for (const ext of CACHEABLE_TYPES) {
+ if (pathname.endsWith(ext)) {
+ return true;
+ }
+ }
+
+ // Check for specific paths that should be cached
+ if (pathname.startsWith('/assets/') ||
+ pathname.startsWith('/dist/') && pathname.endsWith('.js')) {
+ return true;
+ }
+
+ return false;
+}
-var worker = new self.ServiceWorkerWare();
-worker.use(new self.SimpleOfflineCache("offline"));
-worker.init();
+// Log service worker info
+console.log('[ServiceWorker] Loaded version:', CACHE_VERSION);
\ No newline at end of file
diff --git a/src/style.css b/src/style.css
index f9458f7..a2ee583 100644
--- a/src/style.css
+++ b/src/style.css
@@ -536,3 +536,88 @@ hr.solid {
.menuBarClosing .subMenuItem.active > .subMenu {
display: block !important;
}
+
+.dialog-buttons {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ margin-top: 20px;
+}
+
+.dialog-button {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background-color 0.2s;
+}
+
+.dialog-button.primary {
+ background-color: #007cba;
+ color: white;
+}
+
+.dialog-button.primary:hover {
+ background-color: #005a85;
+}
+
+.dialog-button.secondary {
+ background-color: #6c757d;
+ color: white;
+}
+
+.dialog-button.secondary:hover {
+ background-color: #5a6268;
+}
+
+#save-dialog-message {
+ text-align: center;
+ margin-bottom: 10px;
+}
+
+.dialog-close {
+ color: black;
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ border: 0;
+ padding: 0;
+ background-color: transparent;
+ font-weight: bold;
+ font-size: 1.5em;
+ width: 1.2em;
+ height: 1.2em;
+ text-align: center;
+ cursor: pointer;
+ transition: 0.15s;
+ z-index: 10;
+}
+
+.dialog-close:hover {
+ color: #666;
+}
+
+/* Reduce top padding for dialog content */
+.dialog-content {
+ background-color: rgb(255, 255, 255);
+ z-index: 3;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ -webkit-transform: translate(-50%, -50%);
+ -ms-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ margin: 0;
+ padding: 20px 2em 2em 2em; /* Reduced top padding */
+ max-width: 50%;
+ border-radius: 5px;
+ border: none;
+}
+
+/* Ensure h3 has no top margin */
+.dialog-content h3 {
+ margin-top: 0;
+ margin-bottom: 15px;
+ font-size: 1.25em;
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 01290c4..6c0624b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -54,6 +54,10 @@ const config = (env, argv) => ({
},
{
from: path.resolve(__dirname, 'src', 'imagej-js-chatbot-extension.imjoy.html')
+ },
+ {
+ from: path.resolve(__dirname, 'src', 'figlinq.js'),
+ to: path.resolve(__dirname, 'dist', 'figlinq.js')
}
]),
new HtmlWebpackPlugin({