pdf_sidebar_resizer.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /* Copyright 2017 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 { docStyle } from "./ui_utils.js";
  16. const SIDEBAR_WIDTH_VAR = "--sidebar-width";
  17. const SIDEBAR_MIN_WIDTH = 200; // pixels
  18. const SIDEBAR_RESIZING_CLASS = "sidebarResizing";
  19. /**
  20. * @typedef {Object} PDFSidebarResizerOptions
  21. * @property {HTMLDivElement} outerContainer - The outer container
  22. * (encasing both the viewer and sidebar elements).
  23. * @property {HTMLDivElement} resizer - The DOM element that can be dragged in
  24. * order to adjust the width of the sidebar.
  25. */
  26. class PDFSidebarResizer {
  27. /**
  28. * @param {PDFSidebarResizerOptions} options
  29. * @param {EventBus} eventBus - The application event bus.
  30. * @param {IL10n} l10n - Localization service.
  31. */
  32. constructor(options, eventBus, l10n) {
  33. this.isRTL = false;
  34. this.sidebarOpen = false;
  35. this._width = null;
  36. this._outerContainerWidth = null;
  37. this._boundEvents = Object.create(null);
  38. this.outerContainer = options.outerContainer;
  39. this.resizer = options.resizer;
  40. this.eventBus = eventBus;
  41. l10n.getDirection().then(dir => {
  42. this.isRTL = dir === "rtl";
  43. });
  44. this._addEventListeners();
  45. }
  46. /**
  47. * @type {number}
  48. */
  49. get outerContainerWidth() {
  50. return (this._outerContainerWidth ||= this.outerContainer.clientWidth);
  51. }
  52. /**
  53. * @private
  54. * returns {boolean} Indicating if the sidebar width was updated.
  55. */
  56. _updateWidth(width = 0) {
  57. // Prevent the sidebar from becoming too narrow, or from occupying more
  58. // than half of the available viewer width.
  59. const maxWidth = Math.floor(this.outerContainerWidth / 2);
  60. if (width > maxWidth) {
  61. width = maxWidth;
  62. }
  63. if (width < SIDEBAR_MIN_WIDTH) {
  64. width = SIDEBAR_MIN_WIDTH;
  65. }
  66. // Only update the UI when the sidebar width did in fact change.
  67. if (width === this._width) {
  68. return false;
  69. }
  70. this._width = width;
  71. docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
  72. return true;
  73. }
  74. /**
  75. * @private
  76. */
  77. _mouseMove(evt) {
  78. let width = evt.clientX;
  79. // For sidebar resizing to work correctly in RTL mode, invert the width.
  80. if (this.isRTL) {
  81. width = this.outerContainerWidth - width;
  82. }
  83. this._updateWidth(width);
  84. }
  85. /**
  86. * @private
  87. */
  88. _mouseUp(evt) {
  89. // Re-enable the `transition-duration` rules when sidebar resizing ends...
  90. this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
  91. // ... and ensure that rendering will always be triggered.
  92. this.eventBus.dispatch("resize", { source: this });
  93. const _boundEvents = this._boundEvents;
  94. window.removeEventListener("mousemove", _boundEvents.mouseMove);
  95. window.removeEventListener("mouseup", _boundEvents.mouseUp);
  96. }
  97. /**
  98. * @private
  99. */
  100. _addEventListeners() {
  101. const _boundEvents = this._boundEvents;
  102. _boundEvents.mouseMove = this._mouseMove.bind(this);
  103. _boundEvents.mouseUp = this._mouseUp.bind(this);
  104. this.resizer.addEventListener("mousedown", evt => {
  105. if (evt.button !== 0) {
  106. return;
  107. }
  108. // Disable the `transition-duration` rules when sidebar resizing begins,
  109. // in order to improve responsiveness and to avoid visual glitches.
  110. this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
  111. window.addEventListener("mousemove", _boundEvents.mouseMove);
  112. window.addEventListener("mouseup", _boundEvents.mouseUp);
  113. });
  114. this.eventBus._on("sidebarviewchanged", evt => {
  115. this.sidebarOpen = !!evt?.view;
  116. });
  117. this.eventBus._on("resize", evt => {
  118. // When the *entire* viewer is resized, such that it becomes narrower,
  119. // ensure that the sidebar doesn't end up being too wide.
  120. if (evt?.source !== window) {
  121. return;
  122. }
  123. // Always reset the cached width when the viewer is resized.
  124. this._outerContainerWidth = null;
  125. if (!this._width) {
  126. // The sidebar hasn't been resized, hence no need to adjust its width.
  127. return;
  128. }
  129. // NOTE: If the sidebar is closed, we don't need to worry about
  130. // visual glitches nor ensure that rendering is triggered.
  131. if (!this.sidebarOpen) {
  132. this._updateWidth(this._width);
  133. return;
  134. }
  135. this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
  136. const updated = this._updateWidth(this._width);
  137. Promise.resolve().then(() => {
  138. this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
  139. // Trigger rendering if the sidebar width changed, to avoid
  140. // depending on the order in which 'resize' events are handled.
  141. if (updated) {
  142. this.eventBus.dispatch("resize", { source: this });
  143. }
  144. });
  145. });
  146. }
  147. }
  148. export { PDFSidebarResizer };