2
0

AppContainer.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import { Container } from 'unstated';
  2. import InterceptorManager from '~/services/interceptor-manager';
  3. import {
  4. apiDelete, apiGet, apiPost, apiRequest,
  5. } from '../util/apiv1-client';
  6. import {
  7. apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
  8. } from '../util/apiv3-client';
  9. import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
  10. import GrowiRenderer from '../util/GrowiRenderer';
  11. import {
  12. mediaQueryListForDarkMode,
  13. applyColorScheme,
  14. } from '../util/color-scheme';
  15. import { i18nFactory } from '../util/i18n';
  16. /**
  17. * Service container related to options for Application
  18. * @extends {Container} unstated Container
  19. */
  20. export default class AppContainer extends Container {
  21. constructor() {
  22. super();
  23. this.state = {
  24. preferDarkModeByMediaQuery: false,
  25. };
  26. // get csrf token from body element
  27. // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
  28. const body = document.querySelector('body');
  29. this.csrfToken = body.dataset.csrftoken;
  30. this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
  31. const userAgent = window.navigator.userAgent.toLowerCase();
  32. this.isMobile = /iphone|ipad|android/.test(userAgent);
  33. const currentUserElem = document.getElementById('growi-current-user');
  34. if (currentUserElem != null) {
  35. this.currentUser = JSON.parse(currentUserElem.textContent);
  36. }
  37. const isSharedPageElem = document.getElementById('is-shared-page');
  38. // check what kind of user
  39. this.isGuestUser = this.currentUser == null;
  40. this.isSharedUser = isSharedPageElem != null && this.currentUser == null;
  41. const userLocaleId = this.currentUser?.lang;
  42. this.i18n = i18nFactory(userLocaleId);
  43. this.containerInstances = {};
  44. this.componentInstances = {};
  45. this.rendererInstances = {};
  46. this.apiGet = apiGet;
  47. this.apiPost = apiPost;
  48. this.apiDelete = apiDelete;
  49. this.apiRequest = apiRequest;
  50. this.apiv3Get = apiv3Get;
  51. this.apiv3Post = apiv3Post;
  52. this.apiv3Put = apiv3Put;
  53. this.apiv3Delete = apiv3Delete;
  54. this.apiv3 = {
  55. get: apiv3Get,
  56. post: apiv3Post,
  57. put: apiv3Put,
  58. delete: apiv3Delete,
  59. };
  60. }
  61. /**
  62. * Workaround for the mangling in production build to break constructor.name
  63. */
  64. static getClassName() {
  65. return 'AppContainer';
  66. }
  67. initApp() {
  68. this.initMediaQueryForColorScheme();
  69. this.injectToWindow();
  70. }
  71. initContents() {
  72. const body = document.querySelector('body');
  73. this.isAdmin = body.dataset.isAdmin === 'true';
  74. this.isDocSaved = true;
  75. this.originRenderer = new GrowiRenderer(this);
  76. this.interceptorManager = new InterceptorManager();
  77. if (this.currentUser != null) {
  78. // remove old user cache
  79. this.removeOldUserCache();
  80. }
  81. const isPluginEnabled = body.dataset.pluginEnabled === 'true';
  82. if (isPluginEnabled) {
  83. this.initPlugins();
  84. }
  85. this.injectToWindow();
  86. }
  87. async initMediaQueryForColorScheme() {
  88. const switchStateByMediaQuery = async(mql) => {
  89. const preferDarkMode = mql.matches;
  90. this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
  91. applyColorScheme();
  92. };
  93. // add event listener
  94. mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
  95. }
  96. initPlugins() {
  97. const growiPlugin = window.growiPlugin;
  98. growiPlugin.installAll(this, this.originRenderer);
  99. }
  100. injectToWindow() {
  101. window.appContainer = this;
  102. const originRenderer = this.getOriginRenderer();
  103. window.growiRenderer = originRenderer;
  104. // backward compatibility
  105. window.crowi = this;
  106. window.crowiRenderer = originRenderer;
  107. window.crowiPlugin = window.growiPlugin;
  108. }
  109. get currentUserId() {
  110. if (this.currentUser == null) {
  111. return null;
  112. }
  113. return this.currentUser._id;
  114. }
  115. get currentUsername() {
  116. if (this.currentUser == null) {
  117. return null;
  118. }
  119. return this.currentUser.username;
  120. }
  121. /**
  122. * @return {Object} window.Crowi (js/legacy/crowi.js)
  123. */
  124. getCrowiForJquery() {
  125. return window.Crowi;
  126. }
  127. getConfig() {
  128. return this.config;
  129. }
  130. /**
  131. * Register unstated container instance
  132. * @param {object} instance unstated container instance
  133. */
  134. registerContainer(instance) {
  135. if (instance == null) {
  136. throw new Error('The specified instance must not be null');
  137. }
  138. const className = instance.constructor.getClassName();
  139. if (this.containerInstances[className] != null) {
  140. throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
  141. }
  142. this.containerInstances[className] = instance;
  143. }
  144. /**
  145. * Get registered unstated container instance
  146. * !! THIS METHOD SHOULD ONLY BE USED FROM unstated CONTAINERS !!
  147. * !! From component instances, inject containers with `import { Subscribe } from 'unstated'` !!
  148. *
  149. * @param {string} className
  150. */
  151. getContainer(className) {
  152. return this.containerInstances[className];
  153. }
  154. /**
  155. * Register React component instance
  156. * @param {string} id
  157. * @param {object} instance React component instance
  158. */
  159. registerComponentInstance(id, instance) {
  160. if (instance == null) {
  161. throw new Error('The specified instance must not be null');
  162. }
  163. if (this.componentInstances[id] != null) {
  164. throw new Error('The specified instance couldn\'t register because the same id has already been registered');
  165. }
  166. this.componentInstances[id] = instance;
  167. }
  168. /**
  169. * Get registered React component instance
  170. * @param {string} id
  171. */
  172. getComponentInstance(id) {
  173. return this.componentInstances[id];
  174. }
  175. /**
  176. *
  177. * @param {string} breakpoint id of breakpoint
  178. * @param {function} handler event handler for media query
  179. * @param {boolean} invokeOnInit invoke handler after the initialization if true
  180. */
  181. addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
  182. document.addEventListener('DOMContentLoaded', () => {
  183. // get the value of '--breakpoint-*'
  184. const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
  185. const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
  186. // add event listener
  187. mediaQuery.addListener(handler);
  188. // initialize
  189. if (invokeOnInit) {
  190. handler(mediaQuery);
  191. }
  192. });
  193. }
  194. getOriginRenderer() {
  195. return this.originRenderer;
  196. }
  197. /**
  198. * factory method
  199. */
  200. getRenderer(mode) {
  201. if (this.rendererInstances[mode] != null) {
  202. return this.rendererInstances[mode];
  203. }
  204. const renderer = new GrowiRenderer(this, this.originRenderer);
  205. // setup
  206. renderer.initMarkdownItConfigurers(mode);
  207. renderer.setup(mode);
  208. // register
  209. this.rendererInstances[mode] = renderer;
  210. return renderer;
  211. }
  212. getEmojiStrategy() {
  213. return emojiStrategy;
  214. }
  215. removeOldUserCache() {
  216. if (window.localStorage.userByName == null) {
  217. return;
  218. }
  219. const keys = ['userByName', 'userById', 'users', 'lastFetched'];
  220. keys.forEach((key) => {
  221. window.localStorage.removeItem(key);
  222. });
  223. }
  224. launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
  225. let targetComponent;
  226. switch (componentKind) {
  227. case 'page':
  228. targetComponent = this.getComponentInstance('Page');
  229. break;
  230. }
  231. targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
  232. }
  233. launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
  234. let targetComponent;
  235. switch (componentKind) {
  236. case 'page':
  237. targetComponent = this.getComponentInstance('Page');
  238. break;
  239. }
  240. targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
  241. }
  242. }