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

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