1
0

ui-editor.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. /*
  2. * Copyright 2010-2020 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * The code in this file is free software: you can redistribute it and/or
  8. * modify it under the terms of the GNU Affero General Public License
  9. * (GNU AGPL) as published by the Free Software Foundation, either version 3
  10. * of the License, or (at your option) any later version.
  11. *
  12. * The code in this file is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  15. * General Public License for more details.
  16. *
  17. * As additional permission under GNU AGPL version 3 section 7, you may
  18. * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
  19. * AGPL normally required by section 4, provided you include this license
  20. * notice and a URL through which recipients can access the Corresponding
  21. * Source.
  22. */
  23. /* global browser, document, matchMedia, addEventListener, navigator, prompt, URL, MouseEvent, Blob, setInterval, DOMParser */
  24. import * as download from "../../core/common/download.js";
  25. import { onError } from "./../common/common-content-ui.js";
  26. import * as zip from "./../../../lib/single-file-zip.js";
  27. import * as yabson from "./../../lib/yabson/yabson.js";
  28. const EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
  29. const SHARE_PAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelSharePageButton");
  30. const SHARE_SELECTION_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelShareSelectionButton");
  31. const ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
  32. const FOREGROUND_SAVE = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/Vivaldi/.test(navigator.userAgent) && !/OPR/.test(navigator.userAgent);
  33. const SHADOWROOT_ATTRIBUTE_NAME = "shadowrootmode";
  34. const INFOBAR_TAGNAME = "single-file-infobar";
  35. const editorElement = document.querySelector(".editor");
  36. const toolbarElement = document.querySelector(".toolbar");
  37. const highlightYellowButton = document.querySelector(".highlight-yellow-button");
  38. const highlightPinkButton = document.querySelector(".highlight-pink-button");
  39. const highlightBlueButton = document.querySelector(".highlight-blue-button");
  40. const highlightGreenButton = document.querySelector(".highlight-green-button");
  41. const highlightButtons = Array.from(document.querySelectorAll(".highlight-button"));
  42. const toggleNotesButton = document.querySelector(".toggle-notes-button");
  43. const toggleHighlightsButton = document.querySelector(".toggle-highlights-button");
  44. const removeHighlightButton = document.querySelector(".remove-highlight-button");
  45. const addYellowNoteButton = document.querySelector(".add-note-yellow-button");
  46. const addPinkNoteButton = document.querySelector(".add-note-pink-button");
  47. const addBlueNoteButton = document.querySelector(".add-note-blue-button");
  48. const addGreenNoteButton = document.querySelector(".add-note-green-button");
  49. const editPageButton = document.querySelector(".edit-page-button");
  50. const formatPageButton = document.querySelector(".format-page-button");
  51. const cutInnerPageButton = document.querySelector(".cut-inner-page-button");
  52. const cutOuterPageButton = document.querySelector(".cut-outer-page-button");
  53. const undoCutPageButton = document.querySelector(".undo-cut-page-button");
  54. const undoAllCutPageButton = document.querySelector(".undo-all-cut-page-button");
  55. const redoCutPageButton = document.querySelector(".redo-cut-page-button");
  56. const savePageButton = document.querySelector(".save-page-button");
  57. const printPageButton = document.querySelector(".print-page-button");
  58. const lastButton = toolbarElement.querySelector(".buttons:last-of-type [type=button]:last-of-type");
  59. let tabData, tabDataContents = [], downloadParser;
  60. addYellowNoteButton.title = browser.i18n.getMessage("editorAddYellowNote");
  61. addPinkNoteButton.title = browser.i18n.getMessage("editorAddPinkNote");
  62. addBlueNoteButton.title = browser.i18n.getMessage("editorAddBlueNote");
  63. addGreenNoteButton.title = browser.i18n.getMessage("editorAddGreenNote");
  64. highlightYellowButton.title = browser.i18n.getMessage("editorHighlightYellow");
  65. highlightPinkButton.title = browser.i18n.getMessage("editorHighlightPink");
  66. highlightBlueButton.title = browser.i18n.getMessage("editorHighlightBlue");
  67. highlightGreenButton.title = browser.i18n.getMessage("editorHighlightGreen");
  68. toggleNotesButton.title = browser.i18n.getMessage("editorToggleNotes");
  69. toggleHighlightsButton.title = browser.i18n.getMessage("editorToggleHighlights");
  70. removeHighlightButton.title = browser.i18n.getMessage("editorRemoveHighlight");
  71. editPageButton.title = browser.i18n.getMessage("editorEditPage");
  72. formatPageButton.title = browser.i18n.getMessage("editorFormatPage");
  73. cutInnerPageButton.title = browser.i18n.getMessage("editorCutInnerPage");
  74. cutOuterPageButton.title = browser.i18n.getMessage("editorCutOuterPage");
  75. undoCutPageButton.title = browser.i18n.getMessage("editorUndoCutPage");
  76. undoAllCutPageButton.title = browser.i18n.getMessage("editorUndoAllCutPage");
  77. redoCutPageButton.title = browser.i18n.getMessage("editorRedoCutPage");
  78. savePageButton.title = browser.i18n.getMessage("editorSavePage");
  79. printPageButton.title = browser.i18n.getMessage("editorPrintPage");
  80. addYellowNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-yellow" }), "*");
  81. addPinkNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-pink" }), "*");
  82. addBlueNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-blue" }), "*");
  83. addGreenNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-green" }), "*");
  84. document.addEventListener("mouseup", event => {
  85. if (event.target.tagName.toLowerCase() != INFOBAR_TAGNAME) {
  86. editorElement.contentWindow.focus();
  87. toolbarOnTouchEnd(event);
  88. }
  89. }, true);
  90. document.onmousemove = toolbarOnTouchMove;
  91. highlightButtons.forEach(highlightButton => {
  92. highlightButton.onmouseup = () => {
  93. if (toolbarElement.classList.contains("cut-inner-mode")) {
  94. disableCutInnerPage();
  95. }
  96. if (toolbarElement.classList.contains("cut-outer-mode")) {
  97. disableCutOuterPage();
  98. }
  99. if (toolbarElement.classList.contains("remove-highlight-mode")) {
  100. disableRemoveHighlights();
  101. }
  102. const disabled = highlightButton.classList.contains("highlight-disabled");
  103. resetHighlightButtons();
  104. if (disabled) {
  105. highlightButton.classList.remove("highlight-disabled");
  106. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableHighlight", color: "single-file-highlight-" + highlightButton.dataset.color }), "*");
  107. } else {
  108. highlightButton.classList.add("highlight-disabled");
  109. }
  110. };
  111. });
  112. toggleNotesButton.onmouseup = () => {
  113. if (toggleNotesButton.getAttribute("src") == "/src/ui/resources/button_note_visible.png") {
  114. toggleNotesButton.src = "/src/ui/resources/button_note_hidden.png";
  115. editorElement.contentWindow.postMessage(JSON.stringify({ method: "hideNotes" }), "*");
  116. } else {
  117. toggleNotesButton.src = "/src/ui/resources/button_note_visible.png";
  118. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayNotes" }), "*");
  119. }
  120. };
  121. toggleHighlightsButton.onmouseup = () => {
  122. if (toggleHighlightsButton.getAttribute("src") == "/src/ui/resources/button_highlighter_visible.png") {
  123. toggleHighlightsButton.src = "/src/ui/resources/button_highlighter_hidden.png";
  124. editorElement.contentWindow.postMessage(JSON.stringify({ method: "hideHighlights" }), "*");
  125. } else {
  126. displayHighlights();
  127. }
  128. };
  129. removeHighlightButton.onmouseup = () => {
  130. if (toolbarElement.classList.contains("cut-inner-mode")) {
  131. disableCutInnerPage();
  132. }
  133. if (toolbarElement.classList.contains("cut-outer-mode")) {
  134. disableCutOuterPage();
  135. }
  136. if (removeHighlightButton.classList.contains("remove-highlight-disabled")) {
  137. removeHighlightButton.classList.remove("remove-highlight-disabled");
  138. toolbarElement.classList.add("remove-highlight-mode");
  139. resetHighlightButtons();
  140. displayHighlights();
  141. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableRemoveHighlights" }), "*");
  142. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayHighlights" }), "*");
  143. } else {
  144. disableRemoveHighlights();
  145. }
  146. };
  147. editPageButton.onmouseup = () => {
  148. if (toolbarElement.classList.contains("cut-inner-mode")) {
  149. disableCutInnerPage();
  150. }
  151. if (toolbarElement.classList.contains("cut-outer-mode")) {
  152. disableCutOuterPage();
  153. }
  154. if (editPageButton.classList.contains("edit-disabled")) {
  155. enableEditPage();
  156. } else {
  157. disableEditPage();
  158. }
  159. };
  160. formatPageButton.onmouseup = () => {
  161. if (formatPageButton.classList.contains("format-disabled")) {
  162. formatPage();
  163. } else {
  164. cancelFormatPage();
  165. }
  166. };
  167. cutInnerPageButton.onmouseup = () => {
  168. if (toolbarElement.classList.contains("edit-mode")) {
  169. disableEditPage();
  170. }
  171. if (toolbarElement.classList.contains("cut-outer-mode")) {
  172. disableCutOuterPage();
  173. }
  174. if (cutInnerPageButton.classList.contains("cut-disabled")) {
  175. enableCutInnerPage();
  176. } else {
  177. disableCutInnerPage();
  178. }
  179. };
  180. cutOuterPageButton.onmouseup = () => {
  181. if (toolbarElement.classList.contains("edit-mode")) {
  182. disableEditPage();
  183. }
  184. if (toolbarElement.classList.contains("cut-inner-mode")) {
  185. disableCutInnerPage();
  186. }
  187. if (cutOuterPageButton.classList.contains("cut-disabled")) {
  188. enableCutOuterPage();
  189. } else {
  190. disableCutOuterPage();
  191. }
  192. };
  193. undoCutPageButton.onmouseup = () => {
  194. editorElement.contentWindow.postMessage(JSON.stringify({ method: "undoCutPage" }), "*");
  195. };
  196. undoAllCutPageButton.onmouseup = () => {
  197. editorElement.contentWindow.postMessage(JSON.stringify({ method: "undoAllCutPage" }), "*");
  198. };
  199. redoCutPageButton.onmouseup = () => {
  200. editorElement.contentWindow.postMessage(JSON.stringify({ method: "redoCutPage" }), "*");
  201. };
  202. savePageButton.onmouseup = () => {
  203. savePage();
  204. };
  205. if (typeof print == "function") {
  206. printPageButton.onmouseup = () => {
  207. editorElement.contentWindow.postMessage(JSON.stringify({ method: "printPage" }), "*");
  208. };
  209. } else {
  210. printPageButton.remove();
  211. }
  212. let toolbarPositionPointer, toolbarMoving, toolbarTranslateMax;
  213. let orientationPortrait = matchMedia("(orientation: portrait)").matches;
  214. let toolbarTranslate = 0;
  215. toolbarElement.ondragstart = event => event.preventDefault();
  216. toolbarElement.ontouchstart = toolbarOnTouchStart;
  217. toolbarElement.onmousedown = toolbarOnTouchStart;
  218. toolbarElement.ontouchmove = toolbarOnTouchMove;
  219. toolbarElement.ontouchend = toolbarOnTouchEnd;
  220. function viewportSizeChange() {
  221. orientationPortrait = matchMedia("(orientation: portrait)").matches;
  222. toolbarElement.style.setProperty("transform", orientationPortrait ? `translate(0, ${toolbarTranslate}px)` : `translate(${toolbarTranslate}px, 0)`);
  223. }
  224. function toolbarOnTouchStart(event) {
  225. const position = getPosition(event);
  226. toolbarPositionPointer = (orientationPortrait ? position.pageY : position.pageX) - toolbarTranslate;
  227. toolbarTranslateMax = (orientationPortrait ? -lastButton.getBoundingClientRect().top : -lastButton.getBoundingClientRect().left) + toolbarTranslate;
  228. }
  229. function toolbarOnTouchMove(event) {
  230. if (toolbarPositionPointer != null && (event.buttons === undefined || event.buttons == 1)) {
  231. const position = getPosition(event);
  232. const lastToolbarTranslate = toolbarTranslate;
  233. let newToolbarTranslate = (orientationPortrait ? position.pageY : position.pageX) - toolbarPositionPointer;
  234. if (newToolbarTranslate > 0) {
  235. newToolbarTranslate = 0;
  236. }
  237. if (newToolbarTranslate < toolbarTranslateMax) {
  238. newToolbarTranslate = toolbarTranslateMax;
  239. }
  240. if (Math.abs(lastToolbarTranslate - newToolbarTranslate) > (toolbarMoving ? 1 : 8)) {
  241. toolbarTranslate = newToolbarTranslate;
  242. const newTransform = orientationPortrait ? `translate(0px, ${toolbarTranslate}px)` : `translate(${toolbarTranslate}px, 0px)`;
  243. toolbarMoving = true;
  244. toolbarElement.style.setProperty("transform", newTransform);
  245. editorElement.style.setProperty("pointer-events", "none");
  246. event.preventDefault();
  247. }
  248. }
  249. }
  250. function toolbarOnTouchEnd(event) {
  251. if (toolbarMoving) {
  252. editorElement.style.removeProperty("pointer-events");
  253. event.preventDefault();
  254. event.stopPropagation();
  255. }
  256. toolbarPositionPointer = null;
  257. toolbarMoving = false;
  258. }
  259. let updatedResources = {};
  260. addEventListener("resize", viewportSizeChange);
  261. addEventListener("message", event => {
  262. const message = JSON.parse(event.data);
  263. if (message.method == "setContent") {
  264. tabData.options.openEditor = false;
  265. tabData.options.openSavedPage = false;
  266. if (message.compressContent) {
  267. tabData.options.compressContent = true;
  268. if (tabData.selfExtractingArchive !== undefined) {
  269. tabData.options.selfExtractingArchive = tabData.selfExtractingArchive;
  270. }
  271. if (tabData.extractDataFromPageTags !== undefined) {
  272. tabData.options.extractDataFromPage = tabData.extractDataFromPageTags;
  273. }
  274. if (tabData.insertTextBody !== undefined) {
  275. tabData.options.insertTextBody = tabData.insertTextBody;
  276. }
  277. if (tabData.embeddedImage !== undefined) {
  278. tabData.options.embeddedImage = tabData.embeddedImage;
  279. }
  280. if (tabData.insertMetaCSP !== undefined) {
  281. tabData.options.insertMetaCSP = tabData.insertMetaCSP;
  282. }
  283. getContentPageData(tabData.content, message.content, { password: tabData.options.password })
  284. .then(pageData => {
  285. pageData.content = message.content;
  286. pageData.title = message.title;
  287. pageData.doctype = message.doctype;
  288. pageData.viewport = message.viewport;
  289. pageData.url = message.url;
  290. pageData.filename = message.filename || tabData.filename;
  291. pageData.mimeType = "text/html";
  292. if (message.foregroundSave) {
  293. tabData.options.backgroundSave = false;
  294. tabData.options.foregroundSave = true;
  295. }
  296. return download.downloadPage(pageData, tabData.options);
  297. });
  298. } else {
  299. const pageData = {
  300. content: message.content,
  301. filename: message.filename || tabData.filename,
  302. mimeType: "text/html"
  303. };
  304. tabData.options.compressContent = false;
  305. download.downloadPage(pageData, tabData.options);
  306. }
  307. }
  308. if (message.method == "onUpdate") {
  309. tabData.docSaved = message.saved;
  310. }
  311. if (message.method == "onInit") {
  312. tabData.options.disableFormatPage = !message.formatPageEnabled;
  313. formatPageButton.hidden = !message.formatPageEnabled;
  314. document.title = "[SingleFile] " + message.title;
  315. if (message.filename) {
  316. tabData.filename = message.filename;
  317. }
  318. if (message.icon) {
  319. document.querySelectorAll("head > link[rel=icon]").forEach(element => element.remove());
  320. const linkElement = document.createElement("link");
  321. linkElement.rel = "icon";
  322. linkElement.href = message.icon;
  323. document.head.appendChild(linkElement);
  324. }
  325. if (tabData.options.displayInfobarInEditor) {
  326. displayInfobar();
  327. }
  328. tabData.docSaved = true;
  329. if (!message.reset) {
  330. const defaultEditorMode = tabData.options.defaultEditorMode;
  331. if (defaultEditorMode == "edit") {
  332. enableEditPage();
  333. } else if (defaultEditorMode == "format" && !tabData.options.disableFormatPage) {
  334. formatPage();
  335. } else if (defaultEditorMode == "cut") {
  336. enableCutInnerPage();
  337. } else if (defaultEditorMode == "cut-external") {
  338. enableCutOuterPage();
  339. }
  340. }
  341. }
  342. if (message.method == "onError") {
  343. browser.runtime.sendMessage({ method: "ui.processError", error: message.error });
  344. onError(message.error);
  345. }
  346. if (message.method == "savePage") {
  347. savePage();
  348. }
  349. if (message.method == "displayInfobar") {
  350. const doc = new DOMParser().parseFromString(message.content, "text/html");
  351. deserializeShadowRoots(doc.body);
  352. const infobarElement = doc.querySelector(INFOBAR_TAGNAME);
  353. infobarElement.shadowRoot.querySelector("style").textContent += ".infobar { position: absolute; }";
  354. document.querySelector(".editor-container").appendChild(infobarElement);
  355. }
  356. });
  357. browser.runtime.onMessage.addListener(message => {
  358. if (message.method == "devtools.resourceCommitted") {
  359. updatedResources[message.url] = { content: message.content, type: message.type, encoding: message.encoding };
  360. return Promise.resolve({});
  361. }
  362. if (message.method == "content.save") {
  363. tabData.options = message.options;
  364. savePage();
  365. browser.runtime.sendMessage({ method: "ui.processInit" });
  366. return Promise.resolve({});
  367. }
  368. if (message.method == "editor.setTabData") {
  369. if (message.truncated) {
  370. tabDataContents.push(message.content);
  371. } else {
  372. tabDataContents = [message.content];
  373. }
  374. if (!message.truncated || message.finished) {
  375. tabData = JSON.parse(tabDataContents.join(""));
  376. tabData.options = message.options;
  377. tabDataContents = [];
  378. editorElement.contentWindow.postMessage(JSON.stringify({ method: "init", content: tabData.content, password: tabData.options.password, compressContent: message.compressContent }), "*");
  379. editorElement.contentWindow.focus();
  380. setInterval(() => browser.runtime.sendMessage({ method: "ping" }), 15000);
  381. }
  382. return Promise.resolve({});
  383. }
  384. if (message.method == "options.refresh") {
  385. return refreshOptions(message.profileName);
  386. }
  387. if (message.method == "content.error") {
  388. onError(message.error, message.link);
  389. }
  390. if (message.method == "content.download") {
  391. return downloadContent(message);
  392. }
  393. });
  394. addEventListener("load", () => {
  395. browser.runtime.sendMessage({ method: "editor.getTabData" });
  396. });
  397. addEventListener("beforeunload", event => {
  398. if (tabData.options.warnUnsavedPage && !tabData.docSaved) {
  399. event.preventDefault();
  400. event.returnValue = "";
  401. }
  402. });
  403. async function downloadContent(message) {
  404. if (!downloadParser) {
  405. downloadParser = yabson.getParser();
  406. }
  407. const result = await downloadParser.next(message.data);
  408. if (result.done) {
  409. downloadParser = null;
  410. if (result.value.foregroundSave || result.value.sharePage) {
  411. editorElement.contentWindow.postMessage(JSON.stringify({
  412. method: "download",
  413. filename: result.value.filename,
  414. content: Array.from(new Uint8Array(result.value.content)),
  415. mimeType: result.value.mimeType,
  416. sharePage: result.value.sharePage
  417. }), "*");
  418. } else {
  419. const link = document.createElement("a");
  420. link.download = result.value.filename;
  421. link.href = URL.createObjectURL(new Blob([result.value.content], { type: result.value.mimeType }));
  422. link.dispatchEvent(new MouseEvent("click"));
  423. URL.revokeObjectURL(link.href);
  424. }
  425. return browser.runtime.sendMessage({ method: "downloads.end", taskId: result.value.taskId }).then(() => ({}));
  426. } else {
  427. return Promise.resolve({});
  428. }
  429. }
  430. async function refreshOptions(profileName) {
  431. const profiles = await browser.runtime.sendMessage({ method: "config.getProfiles" });
  432. tabData.options = profiles[profileName];
  433. }
  434. function disableEditPage() {
  435. editPageButton.classList.add("edit-disabled");
  436. toolbarElement.classList.remove("edit-mode");
  437. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableEditPage" }), "*");
  438. }
  439. function disableCutInnerPage() {
  440. cutInnerPageButton.classList.add("cut-disabled");
  441. toolbarElement.classList.remove("cut-inner-mode");
  442. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableCutInnerPage" }), "*");
  443. }
  444. function disableCutOuterPage() {
  445. cutOuterPageButton.classList.add("cut-disabled");
  446. toolbarElement.classList.remove("cut-outer-mode");
  447. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableCutOuterPage" }), "*");
  448. }
  449. function resetHighlightButtons() {
  450. highlightButtons.forEach(highlightButton => highlightButton.classList.add("highlight-disabled"));
  451. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableHighlight" }), "*");
  452. }
  453. function disableRemoveHighlights() {
  454. toolbarElement.classList.remove("remove-highlight-mode");
  455. removeHighlightButton.classList.add("remove-highlight-disabled");
  456. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableRemoveHighlights" }), "*");
  457. }
  458. function displayHighlights() {
  459. toggleHighlightsButton.src = "/src/ui/resources/button_highlighter_visible.png";
  460. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayHighlights" }), "*");
  461. }
  462. function enableEditPage() {
  463. editPageButton.classList.remove("edit-disabled");
  464. toolbarElement.classList.add("edit-mode");
  465. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableEditPage" }), "*");
  466. }
  467. function formatPage() {
  468. formatPageButton.classList.remove("format-disabled");
  469. updatedResources = {};
  470. editorElement.contentWindow.postMessage(JSON.stringify({
  471. method: "formatPage",
  472. applySystemTheme: tabData.options.applySystemTheme
  473. }), "*");
  474. }
  475. function cancelFormatPage() {
  476. formatPageButton.classList.add("format-disabled");
  477. updatedResources = {};
  478. editorElement.contentWindow.postMessage(JSON.stringify({ method: "cancelFormatPage" }), "*");
  479. }
  480. function enableCutInnerPage() {
  481. cutInnerPageButton.classList.remove("cut-disabled");
  482. toolbarElement.classList.add("cut-inner-mode");
  483. resetHighlightButtons();
  484. disableRemoveHighlights();
  485. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableCutInnerPage" }), "*");
  486. }
  487. function enableCutOuterPage() {
  488. cutOuterPageButton.classList.remove("cut-disabled");
  489. toolbarElement.classList.add("cut-outer-mode");
  490. resetHighlightButtons();
  491. disableRemoveHighlights();
  492. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableCutOuterPage" }), "*");
  493. }
  494. function savePage() {
  495. editorElement.contentWindow.postMessage(JSON.stringify({
  496. method: "getContent",
  497. compressHTML: tabData.options.compressHTML,
  498. includeInfobar: tabData.options.includeInfobar,
  499. openInfobar: tabData.options.openInfobar,
  500. backgroundSave: tabData.options.backgroundSave,
  501. updatedResources,
  502. filename: tabData.filename,
  503. foregroundSave: FOREGROUND_SAVE,
  504. sharePage: tabData.options.sharePage,
  505. labels: {
  506. EMBEDDED_IMAGE_BUTTON_MESSAGE,
  507. SHARE_PAGE_BUTTON_MESSAGE,
  508. SHARE_SELECTION_BUTTON_MESSAGE,
  509. ERROR_TITLE_MESSAGE
  510. }
  511. }), "*");
  512. }
  513. function displayInfobar() {
  514. editorElement.contentWindow.postMessage(JSON.stringify({
  515. method: "displayInfobar",
  516. openInfobar: tabData.options.openInfobar
  517. }), "*");
  518. }
  519. function getPosition(event) {
  520. if (event.touches && event.touches.length) {
  521. const touch = event.touches[0];
  522. return touch;
  523. } else {
  524. return event;
  525. }
  526. }
  527. async function getContentPageData(zipContent, page, options) {
  528. zip.configure({ workerScripts: { inflate: ["/lib/single-file-z-worker.js"] } });
  529. const zipReader = new zip.ZipReader(new zip.Uint8ArrayReader(new Uint8Array(zipContent)));
  530. const entries = await zipReader.getEntries();
  531. const resources = [];
  532. await Promise.all(entries.map(async entry => {
  533. let data;
  534. if (!options.password && entry.bitFlag.encrypted) {
  535. options.password = prompt("Please enter the password to view the page");
  536. }
  537. if (entry.filename.match(/^([0-9_]+\/)?index.html$/)) {
  538. data = page;
  539. } else {
  540. if (entry.filename.endsWith(".html")) {
  541. data = await entry.getData(new zip.TextWriter(), options);
  542. } else {
  543. data = await entry.getData(new zip.Uint8ArrayWriter(), options);
  544. }
  545. }
  546. const extensionMatch = entry.filename.match(/\.([^.]+)/);
  547. resources.push({
  548. filename: entry.filename.match(/^([0-9_]+\/)?(.*)$/)[2],
  549. extension: extensionMatch && extensionMatch[1],
  550. content: data,
  551. url: entry.comment
  552. });
  553. }));
  554. return getPageData(resources);
  555. }
  556. function getPageData(resources) {
  557. const pageData = JSON.parse(JSON.stringify(EMPTY_PAGE_DATA));
  558. for (const resource of resources) {
  559. const resourcePageData = getPageDataResource(resource, "", pageData);
  560. const filename = resource.filename.substring(resourcePageData.prefixPath.length);
  561. resource.name = filename;
  562. if (filename.startsWith("images/")) {
  563. resourcePageData.resources.images.push(resource);
  564. }
  565. if (filename.startsWith("fonts/")) {
  566. resourcePageData.resources.fonts.push(resource);
  567. }
  568. if (filename.startsWith("scripts/")) {
  569. resourcePageData.resources.scripts.push(resource);
  570. }
  571. if (filename.endsWith(".css")) {
  572. resourcePageData.resources.stylesheets.push(resource);
  573. }
  574. if (filename.endsWith(".html")) {
  575. resourcePageData.content = resource.content;
  576. }
  577. }
  578. return pageData;
  579. }
  580. const EMPTY_PAGE_DATA = {
  581. name: "",
  582. prefixPath: "",
  583. resources: {
  584. stylesheets: [],
  585. images: [],
  586. fonts: [],
  587. scripts: [],
  588. frames: []
  589. }
  590. };
  591. function getPageDataResource(resource, prefixPath = "", pageData) {
  592. const filename = resource.filename.substring(prefixPath.length);
  593. resource.name = filename;
  594. if (filename.startsWith("frames/")) {
  595. const framesIndex = Number(filename.match(/^frames\/(\d+)\//)[1]);
  596. const framePath = "frames/" + framesIndex + "/";
  597. if (!pageData.resources.frames[framesIndex]) {
  598. pageData.resources.frames[framesIndex] = Object.assign(JSON.parse(JSON.stringify(EMPTY_PAGE_DATA)), {
  599. name: framePath,
  600. prefixPath: prefixPath + framePath
  601. });
  602. }
  603. return getPageDataResource(resource, prefixPath + framePath, pageData.resources.frames[framesIndex]);
  604. } else {
  605. return pageData;
  606. }
  607. }
  608. function deserializeShadowRoots(node) {
  609. node.querySelectorAll(`template[${SHADOWROOT_ATTRIBUTE_NAME}]`).forEach(element => {
  610. if (element.parentElement) {
  611. let shadowRoot;
  612. try {
  613. shadowRoot = element.parentElement.attachShadow({ mode: "open" });
  614. const contentDocument = (new DOMParser()).parseFromString(element.innerHTML, "text/html");
  615. Array.from(contentDocument.head.childNodes).forEach(node => shadowRoot.appendChild(node));
  616. Array.from(contentDocument.body.childNodes).forEach(node => shadowRoot.appendChild(node));
  617. } catch (error) {
  618. // ignored
  619. }
  620. if (shadowRoot) {
  621. deserializeShadowRoots(shadowRoot);
  622. element.remove();
  623. }
  624. }
  625. });
  626. }