Menu.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * Menu.js
  3. *
  4. * @author realor
  5. */
  6. import { I18N } from '../i18n/I18N.js'
  7. class AbstractMenuItem {
  8. constructor(menuBar, label) {
  9. this.label = label
  10. this.menuBar = menuBar
  11. this.parentMenu = null
  12. this.itemElement = document.createElement('li')
  13. this.anchorElement = document.createElement('a')
  14. this.anchorElement.href = '#'
  15. this.labelElement = document.createElement('div')
  16. this.labelElement.className = 'label'
  17. this.anchorElement.appendChild(this.labelElement)
  18. I18N.set(this.labelElement, 'innerHTML', label || 'menuitem')
  19. this.itemElement.appendChild(this.anchorElement)
  20. this.anchorElement.addEventListener('pointerenter', () => this.anchorElement.focus())
  21. }
  22. }
  23. class MenuItem extends AbstractMenuItem {
  24. constructor(menuBar, tool) {
  25. super(menuBar, tool.label)
  26. this.anchorElement.addEventListener('click', () => {
  27. if (this.menuBar.isVertical()) {
  28. this.menuBar.hide()
  29. } else {
  30. this.menuBar.focusMenuItem(null)
  31. }
  32. this.menuBar.application.useTool(tool)
  33. })
  34. this.anchorElement.addEventListener('focusin', () => {
  35. this.menuBar.focusMenuItem(this)
  36. })
  37. const keyShortcut = tool.keyShortcut
  38. if (keyShortcut) {
  39. this.menuBar.keyShortcuts.set(keyShortcut, tool)
  40. this.anchorElement.setAttribute('aria-keyshortcuts', keyShortcut)
  41. this.keyElement = document.createElement('div')
  42. this.keyElement.innerHTML = keyShortcut
  43. this.keyElement.className = 'shortcut'
  44. this.anchorElement.appendChild(this.keyElement)
  45. }
  46. }
  47. }
  48. class Menu extends AbstractMenuItem {
  49. constructor(menuBar, label) {
  50. super(menuBar, label)
  51. this.menuItems = [] // child menuItems
  52. this.listElement = document.createElement('ul')
  53. this.itemElement.appendChild(this.listElement)
  54. this.anchorElement.className = 'menu'
  55. this.anchorElement.addEventListener('click', event => {
  56. event.preventDefault()
  57. if (this.isVisible() && this.menuBar.isVertical()) {
  58. this.hide()
  59. } else {
  60. this.menuBar.armed = true
  61. this.drop()
  62. }
  63. })
  64. this.anchorElement.addEventListener('contextmenu', event => event.preventDefault())
  65. this.anchorElement.addEventListener('focusin', () => {
  66. menuBar.focusMenuItem(this)
  67. if (menuBar.armed && !menuBar.isVertical()) {
  68. this.drop()
  69. }
  70. })
  71. }
  72. isVisible() {
  73. return this.itemElement.className === 'drop'
  74. }
  75. drop() {
  76. this.itemElement.className = 'drop'
  77. }
  78. hide(recursive) {
  79. this.itemElement.className = 'hide'
  80. if (recursive) {
  81. for (let menuItem of this.menuItems) {
  82. if (menuItem instanceof Menu) {
  83. menuItem.hide(recursive)
  84. }
  85. }
  86. }
  87. }
  88. addMenuItem(tool) {
  89. let menuItem = new MenuItem(this.menuBar, tool)
  90. menuItem.parentMenu = this
  91. this.menuItems.push(menuItem)
  92. this.listElement.appendChild(menuItem.itemElement)
  93. return menuItem
  94. }
  95. addMenu(label, index) {
  96. let menu = new Menu(this.menuBar, label)
  97. menu.parentMenu = this
  98. const children = this.listElement.children
  99. if (typeof index === 'number' && index < children.length) {
  100. if (index < 0) index = 0
  101. let oldElem = children[index]
  102. this.listElement.insertBefore(menu.itemElement, oldElem)
  103. this.menuItems.splice(index, 0, menu)
  104. } else {
  105. this.menuItems.push(menu)
  106. this.listElement.appendChild(menu.itemElement)
  107. }
  108. return menu
  109. }
  110. }
  111. /*** MenuBar ***/
  112. class MenuBar {
  113. constructor(application, element) {
  114. this.application = application
  115. this.menuItem = null
  116. this.menus = []
  117. this.armed = false
  118. this.keyShortcuts = new Map()
  119. this.navElement = document.createElement('nav')
  120. element.appendChild(this.navElement)
  121. this.listElement = document.createElement('ul')
  122. this.navElement.appendChild(this.listElement)
  123. const menuBar = this
  124. this.dropButtonElement = document.createElement('a')
  125. I18N.set(this.dropButtonElement, 'innerHTML', 'button.menu_show')
  126. this.dropButtonElement.className = 'menu_button'
  127. this.dropButtonElement.setAttribute('role', 'button')
  128. this.dropButtonElement.setAttribute('aria-pressed', 'false')
  129. this.dropButtonElement.addEventListener('click', () => {
  130. if (this.isVisible()) {
  131. menuBar.hide()
  132. } else {
  133. menuBar.drop()
  134. }
  135. })
  136. element.appendChild(this.dropButtonElement)
  137. document.body.addEventListener(
  138. 'pointerdown',
  139. event => {
  140. if ((this.isVertical() && this.isVisible()) || (!this.isVertical() && this.armed)) {
  141. // click outside root menu element ?
  142. const rootMenuElement = this.navElement.parentElement
  143. let element = event.srcElement
  144. while (element !== null && element !== rootMenuElement) {
  145. element = element.parentElement
  146. }
  147. if (element === null) {
  148. // click outside menu, hide menu
  149. event.preventDefault()
  150. if (this.isVertical()) {
  151. this.hide()
  152. } else {
  153. this.hideAllMenus()
  154. }
  155. }
  156. }
  157. },
  158. true
  159. )
  160. document.addEventListener('keydown', event => {
  161. this.processKey(event)
  162. })
  163. window.addEventListener('resize', () => this.hideAllMenus(), false)
  164. window.addEventListener(
  165. 'keyup',
  166. event => {
  167. if (this.armed && event.keyCode === 27) {
  168. this.hideAllMenus()
  169. }
  170. },
  171. false
  172. )
  173. }
  174. addMenu(label, index) {
  175. const menu = new Menu(this, label)
  176. const children = this.listElement.children
  177. if (typeof index === 'number' && index < children.length) {
  178. if (index < 0) index = 0
  179. let oldElem = children[index]
  180. this.listElement.insertBefore(menu.itemElement, oldElem)
  181. this.menus.splice(index, 0, menu)
  182. } else {
  183. this.listElement.appendChild(menu.itemElement)
  184. this.menus.push(menu)
  185. }
  186. return menu
  187. }
  188. isVisible() {
  189. return this.listElement.className === 'menu_drop'
  190. }
  191. drop() {
  192. this.listElement.className = 'menu_drop'
  193. I18N.set(this.dropButtonElement, 'innerHTML', 'button.menu_hide')
  194. this.application.i18n.update(this.dropButtonElement)
  195. this.dropButtonElement.setAttribute('aria-pressed', 'true')
  196. }
  197. hide() {
  198. this.listElement.className = 'menu_hide'
  199. I18N.set(this.dropButtonElement, 'innerHTML', 'button.menu_show')
  200. this.application.i18n.update(this.dropButtonElement)
  201. this.dropButtonElement.setAttribute('aria-pressed', 'false')
  202. this.armed = false
  203. }
  204. isVertical() {
  205. return document.body.clientWidth < 950
  206. }
  207. focusMenuItem(menuItem) {
  208. if (!this.isVertical()) {
  209. let menu
  210. if (this.menuItem) {
  211. // have previous menuItem
  212. menu = this.menuItem.parentMenu
  213. if (menu) {
  214. for (let i = 0; i < menu.menuItems.length; i++) {
  215. let sibling = menu.menuItems[i]
  216. if (sibling instanceof Menu) {
  217. sibling.hide() // hide sibling menu
  218. }
  219. }
  220. do {
  221. menu.hide() // hide parent menus
  222. menu = menu.parentMenu
  223. } while (menu)
  224. } else {
  225. this.menuItem.hide()
  226. }
  227. }
  228. if (menuItem) {
  229. menu = menuItem.parentMenu
  230. while (menu) {
  231. menu.drop() // show parent menu
  232. menu = menu.parentMenu
  233. }
  234. }
  235. }
  236. this.menuItem = menuItem
  237. if (menuItem === null) {
  238. this.armed = false
  239. }
  240. }
  241. hideAllMenus() {
  242. for (var i = 0; i < this.menus.length; i++) {
  243. let menu = this.menus[i]
  244. menu.hide(true)
  245. }
  246. this.menuItem = null
  247. this.armed = false
  248. }
  249. processKey(event) {
  250. if (event.srcElement.nodeName === 'INPUT') return
  251. let keys = []
  252. if (event.altKey) keys.push('Alt')
  253. if (event.ctrlKey) keys.push('Control')
  254. if (event.shiftKey) keys.push('Shift')
  255. let key = event.key
  256. if (key !== 'Alt' && key !== 'Control' && key !== 'Shift') {
  257. if (key.length === 1) key = key.toUpperCase()
  258. keys.push(key)
  259. }
  260. let keyShortcut = keys.join('+')
  261. let tool = this.keyShortcuts.get(keyShortcut)
  262. if (tool) {
  263. this.application.useTool(tool)
  264. event.preventDefault()
  265. }
  266. }
  267. }
  268. export { MenuBar, Menu, MenuItem }