Panel.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /**
  2. * Panel.js
  3. *
  4. * @author realor
  5. */
  6. import { I18N } from '../i18n/I18N.js'
  7. class Panel {
  8. static LARGE_SCREEN_WIDTH = 768
  9. static POSITIONS = ['left', 'right', 'bottom']
  10. static HEADER_HEIGHT = 24
  11. static MARGIN = 6
  12. constructor(application) {
  13. this.application = application
  14. this.panelManager = null
  15. this.minimumHeight = 100 // greater than HEADER_HEIGHT
  16. this.preferredHeight = 0 // 0: all available space, > 0: height in pixels
  17. this.element = document.createElement('div')
  18. this.element.className = 'panel'
  19. this.element.style.margin = Panel.MARGIN + 'px'
  20. this.headerElem = document.createElement('div')
  21. this.headerElem.className = 'header'
  22. this.element.appendChild(this.headerElem)
  23. this.bodyElem = document.createElement('div')
  24. this.bodyElem.className = 'body'
  25. this.element.appendChild(this.bodyElem)
  26. this.minimizeButtonElem = document.createElement('button')
  27. this.minimizeButtonElem.className = 'minimize'
  28. I18N.set(this.minimizeButtonElem, 'aria-label', 'button.minimize')
  29. I18N.set(this.minimizeButtonElem, 'alt', 'button.minimize')
  30. I18N.set(this.minimizeButtonElem, 'title', 'button.minimize')
  31. this.headerElem.appendChild(this.minimizeButtonElem)
  32. this.maximizeButtonElem = document.createElement('button')
  33. this.maximizeButtonElem.className = 'maximize'
  34. I18N.set(this.maximizeButtonElem, 'aria-label', 'button.maximize')
  35. I18N.set(this.maximizeButtonElem, 'alt', 'button.maximize')
  36. I18N.set(this.maximizeButtonElem, 'title', 'button.maximize')
  37. this.headerElem.appendChild(this.maximizeButtonElem)
  38. this.titleElem = document.createElement('a')
  39. this.titleElem.className = 'title'
  40. this.titleElem.href = '#'
  41. this.headerElem.appendChild(this.titleElem)
  42. this.closeButtonElem = document.createElement('button')
  43. this.closeButtonElem.className = 'close'
  44. I18N.set(this.closeButtonElem, 'aria-label', 'button.close')
  45. I18N.set(this.closeButtonElem, 'alt', 'button.close')
  46. I18N.set(this.closeButtonElem, 'title', 'button.close')
  47. this.headerElem.appendChild(this.closeButtonElem)
  48. this.titleElem.addEventListener(
  49. 'click',
  50. event => {
  51. event.preventDefault()
  52. this.zoom()
  53. },
  54. false
  55. )
  56. this.minimizeButtonElem.addEventListener('click', event => (this.minimized = true), false)
  57. this.maximizeButtonElem.addEventListener('click', event => (this.minimized = false), false)
  58. this.closeButtonElem.addEventListener('click', event => (this.visible = false), false)
  59. this._position = null
  60. this.position = 'left'
  61. this.opacity = application.panelOpacity
  62. }
  63. get title() {
  64. return this.titleElem.innerHTML
  65. }
  66. set title(title) {
  67. this.titleElem.innerHTML = title
  68. I18N.set(this.titleElem, 'innerHTML', title)
  69. }
  70. get position() {
  71. return this._position
  72. }
  73. set position(position) {
  74. if (position !== this._position) {
  75. var element = this.element
  76. if (this._position) {
  77. element.classList.remove(this._position)
  78. }
  79. this._position = position
  80. element.classList.add(position)
  81. if (this.panelManager) this.panelManager.updateLayout()
  82. }
  83. }
  84. get visible() {
  85. return this.element.classList.contains('show')
  86. }
  87. set visible(visible) {
  88. if (this.panelManager) this.panelManager.setAnimationEnabled(false)
  89. let prevVisible = this.visible
  90. if (visible && !prevVisible) {
  91. this.element.classList.add('show')
  92. if (this.panelManager) {
  93. this.element.classList.remove('minimized')
  94. this.panelManager.preferredPanel = this
  95. this.panelManager.updateLayout()
  96. }
  97. this.onShow()
  98. } else if (!visible && prevVisible) {
  99. this.element.classList.remove('show')
  100. if (this.panelManager) this.panelManager.updateLayout()
  101. this.onHide()
  102. }
  103. }
  104. get minimized() {
  105. return this.element.classList.contains('minimized')
  106. }
  107. set minimized(minimized) {
  108. if (this.panelManager) {
  109. this.panelManager.setAnimationEnabled(true)
  110. if (minimized === false) {
  111. this.panelManager.preferredPanel = this
  112. }
  113. }
  114. let prevMinimized = this.minimized
  115. if (minimized && !prevMinimized) {
  116. this.element.classList.add('minimized')
  117. if (this.panelManager) this.panelManager.updateLayout()
  118. this.onMinimize()
  119. } else if (!minimized && prevMinimized) {
  120. this.element.classList.remove('minimized')
  121. if (this.panelManager) this.panelManager.updateLayout()
  122. this.onMaximize()
  123. }
  124. }
  125. setTitle(title) {
  126. this.title = title
  127. return this
  128. }
  129. setPosition(position) {
  130. this.position = position
  131. return this
  132. }
  133. setClassName(className) {
  134. this.element.classList.add(className)
  135. return this
  136. }
  137. zoom() {
  138. if (this.panelManager) {
  139. let position = this.panelManager.isLargeScreen() ? this.position : null
  140. let panels = this.panelManager.getPanels(position, true)
  141. for (let panel of panels) {
  142. if (panel !== this) {
  143. panel.element.classList.add('minimized')
  144. }
  145. }
  146. this.element.classList.remove('minimized')
  147. this.panelManager.setAnimationEnabled(true)
  148. this.panelManager.updateLayout()
  149. }
  150. }
  151. get opacity() {
  152. return this._opacity
  153. }
  154. set opacity(opacity) {
  155. this._opacity = opacity
  156. this.element.style.background = 'rgba(255,255,255,' + opacity + ')'
  157. let headerOpacity = Math.min(opacity + 0.2, 1)
  158. this.headerElem.style.background = 'rgba(255,255,255,' + headerOpacity + ')'
  159. }
  160. onShow() {}
  161. onHide() {}
  162. onMinimize() {}
  163. onMaximize() {}
  164. }
  165. class PanelManager {
  166. constructor(container) {
  167. this.container = container || document.body
  168. this.margin = 0
  169. this.headerHeight = Panel.HEADER_HEIGHT
  170. this.panels = []
  171. this.preferredPanel = null
  172. this.resizers = {}
  173. this.resizers.left = new PanelResizer(this, 'left')
  174. this.resizers.right = new PanelResizer(this, 'right')
  175. window.addEventListener(
  176. 'resize',
  177. event => {
  178. this.setAnimationEnabled(false)
  179. this.updateLayout()
  180. },
  181. false
  182. )
  183. }
  184. addPanel(panel) {
  185. let index = this.panels.indexOf(panel)
  186. if (index === -1) {
  187. panel.panelManager = this
  188. this.panels.push(panel)
  189. this.container.appendChild(panel.element)
  190. }
  191. }
  192. removePanel(panel) {
  193. let index = this.panels.indexOf(panel)
  194. if (index !== -1) {
  195. panel.panelManager = null
  196. this.panels.splice(index, 1)
  197. this.container.removeChild(panel.element)
  198. }
  199. }
  200. getPanels(position = null, visible = null) {
  201. let selection = []
  202. for (let i = 0; i < this.panels.length; i++) {
  203. let panel = this.panels[i]
  204. if (position === null || panel.position === position) {
  205. if (visible === null || panel.visible === visible) {
  206. selection.push(panel)
  207. }
  208. }
  209. }
  210. return selection
  211. }
  212. isAnimationEnabled() {
  213. return this.container.classList.contains('animate')
  214. }
  215. setAnimationEnabled(enabled) {
  216. if (enabled) {
  217. this.container.classList.add('animate')
  218. } else {
  219. this.container.classList.remove('animate')
  220. }
  221. }
  222. updateLayout() {
  223. const container = this.container
  224. const height = container.clientHeight - Panel.MARGIN - 1
  225. if (this.isLargeScreen()) {
  226. this.resizers.left.enabled = true
  227. this.resizers.right.enabled = true
  228. let positions = Panel.POSITIONS
  229. for (var i = 0; i < positions.length; i++) {
  230. let position = positions[i]
  231. let panels = this.getPanels(position, true)
  232. let maxHeight = this.layoutElements(panels, height)
  233. let resizer = this.resizers[position]
  234. if (resizer) {
  235. resizer.height = maxHeight
  236. resizer.updateBar()
  237. resizer.saveWidth()
  238. }
  239. }
  240. } else {
  241. this.resizers.left.enabled = false
  242. this.resizers.right.enabled = false
  243. let panels = this.getPanels(null, true)
  244. this.layoutElements(panels, Math.floor(height / 2))
  245. }
  246. }
  247. layoutElements(panels, height) {
  248. let minimumHeight = 0
  249. // calculate minimumHeight
  250. for (let panel of panels) {
  251. if (panel.minimized) {
  252. minimumHeight += this.headerHeight
  253. } else {
  254. minimumHeight += panel.minimumHeight
  255. }
  256. minimumHeight += Panel.MARGIN
  257. }
  258. // minimize required panels to fit all
  259. let total = panels.length
  260. let remainingHeight = height - minimumHeight
  261. let i = 0
  262. while (remainingHeight < 0 && i < total) {
  263. let j = total - i - 1
  264. let panel = panels[j]
  265. if (!panel.minimized && panel !== this.preferredPanel) {
  266. panel.element.classList.add('minimized')
  267. remainingHeight += panel.minimumHeight - this.headerHeight
  268. }
  269. i++
  270. }
  271. // calculate largePanelExtraHeight
  272. let largePanelCount = 0
  273. let smallPanelRequiredHeight = 0
  274. for (let panel of panels) {
  275. if (!panel.minimized) {
  276. if (panel.preferredHeight === 0) {
  277. largePanelCount++
  278. } else if (panel.preferredHeight > panel.minimumHeight) {
  279. smallPanelRequiredHeight += panel.preferredHeight - panel.minimumHeight
  280. }
  281. }
  282. }
  283. let largePanelExtraHeight = 0
  284. let largePanelRemainingHeight = remainingHeight - smallPanelRequiredHeight
  285. if (largePanelRemainingHeight > 0 && largePanelCount > 0) {
  286. largePanelExtraHeight = Math.floor(largePanelRemainingHeight / largePanelCount)
  287. }
  288. // layout panels
  289. let bottom = this.margin
  290. for (let i = 0; i < total; i++) {
  291. let j = total - i - 1
  292. let panel = panels[j]
  293. // set panel bottom
  294. panel.element.style.bottom = bottom + 'px'
  295. // set panel width
  296. if (this.isLargeScreen()) {
  297. let resizer = this.resizers[panel.position]
  298. if (resizer) {
  299. panel.element.style.width = resizer.width - Panel.MARGIN + 'px'
  300. }
  301. } else {
  302. panel.element.style.width = ''
  303. }
  304. // set panel height
  305. let currentPanelHeight
  306. if (panel.minimized) {
  307. currentPanelHeight = this.headerHeight
  308. } else if (panel.preferredHeight === 0) {
  309. // large panel
  310. currentPanelHeight = panel.minimumHeight + largePanelExtraHeight
  311. remainingHeight -= largePanelExtraHeight
  312. } // small panel
  313. else {
  314. currentPanelHeight = panel.minimumHeight
  315. let requiredHeight = panel.preferredHeight - panel.minimumHeight
  316. if (requiredHeight > 0) {
  317. let extraHeight = Math.min(remainingHeight, requiredHeight)
  318. currentPanelHeight += extraHeight
  319. remainingHeight -= extraHeight
  320. }
  321. }
  322. panel.element.style.height = currentPanelHeight + 'px'
  323. bottom += currentPanelHeight + Panel.MARGIN
  324. }
  325. return bottom
  326. }
  327. isLargeScreen() {
  328. return this.container.clientWidth > Panel.LARGE_SCREEN_WIDTH
  329. }
  330. }
  331. class PanelResizer {
  332. constructor(panelManager, side) {
  333. this.panelManager = panelManager
  334. this.side = side
  335. this.height = 0
  336. this.width = 0
  337. this.element = document.createElement('div')
  338. const element = this.element
  339. const container = panelManager.container
  340. element.className = 'resizer'
  341. container.appendChild(element)
  342. this.restoreWidth()
  343. this.updateBar()
  344. const move = event => {
  345. this.width = this.getCurrentWidth(event)
  346. this.updateBar()
  347. this.panelManager.updateLayout()
  348. }
  349. const reset = event => {
  350. element.removeEventListener('pointermove', move, false)
  351. element.removeEventListener('pointerup', reset, false)
  352. element.releasePointerCapture(event.pointerId)
  353. }
  354. element.addEventListener('pointerdown', event => {
  355. element.addEventListener('pointermove', move, false)
  356. element.addEventListener('pointerup', reset, false)
  357. element.setPointerCapture(event.pointerId)
  358. })
  359. }
  360. updateBar() {
  361. this.element.style.height = this.height + 'px'
  362. this.element.style[this.side] = this.width + 'px'
  363. }
  364. get enabled() {
  365. return this.element.style.display === ''
  366. }
  367. set enabled(enabled) {
  368. this.element.style.display = enabled ? '' : 'none'
  369. }
  370. restoreWidth() {
  371. let value = window.localStorage.getItem('bimrocket.resizer.' + this.side)
  372. this.width = parseInt(value) || 250
  373. }
  374. saveWidth() {
  375. window.localStorage.setItem('bimrocket.resizer.' + this.side, this.width)
  376. }
  377. getCurrentWidth(event) {
  378. const rect = this.panelManager.container.getBoundingClientRect()
  379. let curWidth = 0
  380. if (this.side === 'left') {
  381. curWidth = event.clientX - rect.left
  382. } else if (this.side === 'right') {
  383. curWidth = rect.left + rect.width - event.clientX
  384. }
  385. return curWidth
  386. }
  387. }
  388. export { Panel, PanelManager }