AppContainer.js 7.5 KB

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