1
0

ui-editor.js 17 KB

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