ui-editor.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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, prompt, matchMedia, addEventListener, innerHeight, innerWidth */
  24. import * as download from "../../core/common/download.js";
  25. import { onError } from "./../common/content-error.js";
  26. const editorElement = document.querySelector(".editor");
  27. const toolbarElement = document.querySelector(".toolbar");
  28. const highlightYellowButton = document.querySelector(".highlight-yellow-button");
  29. const highlightPinkButton = document.querySelector(".highlight-pink-button");
  30. const highlightBlueButton = document.querySelector(".highlight-blue-button");
  31. const highlightGreenButton = document.querySelector(".highlight-green-button");
  32. const highlightButtons = Array.from(document.querySelectorAll(".highlight-button"));
  33. const toggleNotesButton = document.querySelector(".toggle-notes-button");
  34. const toggleHighlightsButton = document.querySelector(".toggle-highlights-button");
  35. const removeHighlightButton = document.querySelector(".remove-highlight-button");
  36. const addYellowNoteButton = document.querySelector(".add-note-yellow-button");
  37. const addPinkNoteButton = document.querySelector(".add-note-pink-button");
  38. const addBlueNoteButton = document.querySelector(".add-note-blue-button");
  39. const addGreenNoteButton = document.querySelector(".add-note-green-button");
  40. const editPageButton = document.querySelector(".edit-page-button");
  41. const formatPageButton = document.querySelector(".format-page-button");
  42. const cutInnerPageButton = document.querySelector(".cut-inner-page-button");
  43. const cutOuterPageButton = document.querySelector(".cut-outer-page-button");
  44. const undoCutPageButton = document.querySelector(".undo-cut-page-button");
  45. const undoAllCutPageButton = document.querySelector(".undo-all-cut-page-button");
  46. const redoCutPageButton = document.querySelector(".redo-cut-page-button");
  47. const savePageButton = document.querySelector(".save-page-button");
  48. const printPageButton = document.querySelector(".print-page-button");
  49. const lastButton = toolbarElement.querySelector(".buttons:last-of-type [type=button]:last-of-type");
  50. let tabData, tabDataContents = [];
  51. addYellowNoteButton.title = browser.i18n.getMessage("editorAddYellowNote");
  52. addPinkNoteButton.title = browser.i18n.getMessage("editorAddPinkNote");
  53. addBlueNoteButton.title = browser.i18n.getMessage("editorAddBlueNote");
  54. addGreenNoteButton.title = browser.i18n.getMessage("editorAddGreenNote");
  55. highlightYellowButton.title = browser.i18n.getMessage("editorHighlightYellow");
  56. highlightPinkButton.title = browser.i18n.getMessage("editorHighlightPink");
  57. highlightBlueButton.title = browser.i18n.getMessage("editorHighlightBlue");
  58. highlightGreenButton.title = browser.i18n.getMessage("editorHighlightGreen");
  59. toggleNotesButton.title = browser.i18n.getMessage("editorToggleNotes");
  60. toggleHighlightsButton.title = browser.i18n.getMessage("editorToggleHighlights");
  61. removeHighlightButton.title = browser.i18n.getMessage("editorRemoveHighlight");
  62. editPageButton.title = browser.i18n.getMessage("editorEditPage");
  63. formatPageButton.title = browser.i18n.getMessage("editorFormatPage");
  64. cutInnerPageButton.title = browser.i18n.getMessage("editorCutInnerPage");
  65. cutOuterPageButton.title = browser.i18n.getMessage("editorCutOuterPage");
  66. undoCutPageButton.title = browser.i18n.getMessage("editorUndoCutPage");
  67. undoAllCutPageButton.title = browser.i18n.getMessage("editorUndoAllCutPage");
  68. redoCutPageButton.title = browser.i18n.getMessage("editorRedoCutPage");
  69. savePageButton.title = browser.i18n.getMessage("editorSavePage");
  70. printPageButton.title = browser.i18n.getMessage("editorPrintPage");
  71. addYellowNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-yellow" }), "*");
  72. addPinkNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-pink" }), "*");
  73. addBlueNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-blue" }), "*");
  74. addGreenNoteButton.onmouseup = () => editorElement.contentWindow.postMessage(JSON.stringify({ method: "addNote", color: "note-green" }), "*");
  75. document.addEventListener("mouseup", event => {
  76. editorElement.contentWindow.focus();
  77. toolbarOnTouchEnd(event);
  78. }, true);
  79. document.onmousemove = toolbarOnTouchMove;
  80. highlightButtons.forEach(highlightButton => {
  81. highlightButton.onmouseup = () => {
  82. if (toolbarElement.classList.contains("cut-inner-mode")) {
  83. disableCutInnerPage();
  84. }
  85. if (toolbarElement.classList.contains("cut-outer-mode")) {
  86. disableCutOuterPage();
  87. }
  88. if (toolbarElement.classList.contains("remove-highlight-mode")) {
  89. disableRemoveHighlights();
  90. }
  91. const disabled = highlightButton.classList.contains("highlight-disabled");
  92. resetHighlightButtons();
  93. if (disabled) {
  94. highlightButton.classList.remove("highlight-disabled");
  95. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableHighlight", color: "single-file-highlight-" + highlightButton.dataset.color }), "*");
  96. } else {
  97. highlightButton.classList.add("highlight-disabled");
  98. }
  99. };
  100. });
  101. toggleNotesButton.onmouseup = () => {
  102. if (toggleNotesButton.getAttribute("src") == "/extension/ui/resources/button_note_visible.png") {
  103. toggleNotesButton.src = "/extension/ui/resources/button_note_hidden.png";
  104. editorElement.contentWindow.postMessage(JSON.stringify({ method: "hideNotes" }), "*");
  105. } else {
  106. toggleNotesButton.src = "/extension/ui/resources/button_note_visible.png";
  107. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayNotes" }), "*");
  108. }
  109. };
  110. toggleHighlightsButton.onmouseup = () => {
  111. if (toggleHighlightsButton.getAttribute("src") == "/extension/ui/resources/button_highlighter_visible.png") {
  112. toggleHighlightsButton.src = "/extension/ui/resources/button_highlighter_hidden.png";
  113. editorElement.contentWindow.postMessage(JSON.stringify({ method: "hideHighlights" }), "*");
  114. } else {
  115. displayHighlights();
  116. }
  117. };
  118. removeHighlightButton.onmouseup = () => {
  119. if (toolbarElement.classList.contains("cut-inner-mode")) {
  120. disableCutInnerPage();
  121. }
  122. if (toolbarElement.classList.contains("cut-outer-mode")) {
  123. disableCutOuterPage();
  124. }
  125. if (removeHighlightButton.classList.contains("remove-highlight-disabled")) {
  126. removeHighlightButton.classList.remove("remove-highlight-disabled");
  127. toolbarElement.classList.add("remove-highlight-mode");
  128. resetHighlightButtons();
  129. displayHighlights();
  130. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableRemoveHighlights" }), "*");
  131. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayHighlights" }), "*");
  132. } else {
  133. disableRemoveHighlights();
  134. }
  135. };
  136. editPageButton.onmouseup = () => {
  137. if (toolbarElement.classList.contains("cut-inner-mode")) {
  138. disableCutInnerPage();
  139. }
  140. if (toolbarElement.classList.contains("cut-outer-mode")) {
  141. disableCutOuterPage();
  142. }
  143. if (editPageButton.classList.contains("edit-disabled")) {
  144. enableEditPage();
  145. } else {
  146. disableEditPage();
  147. }
  148. };
  149. formatPageButton.onmouseup = () => {
  150. if (formatPageButton.classList.contains("format-disabled")) {
  151. formatPage();
  152. } else {
  153. cancelFormatPage();
  154. }
  155. };
  156. cutInnerPageButton.onmouseup = () => {
  157. if (toolbarElement.classList.contains("edit-mode")) {
  158. disableEditPage();
  159. }
  160. if (toolbarElement.classList.contains("cut-outer-mode")) {
  161. disableCutOuterPage();
  162. }
  163. if (cutInnerPageButton.classList.contains("cut-disabled")) {
  164. enableCutInnerPage();
  165. } else {
  166. disableCutInnerPage();
  167. }
  168. };
  169. cutOuterPageButton.onmouseup = () => {
  170. if (toolbarElement.classList.contains("edit-mode")) {
  171. disableEditPage();
  172. }
  173. if (toolbarElement.classList.contains("cut-inner-mode")) {
  174. disableCutInnerPage();
  175. }
  176. if (cutOuterPageButton.classList.contains("cut-disabled")) {
  177. enableCutOuterPage();
  178. } else {
  179. disableCutOuterPage();
  180. }
  181. };
  182. undoCutPageButton.onmouseup = () => {
  183. editorElement.contentWindow.postMessage(JSON.stringify({ method: "undoCutPage" }), "*");
  184. };
  185. undoAllCutPageButton.onmouseup = () => {
  186. editorElement.contentWindow.postMessage(JSON.stringify({ method: "undoAllCutPage" }), "*");
  187. };
  188. redoCutPageButton.onmouseup = () => {
  189. editorElement.contentWindow.postMessage(JSON.stringify({ method: "redoCutPage" }), "*");
  190. };
  191. savePageButton.onmouseup = () => {
  192. savePage();
  193. };
  194. if (typeof print == "function") {
  195. printPageButton.onmouseup = () => {
  196. editorElement.contentWindow.postMessage(JSON.stringify({ method: "printPage" }), "*");
  197. };
  198. } else {
  199. printPageButton.remove();
  200. }
  201. let toolbarPositionPointer, toolbarMoving, toolbarTranslateMax;
  202. let orientationPortrait = matchMedia("(orientation: portrait)").matches;
  203. let toolbarTranslate = 0;
  204. toolbarElement.ondragstart = event => event.preventDefault();
  205. toolbarElement.ontouchstart = toolbarOnTouchStart;
  206. toolbarElement.onmousedown = toolbarOnTouchStart;
  207. toolbarElement.ontouchmove = toolbarOnTouchMove;
  208. toolbarElement.ontouchend = toolbarOnTouchEnd;
  209. function viewportSizeChange() {
  210. orientationPortrait = matchMedia("(orientation: portrait)").matches;
  211. toolbarElement.style.setProperty("transform", orientationPortrait ? `translate(0, ${toolbarTranslate}px)` : `translate(${toolbarTranslate}px, 0)`);
  212. }
  213. function toolbarOnTouchStart(event) {
  214. const position = getPosition(event);
  215. toolbarPositionPointer = (orientationPortrait ? position.screenY : position.screenX) - toolbarTranslate;
  216. toolbarTranslateMax = ((orientationPortrait ? -lastButton.getBoundingClientRect().top + toolbarTranslate + innerHeight : -lastButton.getBoundingClientRect().left + toolbarTranslate + innerWidth)) - 35;
  217. }
  218. function toolbarOnTouchMove(event) {
  219. if (toolbarPositionPointer != null) {
  220. const position = getPosition(event);
  221. toolbarTranslate = Math.min(Math.max((orientationPortrait ? position.screenY : position.screenX) - toolbarPositionPointer, toolbarTranslateMax), 0);
  222. if (Math.abs(toolbarTranslate)) {
  223. toolbarMoving = true;
  224. toolbarElement.style.setProperty("transform", orientationPortrait ? `translate(0, ${toolbarTranslate}px)` : `translate(${toolbarTranslate}px, 0)`);
  225. editorElement.style.setProperty("pointer-events", "none");
  226. event.preventDefault();
  227. }
  228. }
  229. }
  230. function toolbarOnTouchEnd(event) {
  231. if (toolbarMoving) {
  232. editorElement.style.removeProperty("pointer-events");
  233. event.preventDefault();
  234. event.stopPropagation();
  235. }
  236. toolbarPositionPointer = null;
  237. toolbarMoving = false;
  238. }
  239. let updatedResources = {};
  240. addEventListener("resize", viewportSizeChange);
  241. addEventListener("message", event => {
  242. const message = JSON.parse(event.data);
  243. if (message.method == "setContent") {
  244. const pageData = {
  245. content: message.content,
  246. filename: tabData.filename
  247. };
  248. tabData.options.openEditor = false;
  249. tabData.options.openSavedPage = false;
  250. download.downloadPage(pageData, tabData.options);
  251. }
  252. if (message.method == "onUpdate") {
  253. tabData.docSaved = message.saved;
  254. }
  255. if (message.method == "onInit") {
  256. tabData.options.disableFormatPage = !message.formatPageEnabled;
  257. formatPageButton.hidden = !message.formatPageEnabled;
  258. document.title = "[SingleFile] " + message.title;
  259. if (message.filename) {
  260. tabData.filename = message.filename;
  261. }
  262. if (message.icon) {
  263. const linkElement = document.createElement("link");
  264. linkElement.rel = "icon";
  265. linkElement.href = message.icon;
  266. document.head.appendChild(linkElement);
  267. }
  268. tabData.docSaved = true;
  269. if (!message.reset) {
  270. const defaultEditorMode = tabData.options.defaultEditorMode;
  271. if (defaultEditorMode == "edit") {
  272. enableEditPage();
  273. } else if (defaultEditorMode == "format" && !tabData.options.disableFormatPage) {
  274. formatPage();
  275. } else if (defaultEditorMode == "cut") {
  276. enableCutInnerPage();
  277. } else if (defaultEditorMode == "cut-external") {
  278. enableCutOuterPage();
  279. }
  280. }
  281. }
  282. if (message.method == "savePage") {
  283. savePage();
  284. }
  285. });
  286. browser.runtime.onMessage.addListener(message => {
  287. if (message.method == "devtools.resourceCommitted") {
  288. updatedResources[message.url] = { content: message.content, type: message.type, encoding: message.encoding };
  289. return Promise.resolve({});
  290. }
  291. if (message.method == "content.save") {
  292. tabData.options = message.options;
  293. savePage();
  294. browser.runtime.sendMessage({ method: "ui.processInit" });
  295. return Promise.resolve({});
  296. }
  297. if (message.method == "common.promptValueRequest") {
  298. browser.runtime.sendMessage({ method: "tabs.promptValueResponse", value: prompt(message.promptMessage) });
  299. return Promise.resolve({});
  300. }
  301. if (message.method == "editor.setTabData") {
  302. if (message.truncated) {
  303. tabDataContents.push(message.content);
  304. } else {
  305. tabDataContents = [message.content];
  306. }
  307. if (!message.truncated || message.finished) {
  308. tabData = JSON.parse(tabDataContents.join(""));
  309. tabData.options = message.options;
  310. tabDataContents = [];
  311. editorElement.contentWindow.postMessage(JSON.stringify({ method: "init", content: tabData.content }), "*");
  312. editorElement.contentWindow.focus();
  313. delete tabData.content;
  314. }
  315. return Promise.resolve({});
  316. }
  317. if (message.method == "options.refresh") {
  318. return refreshOptions(message.profileName);
  319. }
  320. if (message.method == "content.error") {
  321. onError(message.error, message.link);
  322. }
  323. });
  324. addEventListener("load", () => {
  325. browser.runtime.sendMessage({ method: "editor.getTabData" });
  326. });
  327. addEventListener("beforeunload", event => {
  328. if (tabData.options.warnUnsavedPage && !tabData.docSaved) {
  329. event.preventDefault();
  330. event.returnValue = "";
  331. }
  332. });
  333. async function refreshOptions(profileName) {
  334. const profiles = await browser.runtime.sendMessage({ method: "config.getProfiles" });
  335. tabData.options = profiles[profileName];
  336. }
  337. function disableEditPage() {
  338. editPageButton.classList.add("edit-disabled");
  339. toolbarElement.classList.remove("edit-mode");
  340. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableEditPage" }), "*");
  341. }
  342. function disableCutInnerPage() {
  343. cutInnerPageButton.classList.add("cut-disabled");
  344. toolbarElement.classList.remove("cut-inner-mode");
  345. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableCutInnerPage" }), "*");
  346. }
  347. function disableCutOuterPage() {
  348. cutOuterPageButton.classList.add("cut-disabled");
  349. toolbarElement.classList.remove("cut-outer-mode");
  350. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableCutOuterPage" }), "*");
  351. }
  352. function resetHighlightButtons() {
  353. highlightButtons.forEach(highlightButton => highlightButton.classList.add("highlight-disabled"));
  354. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableHighlight" }), "*");
  355. }
  356. function disableRemoveHighlights() {
  357. toolbarElement.classList.remove("remove-highlight-mode");
  358. removeHighlightButton.classList.add("remove-highlight-disabled");
  359. editorElement.contentWindow.postMessage(JSON.stringify({ method: "disableRemoveHighlights" }), "*");
  360. }
  361. function displayHighlights() {
  362. toggleHighlightsButton.src = "/extension/ui/resources/button_highlighter_visible.png";
  363. editorElement.contentWindow.postMessage(JSON.stringify({ method: "displayHighlights" }), "*");
  364. }
  365. function enableEditPage() {
  366. editPageButton.classList.remove("edit-disabled");
  367. toolbarElement.classList.add("edit-mode");
  368. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableEditPage" }), "*");
  369. }
  370. function formatPage() {
  371. formatPageButton.classList.remove("format-disabled");
  372. updatedResources = {};
  373. editorElement.contentWindow.postMessage(JSON.stringify({ method: tabData.options.applySystemTheme ? "formatPage" : "formatPageNoTheme" }), "*");
  374. }
  375. function cancelFormatPage() {
  376. formatPageButton.classList.add("format-disabled");
  377. updatedResources = {};
  378. editorElement.contentWindow.postMessage(JSON.stringify({ method: "cancelFormatPage" }), "*");
  379. }
  380. function enableCutInnerPage() {
  381. cutInnerPageButton.classList.remove("cut-disabled");
  382. toolbarElement.classList.add("cut-inner-mode");
  383. resetHighlightButtons();
  384. disableRemoveHighlights();
  385. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableCutInnerPage" }), "*");
  386. }
  387. function enableCutOuterPage() {
  388. cutOuterPageButton.classList.remove("cut-disabled");
  389. toolbarElement.classList.add("cut-outer-mode");
  390. resetHighlightButtons();
  391. disableRemoveHighlights();
  392. editorElement.contentWindow.postMessage(JSON.stringify({ method: "enableCutOuterPage" }), "*");
  393. }
  394. function savePage() {
  395. editorElement.contentWindow.postMessage(JSON.stringify({ method: "getContent", compressHTML: tabData.options.compressHTML, updatedResources }), "*");
  396. }
  397. function getPosition(event) {
  398. if (event.touches && event.touches.length) {
  399. const touch = event.touches[0];
  400. return touch;
  401. } else {
  402. return event;
  403. }
  404. }