Просмотр исходного кода

Merge pull request #8 from gildas-lormeau/master

upd
solokot 5 лет назад
Родитель
Сommit
9d30f296f6

+ 4 - 0
_locales/de/messages.json

@@ -11,6 +11,10 @@
 		"message": "Speichern ausgewählter Links",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotieren der Webseite...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Annotieren und Speichern der Webseite...",
 		"description": "Menu entry: 'Annotate and save the page...'"

+ 8 - 4
_locales/en/messages.json

@@ -7,6 +7,10 @@
 		"message": "Save page with SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Annotate and save the page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -196,10 +200,10 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "skip duplicate files",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "skip duplicate files",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "HTML content",
 		"description": "Options sub-title: 'HTML content'"
 	},

+ 8 - 4
_locales/es/messages.json

@@ -11,6 +11,10 @@
 		"message": "Guardar los links seleccionados",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Anotar la página",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Anotar y guardar la página",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -196,10 +200,10 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "saltar los archivos duplicados",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "saltar los archivos duplicados",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "Contenido HTML",
 		"description": "Options sub-title: 'HTML content'"
 	},

+ 8 - 4
_locales/fr/messages.json

@@ -11,6 +11,10 @@
 		"message": "Sauver les liens selectionnés",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annoter la page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Annoter et sauver la page...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -196,10 +200,10 @@
 		"description": "Value for 'filename conflict resolution' option: 'prompt for a filename'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "passer les fichiers en double",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "passer les fichiers en double",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "Contenu HTML",
 		"description": "Options sub-title: 'HTML content'"
 	},

+ 4 - 0
_locales/ja/messages.json

@@ -11,6 +11,10 @@
 		"message": "Save selected links",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "ページに注釈を付けて保存する...",
 		"description": "Menu entry: 'Annotate and save the page...'"

+ 8 - 4
_locales/pl/messages.json

@@ -11,6 +11,10 @@
 		"message": "Zapisz wybrane odnośniki",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Adnotuj stronę...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Adnotuj i zapisz stronę...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -196,10 +200,10 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "pomiń duplikaty plików",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "pomiń duplikaty plików",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "Zawartość HTML",
 		"description": "Options sub-title: 'HTML content'"
 	},

+ 9 - 5
_locales/ru/messages.json

@@ -11,6 +11,10 @@
 		"message": "Сохранить выбранные ссылки",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Аннотировать и сохранить страницу...",
 		"description": "Menu entry: 'Annotate and save the page...'"
@@ -196,10 +200,10 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "пропустить дублирующиеся файлы",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "пропустить дублирующиеся файлы",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "Содержимое HTML",
 		"description": "Options sub-title: 'HTML content'"
 	},
@@ -635,4 +639,4 @@
 		"message": "Отмена",
 		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
-}
+}

+ 4 - 0
_locales/uk/messages.json

@@ -11,6 +11,10 @@
 		"message": "Save selected links",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "Анотувати і зберегти сторінку...",
 		"description": "Menu entry: 'Annotate and save the page...'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -11,6 +11,10 @@
 		"message": "保存所选链接",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "标注并保存该页面...",
 		"description": "Menu entry: 'Annotate and save the page...'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -11,6 +11,10 @@
 		"message": "保存所選鏈接",
 		"description": "Menu entry: 'Save selected links'"
 	},
