You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

init.js 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /* globals Snap */
  2. /**
  3. * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
  4. *
  5. * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. import _ from 'underscore'
  23. import $ from 'jquery'
  24. import moment from 'moment'
  25. import cssVars from 'css-vars-ponyfill'
  26. import { initSessionHeartBeat } from './session-heartbeat'
  27. import OC from './OC/index'
  28. import { setUp as setUpContactsMenu } from './components/ContactsMenu'
  29. import { setUp as setUpMainMenu } from './components/MainMenu'
  30. import { setUp as setUpUserMenu } from './components/UserMenu'
  31. import PasswordConfirmation from './OC/password-confirmation'
  32. // keep in sync with core/css/variables.scss
  33. const breakpointMobileWidth = 1024
  34. const resizeMenu = () => {
  35. const appList = $('#appmenu li')
  36. const rightHeaderWidth = $('.header-right').outerWidth()
  37. const headerWidth = $('header').outerWidth()
  38. const usePercentualAppMenuLimit = 0.33
  39. const minAppsDesktop = 8
  40. let availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210)
  41. const isMobile = $(window).width() < breakpointMobileWidth
  42. if (!isMobile) {
  43. availableWidth = availableWidth * usePercentualAppMenuLimit
  44. }
  45. let appCount = Math.floor((availableWidth / $(appList).width()))
  46. if (isMobile && appCount > minAppsDesktop) {
  47. appCount = minAppsDesktop
  48. }
  49. if (!isMobile && appCount < minAppsDesktop) {
  50. appCount = minAppsDesktop
  51. }
  52. // show at least 2 apps in the popover
  53. if (appList.length - 1 - appCount >= 1) {
  54. appCount--
  55. }
  56. $('#more-apps a').removeClass('active')
  57. let lastShownApp
  58. for (let k = 0; k < appList.length - 1; k++) {
  59. const name = $(appList[k]).data('id')
  60. if (k < appCount) {
  61. $(appList[k]).removeClass('hidden')
  62. $('#apps li[data-id=' + name + ']').addClass('in-header')
  63. lastShownApp = appList[k]
  64. } else {
  65. $(appList[k]).addClass('hidden')
  66. $('#apps li[data-id=' + name + ']').removeClass('in-header')
  67. // move active app to last position if it is active
  68. if (appCount > 0 && $(appList[k]).children('a').hasClass('active')) {
  69. $(lastShownApp).addClass('hidden')
  70. $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header')
  71. $(appList[k]).removeClass('hidden')
  72. $('#apps li[data-id=' + name + ']').addClass('in-header')
  73. }
  74. }
  75. }
  76. // show/hide more apps icon
  77. if ($('#apps li:not(.in-header)').length === 0) {
  78. $('#more-apps').hide()
  79. $('#navigation').hide()
  80. } else {
  81. $('#more-apps').show()
  82. }
  83. }
  84. const initLiveTimestamps = () => {
  85. // Update live timestamps every 30 seconds
  86. setInterval(() => {
  87. $('.live-relative-timestamp').each(function() {
  88. $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10)))
  89. })
  90. }, 30 * 1000)
  91. }
  92. /**
  93. * Initializes core
  94. */
  95. export const initCore = () => {
  96. /**
  97. * Set users locale to moment.js as soon as possible
  98. */
  99. moment.locale(OC.getLocale())
  100. const userAgent = window.navigator.userAgent
  101. const msie = userAgent.indexOf('MSIE ')
  102. const trident = userAgent.indexOf('Trident/')
  103. const edge = userAgent.indexOf('Edge/')
  104. if (msie > 0 || trident > 0) {
  105. // (IE 10 or older) || IE 11
  106. $('html').addClass('ie')
  107. } else if (edge > 0) {
  108. // for edge
  109. $('html').addClass('edge')
  110. }
  111. // css variables fallback for IE
  112. if (msie > 0 || trident > 0 || edge > 0) {
  113. console.info('Legacy browser detected, applying css vars polyfill')
  114. cssVars({
  115. watch: true,
  116. // set edge < 16 as incompatible
  117. onlyLegacy: !(/Edge\/([0-9]{2})\./i.test(navigator.userAgent)
  118. && parseInt(/Edge\/([0-9]{2})\./i.exec(navigator.userAgent)[1]) < 16),
  119. })
  120. }
  121. $(window).on('unload.main', () => { OC._unloadCalled = true })
  122. $(window).on('beforeunload.main', () => {
  123. // super-trick thanks to http://stackoverflow.com/a/4651049
  124. // in case another handler displays a confirmation dialog (ex: navigating away
  125. // during an upload), there are two possible outcomes: user clicked "ok" or
  126. // "cancel"
  127. // first timeout handler is called after unload dialog is closed
  128. setTimeout(() => {
  129. OC._userIsNavigatingAway = true
  130. // second timeout event is only called if user cancelled (Chrome),
  131. // but in other browsers it might still be triggered, so need to
  132. // set a higher delay...
  133. setTimeout(() => {
  134. if (!OC._unloadCalled) {
  135. OC._userIsNavigatingAway = false
  136. }
  137. }, 10000)
  138. }, 1)
  139. })
  140. $(document).on('ajaxError.main', function(event, request, settings) {
  141. if (settings && settings.allowAuthErrors) {
  142. return
  143. }
  144. OC._processAjaxError(request)
  145. })
  146. initSessionHeartBeat()
  147. OC.registerMenu($('#expand'), $('#expanddiv'), false, true)
  148. // toggle for menus
  149. $(document).on('mouseup.closemenus', event => {
  150. const $el = $(event.target)
  151. if ($el.closest('.menu').length || $el.closest('.menutoggle').length) {
  152. // don't close when clicking on the menu directly or a menu toggle
  153. return false
  154. }
  155. OC.hideMenus()
  156. })
  157. setUpMainMenu()
  158. setUpUserMenu()
  159. setUpContactsMenu()
  160. // move triangle of apps dropdown to align with app name triangle
  161. // 2 is the additional offset between the triangles
  162. if ($('#navigation').length) {
  163. $('#header #nextcloud + .menutoggle').on('click', () => {
  164. $('#menu-css-helper').remove()
  165. const caretPosition = $('.header-appname + .icon-caret').offset().left - 2
  166. if (caretPosition > 255) {
  167. // if the app name is longer than the menu, just put the triangle in the middle
  168. } else {
  169. $('head').append('<style id="menu-css-helper">#navigation:after { left: ' + caretPosition + 'px }</style>')
  170. }
  171. })
  172. $('#header #appmenu .menutoggle').on('click', () => {
  173. $('#appmenu').toggleClass('menu-open')
  174. if ($('#appmenu').is(':visible')) {
  175. $('#menu-css-helper').remove()
  176. }
  177. })
  178. }
  179. $(window).resize(resizeMenu)
  180. setTimeout(resizeMenu, 0)
  181. // just add snapper for logged in users
  182. // and if the app doesn't handle the nav slider itself
  183. if ($('#app-navigation').length && !$('html').hasClass('lte9')
  184. && !$('#app-content').hasClass('no-snapper')) {
  185. // App sidebar on mobile
  186. const snapper = new Snap({
  187. element: document.getElementById('app-content'),
  188. disable: 'right',
  189. maxPosition: 300, // $navigation-width
  190. minDragDistance: 100,
  191. })
  192. $('#app-content').prepend('<div id="app-navigation-toggle" class="icon-menu" style="display:none" tabindex="0"></div>')
  193. const toggleSnapperOnButton = () => {
  194. if (snapper.state().state === 'left') {
  195. snapper.close()
  196. } else {
  197. snapper.open('left')
  198. }
  199. }
  200. $('#app-navigation-toggle').click(toggleSnapperOnButton)
  201. $('#app-navigation-toggle').keypress(e => {
  202. if (e.which === 13) {
  203. toggleSnapperOnButton()
  204. }
  205. })
  206. // close sidebar when switching navigation entry
  207. const $appNavigation = $('#app-navigation')
  208. $appNavigation.delegate('a, :button', 'click', event => {
  209. const $target = $(event.target)
  210. // don't hide navigation when changing settings or adding things
  211. if ($target.is('.app-navigation-noclose')
  212. || $target.closest('.app-navigation-noclose').length) {
  213. return
  214. }
  215. if ($target.is('.app-navigation-entry-utils-menu-button')
  216. || $target.closest('.app-navigation-entry-utils-menu-button').length) {
  217. return
  218. }
  219. if ($target.is('.add-new')
  220. || $target.closest('.add-new').length) {
  221. return
  222. }
  223. if ($target.is('#app-settings')
  224. || $target.closest('#app-settings').length) {
  225. return
  226. }
  227. snapper.close()
  228. })
  229. let navigationBarSlideGestureEnabled = false
  230. let navigationBarSlideGestureAllowed = true
  231. let navigationBarSlideGestureEnablePending = false
  232. OC.allowNavigationBarSlideGesture = () => {
  233. navigationBarSlideGestureAllowed = true
  234. if (navigationBarSlideGestureEnablePending) {
  235. snapper.enable()
  236. navigationBarSlideGestureEnabled = true
  237. navigationBarSlideGestureEnablePending = false
  238. }
  239. }
  240. OC.disallowNavigationBarSlideGesture = () => {
  241. navigationBarSlideGestureAllowed = false
  242. if (navigationBarSlideGestureEnabled) {
  243. const endCurrentDrag = true
  244. snapper.disable(endCurrentDrag)
  245. navigationBarSlideGestureEnabled = false
  246. navigationBarSlideGestureEnablePending = true
  247. }
  248. }
  249. const toggleSnapperOnSize = () => {
  250. if ($(window).width() > breakpointMobileWidth) {
  251. snapper.close()
  252. snapper.disable()
  253. navigationBarSlideGestureEnabled = false
  254. navigationBarSlideGestureEnablePending = false
  255. } else if (navigationBarSlideGestureAllowed) {
  256. snapper.enable()
  257. navigationBarSlideGestureEnabled = true
  258. navigationBarSlideGestureEnablePending = false
  259. } else {
  260. navigationBarSlideGestureEnablePending = true
  261. }
  262. }
  263. $(window).resize(_.debounce(toggleSnapperOnSize, 250))
  264. // initial call
  265. toggleSnapperOnSize()
  266. }
  267. initLiveTimestamps()
  268. PasswordConfirmation.init()
  269. }