pdf_sidebar.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /* Copyright 2016 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import {
  16. PresentationModeState,
  17. RenderingStates,
  18. SidebarView,
  19. } from "./ui_utils.js";
  20. const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
  21. /**
  22. * @typedef {Object} PDFSidebarOptions
  23. * @property {PDFSidebarElements} elements - The DOM elements.
  24. * @property {PDFViewer} pdfViewer - The document viewer.
  25. * @property {PDFThumbnailViewer} pdfThumbnailViewer - The thumbnail viewer.
  26. * @property {EventBus} eventBus - The application event bus.
  27. * @property {IL10n} l10n - The localization service.
  28. */
  29. /**
  30. * @typedef {Object} PDFSidebarElements
  31. * @property {HTMLDivElement} outerContainer - The outer container
  32. * (encasing both the viewer and sidebar elements).
  33. * @property {HTMLDivElement} sidebarContainer - The sidebar container
  34. * (in which the views are placed).
  35. * @property {HTMLButtonElement} toggleButton - The button used for
  36. * opening/closing the sidebar.
  37. * @property {HTMLButtonElement} thumbnailButton - The button used to show
  38. * the thumbnail view.
  39. * @property {HTMLButtonElement} outlineButton - The button used to show
  40. * the outline view.
  41. * @property {HTMLButtonElement} attachmentsButton - The button used to show
  42. * the attachments view.
  43. * @property {HTMLButtonElement} layersButton - The button used to show
  44. * the layers view.
  45. * @property {HTMLDivElement} thumbnailView - The container in which
  46. * the thumbnails are placed.
  47. * @property {HTMLDivElement} outlineView - The container in which
  48. * the outline is placed.
  49. * @property {HTMLDivElement} attachmentsView - The container in which
  50. * the attachments are placed.
  51. * @property {HTMLDivElement} layersView - The container in which
  52. * the layers are placed.
  53. * @property {HTMLDivElement} outlineOptionsContainer - The container in which
  54. * the outline view-specific option button(s) are placed.
  55. * @property {HTMLButtonElement} currentOutlineItemButton - The button used to
  56. * find the current outline item.
  57. */
  58. class PDFSidebar {
  59. /**
  60. * @param {PDFSidebarOptions} options
  61. */
  62. constructor({ elements, pdfViewer, pdfThumbnailViewer, eventBus, l10n }) {
  63. this.isOpen = false;
  64. this.active = SidebarView.THUMBS;
  65. this.isInitialViewSet = false;
  66. this.isInitialEventDispatched = false;
  67. /**
  68. * Callback used when the sidebar has been opened/closed, to ensure that
  69. * the viewers (PDFViewer/PDFThumbnailViewer) are updated correctly.
  70. */
  71. this.onToggled = null;
  72. this.pdfViewer = pdfViewer;
  73. this.pdfThumbnailViewer = pdfThumbnailViewer;
  74. this.outerContainer = elements.outerContainer;
  75. this.sidebarContainer = elements.sidebarContainer;
  76. this.toggleButton = elements.toggleButton;
  77. this.thumbnailButton = elements.thumbnailButton;
  78. this.outlineButton = elements.outlineButton;
  79. this.attachmentsButton = elements.attachmentsButton;
  80. this.layersButton = elements.layersButton;
  81. this.thumbnailView = elements.thumbnailView;
  82. this.outlineView = elements.outlineView;
  83. this.attachmentsView = elements.attachmentsView;
  84. this.layersView = elements.layersView;
  85. this._outlineOptionsContainer = elements.outlineOptionsContainer;
  86. this._currentOutlineItemButton = elements.currentOutlineItemButton;
  87. this.eventBus = eventBus;
  88. this.l10n = l10n;
  89. this.#addEventListeners();
  90. }
  91. reset() {
  92. this.isInitialViewSet = false;
  93. this.isInitialEventDispatched = false;
  94. this.#hideUINotification(/* reset = */ true);
  95. this.switchView(SidebarView.THUMBS);
  96. this.outlineButton.disabled = false;
  97. this.attachmentsButton.disabled = false;
  98. this.layersButton.disabled = false;
  99. this._currentOutlineItemButton.disabled = true;
  100. }
  101. /**
  102. * @type {number} One of the values in {SidebarView}.
  103. */
  104. get visibleView() {
  105. return this.isOpen ? this.active : SidebarView.NONE;
  106. }
  107. /**
  108. * @param {number} view - The sidebar view that should become visible,
  109. * must be one of the values in {SidebarView}.
  110. */
  111. setInitialView(view = SidebarView.NONE) {
  112. if (this.isInitialViewSet) {
  113. return;
  114. }
  115. this.isInitialViewSet = true;
  116. // If the user has already manually opened the sidebar, immediately closing
  117. // it would be bad UX; also ignore the "unknown" sidebar view value.
  118. if (view === SidebarView.NONE || view === SidebarView.UNKNOWN) {
  119. this.#dispatchEvent();
  120. return;
  121. }
  122. this.switchView(view, /* forceOpen = */ true);
  123. // Prevent dispatching two back-to-back "sidebarviewchanged" events,
  124. // since `this.switchView` dispatched the event if the view changed.
  125. if (!this.isInitialEventDispatched) {
  126. this.#dispatchEvent();
  127. }
  128. }
  129. /**
  130. * @param {number} view - The sidebar view that should be switched to,
  131. * must be one of the values in {SidebarView}.
  132. * @param {boolean} [forceOpen] - Ensure that the sidebar is open.
  133. * The default value is `false`.
  134. */
  135. switchView(view, forceOpen = false) {
  136. const isViewChanged = view !== this.active;
  137. let shouldForceRendering = false;
  138. switch (view) {
  139. case SidebarView.NONE:
  140. if (this.isOpen) {
  141. this.close();
  142. }
  143. return; // Closing will trigger rendering and dispatch the event.
  144. case SidebarView.THUMBS:
  145. if (this.isOpen && isViewChanged) {
  146. shouldForceRendering = true;
  147. }
  148. break;
  149. case SidebarView.OUTLINE:
  150. if (this.outlineButton.disabled) {
  151. return;
  152. }
  153. break;
  154. case SidebarView.ATTACHMENTS:
  155. if (this.attachmentsButton.disabled) {
  156. return;
  157. }
  158. break;
  159. case SidebarView.LAYERS:
  160. if (this.layersButton.disabled) {
  161. return;
  162. }
  163. break;
  164. default:
  165. console.error(`PDFSidebar.switchView: "${view}" is not a valid view.`);
  166. return;
  167. }
  168. // Update the active view *after* it has been validated above,
  169. // in order to prevent setting it to an invalid state.
  170. this.active = view;
  171. const isThumbs = view === SidebarView.THUMBS,
  172. isOutline = view === SidebarView.OUTLINE,
  173. isAttachments = view === SidebarView.ATTACHMENTS,
  174. isLayers = view === SidebarView.LAYERS;
  175. // Update the CSS classes (and aria attributes), for all buttons...
  176. this.thumbnailButton.classList.toggle("toggled", isThumbs);
  177. this.outlineButton.classList.toggle("toggled", isOutline);
  178. this.attachmentsButton.classList.toggle("toggled", isAttachments);
  179. this.layersButton.classList.toggle("toggled", isLayers);
  180. this.thumbnailButton.setAttribute("aria-checked", isThumbs);
  181. this.outlineButton.setAttribute("aria-checked", isOutline);
  182. this.attachmentsButton.setAttribute("aria-checked", isAttachments);
  183. this.layersButton.setAttribute("aria-checked", isLayers);
  184. // ... and for all views.
  185. this.thumbnailView.classList.toggle("hidden", !isThumbs);
  186. this.outlineView.classList.toggle("hidden", !isOutline);
  187. this.attachmentsView.classList.toggle("hidden", !isAttachments);
  188. this.layersView.classList.toggle("hidden", !isLayers);
  189. // Finally, update view-specific CSS classes.
  190. this._outlineOptionsContainer.classList.toggle("hidden", !isOutline);
  191. if (forceOpen && !this.isOpen) {
  192. this.open();
  193. return; // Opening will trigger rendering and dispatch the event.
  194. }
  195. if (shouldForceRendering) {
  196. this.#updateThumbnailViewer();
  197. this.#forceRendering();
  198. }
  199. if (isViewChanged) {
  200. this.#dispatchEvent();
  201. }
  202. }
  203. open() {
  204. if (this.isOpen) {
  205. return;
  206. }
  207. this.isOpen = true;
  208. this.toggleButton.classList.add("toggled");
  209. this.toggleButton.setAttribute("aria-expanded", "true");
  210. this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");
  211. if (this.active === SidebarView.THUMBS) {
  212. this.#updateThumbnailViewer();
  213. }
  214. this.#forceRendering();
  215. this.#dispatchEvent();
  216. this.#hideUINotification();
  217. }
  218. close() {
  219. if (!this.isOpen) {
  220. return;
  221. }
  222. this.isOpen = false;
  223. this.toggleButton.classList.remove("toggled");
  224. this.toggleButton.setAttribute("aria-expanded", "false");
  225. this.outerContainer.classList.add("sidebarMoving");
  226. this.outerContainer.classList.remove("sidebarOpen");
  227. this.#forceRendering();
  228. this.#dispatchEvent();
  229. }
  230. toggle() {
  231. if (this.isOpen) {
  232. this.close();
  233. } else {
  234. this.open();
  235. }
  236. }
  237. #dispatchEvent() {
  238. if (this.isInitialViewSet && !this.isInitialEventDispatched) {
  239. this.isInitialEventDispatched = true;
  240. }
  241. this.eventBus.dispatch("sidebarviewchanged", {
  242. source: this,
  243. view: this.visibleView,
  244. });
  245. }
  246. #forceRendering() {
  247. if (this.onToggled) {
  248. this.onToggled();
  249. } else {
  250. // Fallback
  251. this.pdfViewer.forceRendering();
  252. this.pdfThumbnailViewer.forceRendering();
  253. }
  254. }
  255. #updateThumbnailViewer() {
  256. const { pdfViewer, pdfThumbnailViewer } = this;
  257. // Use the rendered pages to set the corresponding thumbnail images.
  258. const pagesCount = pdfViewer.pagesCount;
  259. for (let pageIndex = 0; pageIndex < pagesCount; pageIndex++) {
  260. const pageView = pdfViewer.getPageView(pageIndex);
  261. if (pageView?.renderingState === RenderingStates.FINISHED) {
  262. const thumbnailView = pdfThumbnailViewer.getThumbnail(pageIndex);
  263. thumbnailView.setImage(pageView);
  264. }
  265. }
  266. pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);
  267. }
  268. #showUINotification() {
  269. this.toggleButton.setAttribute(
  270. "data-l10n-id",
  271. "toggle_sidebar_notification2"
  272. );
  273. this.l10n.translate(this.toggleButton);
  274. if (!this.isOpen) {
  275. // Only show the notification on the `toggleButton` if the sidebar is
  276. // currently closed, to avoid unnecessarily bothering the user.
  277. this.toggleButton.classList.add(UI_NOTIFICATION_CLASS);
  278. }
  279. }
  280. #hideUINotification(reset = false) {
  281. if (this.isOpen || reset) {
  282. // Only hide the notification on the `toggleButton` if the sidebar is
  283. // currently open, or when the current PDF document is being closed.
  284. this.toggleButton.classList.remove(UI_NOTIFICATION_CLASS);
  285. }
  286. if (reset) {
  287. this.toggleButton.setAttribute("data-l10n-id", "toggle_sidebar");
  288. this.l10n.translate(this.toggleButton);
  289. }
  290. }
  291. #addEventListeners() {
  292. this.sidebarContainer.addEventListener("transitionend", evt => {
  293. if (evt.target === this.sidebarContainer) {
  294. this.outerContainer.classList.remove("sidebarMoving");
  295. }
  296. });
  297. this.toggleButton.addEventListener("click", () => {
  298. this.toggle();
  299. });
  300. // Buttons for switching views.
  301. this.thumbnailButton.addEventListener("click", () => {
  302. this.switchView(SidebarView.THUMBS);
  303. });
  304. this.outlineButton.addEventListener("click", () => {
  305. this.switchView(SidebarView.OUTLINE);
  306. });
  307. this.outlineButton.addEventListener("dblclick", () => {
  308. this.eventBus.dispatch("toggleoutlinetree", { source: this });
  309. });
  310. this.attachmentsButton.addEventListener("click", () => {
  311. this.switchView(SidebarView.ATTACHMENTS);
  312. });
  313. this.layersButton.addEventListener("click", () => {
  314. this.switchView(SidebarView.LAYERS);
  315. });
  316. this.layersButton.addEventListener("dblclick", () => {
  317. this.eventBus.dispatch("resetlayers", { source: this });
  318. });
  319. // Buttons for view-specific options.
  320. this._currentOutlineItemButton.addEventListener("click", () => {
  321. this.eventBus.dispatch("currentoutlineitem", { source: this });
  322. });
  323. // Disable/enable views.
  324. const onTreeLoaded = (count, button, view) => {
  325. button.disabled = !count;
  326. if (count) {
  327. this.#showUINotification();
  328. } else if (this.active === view) {
  329. // If the `view` was opened by the user during document load,
  330. // switch away from it if it turns out to be empty.
  331. this.switchView(SidebarView.THUMBS);
  332. }
  333. };
  334. this.eventBus._on("outlineloaded", evt => {
  335. onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
  336. evt.currentOutlineItemPromise.then(enabled => {
  337. if (!this.isInitialViewSet) {
  338. return;
  339. }
  340. this._currentOutlineItemButton.disabled = !enabled;
  341. });
  342. });
  343. this.eventBus._on("attachmentsloaded", evt => {
  344. onTreeLoaded(
  345. evt.attachmentsCount,
  346. this.attachmentsButton,
  347. SidebarView.ATTACHMENTS
  348. );
  349. });
  350. this.eventBus._on("layersloaded", evt => {
  351. onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
  352. });
  353. // Update the thumbnailViewer, if visible, when exiting presentation mode.
  354. this.eventBus._on("presentationmodechanged", evt => {
  355. if (
  356. evt.state === PresentationModeState.NORMAL &&
  357. this.visibleView === SidebarView.THUMBS
  358. ) {
  359. this.#updateThumbnailViewer();
  360. }
  361. });
  362. }
  363. }
  364. export { PDFSidebar };