+	"menuEditPage": {
+		"message": "Annotate the page...",
+		"description": "Menu entry: 'Annotate the page...'"
+	},
 	"menuEditAndSavePage": {
 		"message": "標註並保存該頁面...",
 		"description": "Menu entry: 'Annotate and save the page...'"

+ 1 - 1
cli/README.MD

@@ -20,7 +20,7 @@ SingleFile can be launched from the command line by running it into a (headless)
   
 - As an alternative to decompressing the master archive, you can clone the repository if `git` is installed on your machine and go into the `SingleFile/cli` directory.
 
-  `git clone https://github.com/gildas-lormeau/SingleFile.git`
+  `git clone --depth 1 --recursive https://github.com/gildas-lormeau/SingleFile.git`
   
   `cd SingleFile`
   

+ 6 - 1
extension/core/bg/business.js

@@ -61,6 +61,7 @@ singlefile.extension.core.bg.business = (() => {
 				taskInfo.cancel = cancelCallback;
 			}
 		},
+		openEditor,
 		onSaveEnd: taskId => {
 			const taskInfo = tasks.find(taskInfo => taskInfo.id == taskId);
 			if (taskInfo) {
@@ -132,6 +133,10 @@ singlefile.extension.core.bg.business = (() => {
 		runTasks();
 	}
 
+	function openEditor(tab) {
+		singlefile.extension.core.bg.tabs.sendMessage(tab.id, { method: "content.openEditor" });
+	}
+
 	async function initMaxParallelWorkers() {
 		if (!maxParallelWorkers) {
 			maxParallelWorkers = (await singlefile.extension.core.bg.config.get()).maxParallelWorkers;
@@ -167,7 +172,7 @@ singlefile.extension.core.bg.business = (() => {
 			if (!taskInfo.tab.id) {
 				let scriptsInjected;
 				try {
-					const tab = await tabs.create({ url: taskInfo.tab.url, active: false });
+					const tab = await tabs.createAndWait({ url: taskInfo.tab.url, active: false });
 					taskInfo.tab.id = taskInfo.options.tabId = tab.id;
 					taskInfo.tab.index = taskInfo.options.tabIndex = tab.index;
 					ui.onStart(taskInfo.tab.id, INJECT_SCRIPTS_STEP);

+ 7 - 8
extension/core/bg/editor.js

@@ -34,7 +34,7 @@ singlefile.extension.core.bg.editor = (() => {
 	return {
 		onMessage,
 		onTabRemoved,
-		onInit,
+		isEditor,
 		open
 	};
 
@@ -43,7 +43,7 @@ singlefile.extension.core.bg.editor = (() => {
 		if (tabIndex != null) {
 			createTabProperties.index = tabIndex;
 		}
-		const tab = await browser.tabs.create(createTabProperties);
+		const tab = await singlefile.extension.core.bg.tabs.create(createTabProperties);
 		tabsData.set(tab.id, { content, filename, options });
 	}
 
@@ -51,10 +51,8 @@ singlefile.extension.core.bg.editor = (() => {
 		tabsData.delete(tabId);
 	}
 
-	function onInit(tab) {
-		if (tab.url != EDITOR_URL) {
-			tabsData.delete(tab.id);
-		}
+	function isEditor(tab) {
+		return tab.url == EDITOR_URL;
 	}
 
 	async function onMessage(message, sender) {
@@ -96,8 +94,9 @@ singlefile.extension.core.bg.editor = (() => {
 			}
 			if (!message.truncated || message.finished) {
 				const options = await singlefile.extension.core.bg.config.getOptions(tab && tab.url);
-				await singlefile.extension.core.bg.tabs.remove(tab.id);
-				await open({ tabIndex: tab.index, filename: message.filename, content: contents.join("") }, options);
+				const updateTabProperties = { url: EDITOR_PAGE_URL };
+				await singlefile.extension.core.bg.tabs.update(tab.id, updateTabProperties);
+				tabsData.set(tab.id, { content: contents.join(""), filename: message.filename, options });
 			}
 		}
 	}

+ 4 - 4
extension/core/bg/tabs-data.js

@@ -29,10 +29,10 @@ singlefile.extension.core.bg.tabsData = (() => {
 	setTimeout(() => getPersistent().then(tabsData => persistentData = tabsData), 0);
 	return {
 		onMessage,
-		onTabRemoved,
 		getTemporary,
 		get: getPersistent,
-		set: setPersistent
+		set: setPersistent,
+		remove
 	};
 
 	function onMessage(message) {
@@ -44,13 +44,13 @@ singlefile.extension.core.bg.tabsData = (() => {
 		}
 	}
 
-	async function onTabRemoved(tabId) {
+	async function remove(tabId) {
 		if (temporaryData) {
 			delete temporaryData[tabId];
 		}
 		const tabsData = await getPersistent();
 		delete tabsData[tabId];
-		setPersistent(tabsData);
+		await setPersistent(tabsData);
 	}
 
 	function getTemporary(desiredTabId) {

+ 20 - 4
extension/core/bg/tabs.js

@@ -37,7 +37,8 @@ singlefile.extension.core.bg.tabs = (() => {
 			const tabs = await browser.tabs.query(options);
 			return tabs.sort((tab1, tab2) => tab1.index - tab2.index);
 		},
-		create: async createProperties => {
+		create: createProperties => browser.tabs.create(createProperties),
+		createAndWait: async createProperties => {
 			const tab = await browser.tabs.create(createProperties);
 			return new Promise((resolve, reject) => {
 				browser.tabs.onUpdated.addListener(onTabUpdated);
@@ -58,6 +59,7 @@ singlefile.extension.core.bg.tabs = (() => {
 			});
 		},
 		sendMessage: (tabId, message, options) => browser.tabs.sendMessage(tabId, message, options),
+		update: (tabId, updateProperties) => browser.tabs.update(tabId, updateProperties),
 		remove: tabId => browser.tabs.remove(tabId),
 		promptValue: async promptMessage => {
 			const tabs = await browser.tabs.query({ currentWindow: true, active: true });
@@ -118,10 +120,10 @@ singlefile.extension.core.bg.tabs = (() => {
 
 	async function onMessage(message, sender) {
 		if (message.method.endsWith(".init")) {
+			await onInit(sender.tab, message);
 			singlefile.extension.ui.bg.main.onInit(sender.tab);
 			singlefile.extension.core.bg.business.onInit(sender.tab);
 			singlefile.extension.core.bg.autosave.onInit(sender.tab);
-			singlefile.extension.core.bg.editor.onInit(sender.tab);
 		}
 		if (message.method.endsWith(".promptValueResponse")) {
 			const promptPromise = pendingPrompts.get(sender.tab.id);
@@ -138,9 +140,23 @@ singlefile.extension.core.bg.tabs = (() => {
 		}
 	}
 
+	async function onInit(tab, options) {
+		await singlefile.extension.core.bg.tabsData.remove(tab.id);
+		const tabsData = await singlefile.extension.core.bg.tabsData.get(tab.id);
+		tabsData[tab.id].savedPageDetected = options.savedPageDetected;
+		await singlefile.extension.core.bg.tabsData.set(tabsData);
+	}
+
 	async function onTabUpdated(tabId, changeInfo) {
 		if (changeInfo.status == "complete") {
 			setTimeout(() => browser.tabs.sendMessage(tabId, { method: "content.maybeInit" }), DELAY_MAYBE_INIT);
+			const tab = await browser.tabs.get(tabId);
+			if (singlefile.extension.core.bg.editor.isEditor(tab)) {
+				const tabsData = await singlefile.extension.core.bg.tabsData.get(tab.id);
+				tabsData[tab.id].editorDetected = true;
+				await singlefile.extension.core.bg.tabsData.set(tabsData);
+				singlefile.extension.ui.bg.main.onTabActivated(tab);
+			}
 		}
 	}
 
@@ -150,11 +166,11 @@ singlefile.extension.core.bg.tabs = (() => {
 
 	async function onTabActivated(activeInfo) {
 		const tab = await browser.tabs.get(activeInfo.tabId);
-		singlefile.extension.ui.bg.main.onTabActivated(tab, activeInfo);
+		singlefile.extension.ui.bg.main.onTabActivated(tab);
 	}
 
 	function onTabRemoved(tabId) {
-		singlefile.extension.core.bg.tabsData.onTabRemoved(tabId);
+		singlefile.extension.core.bg.tabsData.remove(tabId);
 		singlefile.extension.core.bg.editor.onTabRemoved(tabId);
 		singlefile.extension.core.bg.business.onTabRemoved(tabId);
 	}

+ 36 - 22
extension/core/content/content-bootstrap.js

@@ -48,12 +48,13 @@ this.singlefile.extension.core.content.bootstrap = this.singlefile.extension.cor
 		if ((autoSaveEnabled && message.method == "content.autosave") ||
 			message.method == "content.maybeInit" ||
 			message.method == "content.init" ||
+			message.method == "content.openEditor" ||
 			message.method == "devtools.resourceCommitted" ||
 			message.method == "common.promptValueRequest") {
 			return onMessage(message);
 		}
 	});
-	init();
+	document.addEventListener("DOMContentLoaded", init, false);
 	return {};
 
 	async function onMessage(message) {
@@ -71,6 +72,14 @@ this.singlefile.extension.core.content.bootstrap = this.singlefile.extension.cor
 			refresh();
 			return {};
 		}
+		if (message.method == "content.openEditor") {
+			if (detectSavedPage(document)) {
+				openEditor(document);
+			} else {
+				refresh();
+			}
+			return {};
+		}
 		if (message.method == "devtools.resourceCommitted") {
 			singlefile.extension.core.content.updatedResources[message.url] = { content: message.content, type: message.type, encoding: message.encoding };
 			return {};
@@ -85,7 +94,7 @@ this.singlefile.extension.core.content.bootstrap = this.singlefile.extension.cor
 		if (previousLocationHref != location.href && !singlefile.extension.core.processing) {
 			pageAutoSaved = false;
 			previousLocationHref = location.href;
-			browser.runtime.sendMessage({ method: "tabs.init" });
+			browser.runtime.sendMessage({ method: "tabs.init", savedPageDetected: detectSavedPage(document) });
 			browser.runtime.sendMessage({ method: "ui.processInit" });
 		}
 	}
@@ -186,30 +195,35 @@ this.singlefile.extension.core.content.bootstrap = this.singlefile.extension.cor
 	}
 
 	async function openEditor(document) {
-		const helper = singlefile.lib.helper;
-		if (document.documentElement.firstChild.nodeType == Node.COMMENT_NODE &&
-			(document.documentElement.firstChild.textContent.includes(helper.COMMENT_HEADER) || document.documentElement.firstChild.textContent.includes(helper.COMMENT_HEADER_LEGACY))) {
-			serializeShadowRoots(document);
-			const content = singlefile.lib.modules.serializer.process(document);
-			for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
-				const message = {
-					method: "editor.open",
-					filename: decodeURIComponent(location.href.match(/^.*\/(.*)$/)[1])
-				};
-				message.truncated = content.length > MAX_CONTENT_SIZE;
-				if (message.truncated) {
-					message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > content.length;
-					message.content = content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
-				} else {
-					message.content = content;
-				}
-				await browser.runtime.sendMessage(message);
+		const infobarElement = document.querySelector("singlefile-infobar");
+		if (infobarElement) {
+			infobarElement.remove();
+		}
+		serializeShadowRoots(document);
+		const content = singlefile.lib.modules.serializer.process(document);
+		for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
+			const message = {
+				method: "editor.open",
+				filename: decodeURIComponent(location.href.match(/^.*\/(.*)$/)[1])
+			};
+			message.truncated = content.length > MAX_CONTENT_SIZE;
+			if (message.truncated) {
+				message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > content.length;
+				message.content = content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
+			} else {
+				message.content = content;
 			}
-		} else {
-			refresh();
+			await browser.runtime.sendMessage(message);
 		}
 	}
 
+	function detectSavedPage(document) {
+		const helper = singlefile.lib.helper;
+		const firstDocumentChild = document.documentElement.firstChild;
+		return firstDocumentChild.nodeType == Node.COMMENT_NODE &&
+			(firstDocumentChild.textContent.includes(helper.COMMENT_HEADER) || firstDocumentChild.textContent.includes(helper.COMMENT_HEADER_LEGACY));
+	}
+
 	function serializeShadowRoots(node) {
 		const SHADOW_MODE_ATTRIBUTE_NAME = "shadowmode";
 		node.querySelectorAll("*").forEach(element => {

+ 3 - 3
extension/ui/bg/ui-editor.js

@@ -199,11 +199,11 @@ singlefile.extension.ui.bg.editor = (() => {
 			tabData.options = message.options;
 			savePage();
 			browser.runtime.sendMessage({ method: "ui.processInit" });
-			return {};
+			return Promise.resolve({});
 		}
 		if (message.method == "common.promptValueRequest") {
 			browser.runtime.sendMessage({ method: "tabs.promptValueResponse", value: prompt(message.promptMessage) });
-			return {};
+			return Promise.resolve({});
 		}
 		if (message.method == "editor.setTabData") {
 			if (message.truncated) {
@@ -217,7 +217,7 @@ singlefile.extension.ui.bg.editor = (() => {
 				editorElement.contentWindow.postMessage(JSON.stringify({ method: "init", content: tabData.content }), "*");
 				delete tabData.content;
 			}
-			return {};
+			return Promise.resolve({});
 		}
 	});
 

+ 2 - 2
extension/ui/bg/ui-main.js

@@ -60,8 +60,8 @@ singlefile.extension.ui.bg.main = (() => {
 		onTabCreated(tab) {
 			singlefile.extension.ui.bg.menus.onTabCreated(tab);
 		},
-		onTabActivated(tab, activeInfo) {
-			singlefile.extension.ui.bg.menus.onTabActivated(tab, activeInfo);
+		onTabActivated(tab) {
+			singlefile.extension.ui.bg.menus.onTabActivated(tab);
 		},
 		onInit(tab) {
 			singlefile.extension.ui.bg.menus.onInit(tab);

+ 71 - 34
extension/ui/bg/ui-menus.js

@@ -53,6 +53,7 @@ singlefile.extension.ui.bg.menus = (() => {
 	const MENU_UPDATE_RULE_MESSAGE = browser.i18n.getMessage("menuUpdateRule");
 	const MENU_SAVE_PAGE_MESSAGE = browser.i18n.getMessage("menuSavePage");
 	const MENU_SAVE_SELECTED_LINKS = browser.i18n.getMessage("menuSaveSelectedLinks");
+	const MENU_EDIT_PAGE_MESSAGE = browser.i18n.getMessage("menuEditPage");
 	const MENU_EDIT_AND_SAVE_PAGE_MESSAGE = browser.i18n.getMessage("menuEditAndSavePage");
 	const MENU_VIEW_PENDINGS_MESSAGE = browser.i18n.getMessage("menuViewPendingSaves");
 	const MENU_SAVE_SELECTION_MESSAGE = browser.i18n.getMessage("menuSaveSelection");
@@ -68,10 +69,21 @@ singlefile.extension.ui.bg.menus = (() => {
 	const MENU_AUTOSAVE_TAB_MESSAGE = browser.i18n.getMessage("menuAutoSaveTab");
 	const MENU_AUTOSAVE_UNPINNED_TABS_MESSAGE = browser.i18n.getMessage("menuAutoSaveUnpinnedTabs");
 	const MENU_AUTOSAVE_ALL_TABS_MESSAGE = browser.i18n.getMessage("menuAutoSaveAllTabs");
+	const MENU_TOP_VISIBLE_ENTRIES = [
+		MENU_ID_EDIT_AND_SAVE_PAGE,
+		MENU_ID_SAVE_SELECTED_LINKS,
+		MENU_ID_VIEW_PENDINGS,
+		MENU_ID_SAVE_SELECTED,
+		MENU_ID_SAVE_FRAME,
+		MENU_ID_AUTO_SAVE,
+		MENU_ID_SELECT_PROFILE,
+		MENU_ID_ASSOCIATE_WITH_PROFILE
+	];
 
 	const menusCheckedState = new Map();
 	const menusTitleState = new Map();
-	let menusVisibleState;
+	let contextMenuVisibleState = true;
+	let allMenuVisibleState = true;
 	let profileIndexes = new Map();
 	let menusCreated, pendingRefresh;
 	initialize();
@@ -357,10 +369,15 @@ singlefile.extension.ui.bg.menus = (() => {
 					}
 				}
 				if (event.menuItemId == MENU_ID_EDIT_AND_SAVE_PAGE) {
-					if (event.linkUrl) {
-						business.saveUrls([event.linkUrl], { openEditor: true });
+					const allTabsData = await tabsData.get(tab.id);
+					if (allTabsData[tab.id].savedPageDetected) {
+						business.openEditor(tab);
 					} else {
-						business.saveTabs([tab], { openEditor: true });
+						if (event.linkUrl) {
+							business.saveUrls([event.linkUrl], { openEditor: true });
+						} else {
+							business.saveTabs([tab], { openEditor: true });
+						}
 					}
 				}
 				if (event.menuItemId == MENU_ID_SAVE_SELECTED_LINKS) {
@@ -466,45 +483,65 @@ singlefile.extension.ui.bg.menus = (() => {
 	async function refreshTab(tab) {
 		const config = singlefile.extension.core.bg.config;
 		if (BROWSER_MENUS_API_SUPPORTED && menusCreated) {
-			const tabsData = await singlefile.extension.core.bg.tabsData.get(tab.id);
 			const promises = [];
-			promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_DISABLED, !tabsData[tab.id].autoSave));
-			promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_TAB, tabsData[tab.id].autoSave));
-			promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_UNPINNED, Boolean(tabsData.autoSaveUnpinned)));
-			promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_ALL, Boolean(tabsData.autoSaveAll)));
-			if (tab && tab.url) {
-				const options = await config.getOptions(tab.url);
-				promises.push(updateVisibleValue(tab, options.contextMenuEnabled));
-				promises.push(menus.update(MENU_ID_SAVE_SELECTED, { visible: !options.saveRawPage }));
-				let selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default";
-				let title = MENU_CREATE_DOMAIN_RULE_MESSAGE;
-				const [profiles, rule] = await Promise.all([config.getProfiles(), config.getRule(tab.url)]);
-				if (rule) {
-					const profileIndex = profileIndexes.get(rule.profile);
-					if (profileIndex) {
-						selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex;
-						title = MENU_UPDATE_RULE_MESSAGE;
-					}
-				}
-				if (Object.keys(profiles).length > 1) {
-					Object.keys(profiles).forEach((profileName, profileIndex) => {
-						if (profileName == config.DEFAULT_PROFILE_NAME) {
-							promises.push(updateCheckedValue(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default", selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default"));
-						} else {
-							promises.push(updateCheckedValue(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex, selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex));
+			const tabsData = await singlefile.extension.core.bg.tabsData.get(tab.id);
+			if (tabsData[tab.id].editorDetected) {
+				updateAllVisibleValues(false);
+			} else {
+				updateAllVisibleValues(true);
+				promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_DISABLED, !tabsData[tab.id].autoSave));
+				promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_TAB, tabsData[tab.id].autoSave));
+				promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_UNPINNED, Boolean(tabsData.autoSaveUnpinned)));
+				promises.push(updateCheckedValue(MENU_ID_AUTO_SAVE_ALL, Boolean(tabsData.autoSaveAll)));
+				if (tab && tab.url) {
+					const options = await config.getOptions(tab.url);
+					promises.push(updateVisibleValue(tab, options.contextMenuEnabled));
+					promises.push(updateTitleValue(MENU_ID_EDIT_AND_SAVE_PAGE, tabsData[tab.id].savedPageDetected ? MENU_EDIT_PAGE_MESSAGE : MENU_EDIT_AND_SAVE_PAGE_MESSAGE));
+					promises.push(menus.update(MENU_ID_SAVE_SELECTED, { visible: !options.saveRawPage }));
+					let selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default";
+					let title = MENU_CREATE_DOMAIN_RULE_MESSAGE;
+					const [profiles, rule] = await Promise.all([config.getProfiles(), config.getRule(tab.url)]);
+					if (rule) {
+						const profileIndex = profileIndexes.get(rule.profile);
+						if (profileIndex) {
+							selectedEntryId = MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex;
+							title = MENU_UPDATE_RULE_MESSAGE;
 						}
-					});
-					promises.push(updateTitleValue(MENU_ID_ASSOCIATE_WITH_PROFILE, title));
+					}
+					if (Object.keys(profiles).length > 1) {
+						Object.keys(profiles).forEach((profileName, profileIndex) => {
+							if (profileName == config.DEFAULT_PROFILE_NAME) {
+								promises.push(updateCheckedValue(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default", selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + "default"));
+							} else {
+								promises.push(updateCheckedValue(MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex, selectedEntryId == MENU_ID_ASSOCIATE_WITH_PROFILE_PREFIX + profileIndex));
+							}
+						});
+						promises.push(updateTitleValue(MENU_ID_ASSOCIATE_WITH_PROFILE, title));
+					}
 				}
 			}
 			await Promise.all(promises);
 		}
 	}
 
+	async function updateAllVisibleValues(visible) {
+		const lastVisibleState = allMenuVisibleState;
+		allMenuVisibleState = visible;
+		if (lastVisibleState === undefined || lastVisibleState != visible) {
+			const promises = [];
+			try {
+				MENU_TOP_VISIBLE_ENTRIES.forEach(id => promises.push(menus.update(id, { visible })));
+				await Promise.all(promises);
+			} catch (error) {
+				// ignored
+			}
+		}
+	}
+
 	async function updateVisibleValue(tab, visible) {
-		const lastVisibleValue = menusVisibleState;
-		menusVisibleState = visible;
-		if (lastVisibleValue === undefined || lastVisibleValue != visible) {
+		const lastVisibleState = contextMenuVisibleState;
+		contextMenuVisibleState = visible;
+		if (lastVisibleState === undefined || lastVisibleState != visible) {
 			await createMenus(tab);
 		}
 	}

+ 10 - 0
extension/ui/content/content-ui-editor-web.js

@@ -898,6 +898,11 @@ table {
 			element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.innerHTML);
 			element.textContent = "";
 		});
+		contentDocument.querySelectorAll("iframe").forEach(element => {
+			const pointerEvents = "pointer-events";
+			element.style.setProperty("-sf-" + pointerEvents, element.style.getPropertyValue(pointerEvents), element.style.getPropertyPriority(pointerEvents));
+			element.style.setProperty(pointerEvents, "none", "important");
+		});
 		document.replaceChild(contentDocument.documentElement, document.documentElement);
 		deserializeShadowRoots(document);
 		const iconElement = document.querySelector("link[rel*=icon]");
@@ -1386,6 +1391,11 @@ table {
 				mainElement.textContent = mainElement.value;
 			}
 		});
+		doc.querySelectorAll("iframe").forEach(element => {
+			const pointerEvents = "pointer-events";
+			element.style.setProperty(pointerEvents, element.style.getPropertyValue("-sf-" + pointerEvents), element.style.getPropertyPriority("-sf-" + pointerEvents));
+			element.style.removeProperty("-sf-" + pointerEvents);
+		});
 		delete doc.body.contentEditable;
 		const scriptElement = doc.createElement("script");
 		scriptElement.setAttribute(SCRIPT_TEMPLATE_SHADOW_ROOT, "");

+ 6 - 4
lib/single-file/processors/lazy/content/content-lazy-loader.js

@@ -86,9 +86,9 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 			}, options.loadDeferredImagesMaxIdleTime * 1.2);
 			maxTimeoutId = await deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, cleanupAndResolve);
 			observer.observe(document, { subtree: true, childList: true, attributes: true });
-			addEventListener.call(window, frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
-			addEventListener.call(window, frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
 			if (frames) {
+				addEventListener.call(window, frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
+				addEventListener.call(window, frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
 				frames.loadDeferredImagesStart(options);
 			}
 
@@ -110,8 +110,10 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 
 			function cleanupAndResolve(value) {
 				observer.disconnect();
-				removeEventListener.call(window, frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
-				removeEventListener.call(window, frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
+				if (frames) {
+					removeEventListener.call(window, frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
+					removeEventListener.call(window, frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
+				}
 				resolve(value);
 			}
 		});

+ 1 - 1
manifest.json

@@ -8,7 +8,7 @@
 		"64": "extension/ui/resources/icon_64.png",
 		"128": "extension/ui/resources/icon_128.png"
 	},
-	"version": "1.17.52",
+	"version": "1.17.56",
 	"description": "__MSG_extensionDescription__",
 	"content_scripts": [
 		{