Front end of the Slack clone application.

app.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. import { EventEmitter, Injectable, Optional } from '@angular/core';
  2. import { DOCUMENT, Title } from '@angular/platform-browser';
  3. import * as Constants from './app-constants';
  4. import { Config } from '../../config/config';
  5. import { DIRECTION_BACK, DIRECTION_FORWARD, isTabs } from '../../navigation/nav-util';
  6. import { MenuController } from './menu-controller';
  7. import { Platform } from '../../platform/platform';
  8. import { IOSTransition } from '../../transitions/transition-ios';
  9. import { MDTransition } from '../../transitions/transition-md';
  10. import { WPTransition } from '../../transitions/transition-wp';
  11. /**
  12. * @name App
  13. * @description
  14. * App is a utility class used in Ionic to get information about various aspects of an app
  15. */
  16. export class App {
  17. constructor(_config, _plt, _menuCtrl) {
  18. this._config = _config;
  19. this._plt = _plt;
  20. this._menuCtrl = _menuCtrl;
  21. this._disTime = 0;
  22. this._scrollTime = 0;
  23. this._title = '';
  24. this._titleSrv = new Title(DOCUMENT);
  25. this._rootNavs = new Map();
  26. this._didScroll = false;
  27. /**
  28. * Observable that emits whenever a view loads in the app.
  29. * @returns {Observable} Returns an observable
  30. */
  31. this.viewDidLoad = new EventEmitter();
  32. /**
  33. * Observable that emits before any view is entered in the app.
  34. * @returns {Observable} Returns an observable
  35. */
  36. this.viewWillEnter = new EventEmitter();
  37. /**
  38. * Observable that emits after any view is entered in the app.
  39. * @returns {Observable} Returns an observable
  40. */
  41. this.viewDidEnter = new EventEmitter();
  42. /**
  43. * Observable that emits before any view is exited in the app.
  44. * @returns {Observable} Returns an observable
  45. */
  46. this.viewWillLeave = new EventEmitter();
  47. /**
  48. * Observable that emits after any view is exited in the app.
  49. * @returns {Observable} Returns an observable
  50. */
  51. this.viewDidLeave = new EventEmitter();
  52. /**
  53. * Observable that emits before any view unloads in the app.
  54. * @returns {Observable} Returns an observable
  55. */
  56. this.viewWillUnload = new EventEmitter();
  57. // listen for hardware back button events
  58. // register this back button action with a default priority
  59. _plt.registerBackButtonAction(this.goBack.bind(this));
  60. this._disableScrollAssist = _config.getBoolean('disableScrollAssist', false);
  61. const blurring = _config.getBoolean('inputBlurring', false);
  62. if (blurring) {
  63. this._enableInputBlurring();
  64. }
  65. (void 0) /* runInDev */;
  66. _config.setTransition('ios-transition', IOSTransition);
  67. _config.setTransition('md-transition', MDTransition);
  68. _config.setTransition('wp-transition', WPTransition);
  69. }
  70. /**
  71. * Sets the document title.
  72. * @param {string} val Value to set the document title to.
  73. */
  74. setTitle(val) {
  75. if (val !== this._title) {
  76. this._title = val;
  77. this._titleSrv.setTitle(val);
  78. }
  79. }
  80. /**
  81. * @hidden
  82. */
  83. setElementClass(className, isAdd) {
  84. this._appRoot.setElementClass(className, isAdd);
  85. }
  86. /**
  87. * @hidden
  88. * Sets if the app is currently enabled or not, meaning if it's
  89. * available to accept new user commands. For example, this is set to `false`
  90. * while views transition, a modal slides up, an action-sheet
  91. * slides up, etc. After the transition completes it is set back to `true`.
  92. * @param {boolean} isEnabled `true` for enabled, `false` for disabled
  93. * @param {number} duration When `isEnabled` is set to `false`, this argument
  94. * is used to set the maximum number of milliseconds that app will wait until
  95. * it will automatically enable the app again. It's basically a fallback incase
  96. * something goes wrong during a transition and the app wasn't re-enabled correctly.
  97. */
  98. setEnabled(isEnabled, duration = 700, minDuration = 0) {
  99. this._disTime = (isEnabled ? 0 : Date.now() + duration);
  100. if (this._clickBlock) {
  101. if (isEnabled) {
  102. // disable the click block if it's enabled, or the duration is tiny
  103. this._clickBlock.activate(false, CLICK_BLOCK_BUFFER_IN_MILLIS, minDuration);
  104. }
  105. else {
  106. // show the click block for duration + some number
  107. this._clickBlock.activate(true, duration + CLICK_BLOCK_BUFFER_IN_MILLIS, minDuration);
  108. }
  109. }
  110. }
  111. /**
  112. * @hidden
  113. * Toggles whether an application can be scrolled
  114. * @param {boolean} disableScroll when set to `false`, the application's
  115. * scrolling is enabled. When set to `true`, scrolling is disabled.
  116. */
  117. _setDisableScroll(disableScroll) {
  118. if (this._disableScrollAssist) {
  119. this._appRoot._disableScroll(disableScroll);
  120. }
  121. }
  122. /**
  123. * @hidden
  124. * Boolean if the app is actively enabled or not.
  125. * @return {boolean}
  126. */
  127. isEnabled() {
  128. const disTime = this._disTime;
  129. if (disTime === 0) {
  130. return true;
  131. }
  132. return (disTime < Date.now());
  133. }
  134. /**
  135. * @hidden
  136. */
  137. setScrolling() {
  138. this._scrollTime = Date.now() + ACTIVE_SCROLLING_TIME;
  139. this._didScroll = true;
  140. }
  141. /**
  142. * Boolean if the app is actively scrolling or not.
  143. * @return {boolean} returns true or false
  144. */
  145. isScrolling() {
  146. const scrollTime = this._scrollTime;
  147. if (scrollTime === 0) {
  148. return false;
  149. }
  150. if (scrollTime < Date.now()) {
  151. this._scrollTime = 0;
  152. return false;
  153. }
  154. return true;
  155. }
  156. /**
  157. * @return {NavController} Returns the first Active Nav Controller from the list. This method is deprecated
  158. */
  159. getActiveNav() {
  160. console.warn('(getActiveNav) is deprecated and will be removed in the next major release. Use getActiveNavs instead.');
  161. const navs = this.getActiveNavs();
  162. if (navs && navs.length) {
  163. return navs[0];
  164. }
  165. return null;
  166. }
  167. /**
  168. * @return {NavController[]} Returns the active NavControllers. Using this method is preferred when we need access to the top-level navigation controller while on the outside views and handlers like `registerBackButtonAction()`
  169. */
  170. getActiveNavs(rootNavId) {
  171. const portal = this._appRoot._getPortal(Constants.PORTAL_MODAL);
  172. if (portal.length() > 0) {
  173. return findTopNavs(portal);
  174. }
  175. if (!this._rootNavs || !this._rootNavs.size) {
  176. return [];
  177. }
  178. if (this._rootNavs.size === 1) {
  179. return findTopNavs(this._rootNavs.values().next().value);
  180. }
  181. if (rootNavId) {
  182. return findTopNavs(this._rootNavs.get(rootNavId));
  183. }
  184. // fallback to just using all root names
  185. let activeNavs = [];
  186. this._rootNavs.forEach(nav => {
  187. const topNavs = findTopNavs(nav);
  188. activeNavs = activeNavs.concat(topNavs);
  189. });
  190. return activeNavs;
  191. }
  192. getRootNav() {
  193. console.warn('(getRootNav) is deprecated and will be removed in the next major release. Use getRootNavById instead.');
  194. const rootNavs = this.getRootNavs();
  195. if (rootNavs.length === 0) {
  196. return null;
  197. }
  198. else if (rootNavs.length > 1) {
  199. console.warn('(getRootNav) there are multiple root navs, use getRootNavs instead');
  200. }
  201. return rootNavs[0];
  202. }
  203. getRootNavs() {
  204. const navs = [];
  205. this._rootNavs.forEach(nav => navs.push(nav));
  206. return navs;
  207. }
  208. /**
  209. * @return {NavController} Returns the root NavController
  210. */
  211. getRootNavById(navId) {
  212. return this._rootNavs.get(navId);
  213. }
  214. /**
  215. * @hidden
  216. */
  217. registerRootNav(nav) {
  218. this._rootNavs.set(nav.id, nav);
  219. }
  220. /**
  221. * @hidden
  222. */
  223. unregisterRootNav(nav) {
  224. this._rootNavs.delete(nav.id);
  225. }
  226. getActiveNavContainers() {
  227. // for each root nav container, get it's active nav
  228. let list = [];
  229. this._rootNavs.forEach((container) => {
  230. list = list.concat(findTopNavs(container));
  231. });
  232. return list;
  233. }
  234. /**
  235. * @hidden
  236. */
  237. present(enteringView, opts, appPortal) {
  238. (void 0) /* assert */;
  239. const portal = this._appRoot._getPortal(appPortal);
  240. // Set Nav must be set here in order to dimiss() work synchnously.
  241. // TODO: move _setNav() to the earlier stages of NavController. _queueTrns()
  242. enteringView._setNav(portal);
  243. opts.direction = DIRECTION_FORWARD;
  244. if (!opts.animation) {
  245. opts.animation = enteringView.getTransitionName(DIRECTION_FORWARD);
  246. }
  247. enteringView.setLeavingOpts({
  248. keyboardClose: opts.keyboardClose,
  249. direction: DIRECTION_BACK,
  250. animation: enteringView.getTransitionName(DIRECTION_BACK),
  251. ev: opts.ev
  252. });
  253. return portal.insertPages(-1, [enteringView], opts);
  254. }
  255. /**
  256. * @hidden
  257. */
  258. goBack() {
  259. if (this._menuCtrl && this._menuCtrl.isOpen()) {
  260. return this._menuCtrl.close();
  261. }
  262. const navPromise = this.navPop();
  263. if (!navPromise) {
  264. // no views to go back to
  265. // let's exit the app
  266. if (this._config.getBoolean('navExitApp', true)) {
  267. (void 0) /* console.debug */;
  268. this._plt.exitApp();
  269. }
  270. }
  271. return navPromise;
  272. }
  273. /**
  274. * @hidden
  275. */
  276. navPop() {
  277. if (!this._rootNavs || this._rootNavs.size === 0 || !this.isEnabled()) {
  278. return Promise.resolve();
  279. }
  280. // If there are any alert/actionsheet open, let's do nothing
  281. const portal = this._appRoot._getPortal(Constants.PORTAL_DEFAULT);
  282. if (portal.length() > 0) {
  283. return Promise.resolve();
  284. }
  285. let navToPop = null;
  286. let mostRecentVC = null;
  287. this._rootNavs.forEach((navContainer) => {
  288. const activeNavs = this.getActiveNavs(navContainer.id);
  289. const poppableNavs = activeNavs.map(activeNav => getPoppableNav(activeNav)).filter(nav => !!nav);
  290. poppableNavs.forEach(poppable => {
  291. const topViewController = poppable.last();
  292. if (poppable._isPortal || (topViewController && poppable.length() > 1 && (!mostRecentVC || topViewController._ts >= mostRecentVC._ts))) {
  293. mostRecentVC = topViewController;
  294. navToPop = poppable;
  295. }
  296. });
  297. });
  298. if (navToPop) {
  299. return navToPop.pop();
  300. }
  301. }
  302. /**
  303. * @hidden
  304. */
  305. _enableInputBlurring() {
  306. (void 0) /* console.debug */;
  307. let focused = true;
  308. const self = this;
  309. const platform = this._plt;
  310. platform.registerListener(platform.doc(), 'focusin', onFocusin, { capture: true, zone: false, passive: true });
  311. platform.registerListener(platform.doc(), 'touchend', onTouchend, { capture: false, zone: false, passive: true });
  312. function onFocusin() {
  313. focused = true;
  314. }
  315. function onTouchend(ev) {
  316. // if app did scroll return early
  317. if (self._didScroll) {
  318. self._didScroll = false;
  319. return;
  320. }
  321. const active = self._plt.getActiveElement();
  322. if (!active) {
  323. return;
  324. }
  325. // only blur if the active element is a text-input or a textarea
  326. if (SKIP_BLURRING.indexOf(active.tagName) === -1) {
  327. return;
  328. }
  329. // if the selected target is the active element, do not blur
  330. const tapped = ev.target;
  331. if (tapped === active) {
  332. return;
  333. }
  334. if (SKIP_BLURRING.indexOf(tapped.tagName) >= 0) {
  335. return;
  336. }
  337. // skip if div is a cover
  338. if (tapped.classList.contains('input-cover')) {
  339. return;
  340. }
  341. focused = false;
  342. // TODO: find a better way, why 50ms?
  343. platform.timeout(() => {
  344. if (!focused) {
  345. active.blur();
  346. }
  347. }, 50);
  348. }
  349. }
  350. getNavByIdOrName(id) {
  351. const navs = Array.from(this._rootNavs.values());
  352. for (const navContainer of navs) {
  353. const match = getNavByIdOrName(navContainer, id);
  354. if (match) {
  355. return match;
  356. }
  357. }
  358. return null;
  359. }
  360. }
  361. App.decorators = [
  362. { type: Injectable },
  363. ];
  364. /** @nocollapse */
  365. App.ctorParameters = () => [
  366. { type: Config, },
  367. { type: Platform, },
  368. { type: MenuController, decorators: [{ type: Optional },] },
  369. ];
  370. export function getNavByIdOrName(nav, id) {
  371. if (nav.id === id || nav.name === id) {
  372. return nav;
  373. }
  374. for (const child of nav.getAllChildNavs()) {
  375. const tmp = getNavByIdOrName(child, id);
  376. if (tmp) {
  377. return tmp;
  378. }
  379. }
  380. return null;
  381. }
  382. function getPoppableNav(nav) {
  383. if (!nav) {
  384. return null;
  385. }
  386. if (isTabs(nav)) {
  387. // tabs aren't a nav, so just call this function again immediately on the parent on tabs
  388. return getPoppableNav(nav.parent);
  389. }
  390. const len = nav.length();
  391. if (len > 1 || (nav._isPortal && len > 0)) {
  392. // this nav controller has more than one view
  393. // use this nav!
  394. return nav;
  395. }
  396. // try again using the parent nav (if there is one)
  397. return getPoppableNav(nav.parent);
  398. }
  399. export function findTopNavs(nav) {
  400. let containers = [];
  401. const childNavs = nav.getActiveChildNavs();
  402. if (!childNavs || !childNavs.length) {
  403. containers.push(nav);
  404. }
  405. else {
  406. childNavs.forEach(childNav => {
  407. const topNavs = findTopNavs(childNav);
  408. containers = containers.concat(topNavs);
  409. });
  410. }
  411. return containers;
  412. }
  413. const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
  414. const ACTIVE_SCROLLING_TIME = 100;
  415. const CLICK_BLOCK_BUFFER_IN_MILLIS = 64;
  416. //# sourceMappingURL=app.js.map