2
0

AppContainer.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { Container } from 'unstated';
  2. import axios from 'axios';
  3. import urljoin from 'url-join';
  4. import InterceptorManager from '@commons/service/interceptor-manager';
  5. import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
  6. import GrowiRenderer from '../util/GrowiRenderer';
  7. import {
  8. mediaQueryListForDarkMode,
  9. applyColorScheme,
  10. } from '../util/color-scheme';
  11. import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
  12. import {
  13. DetachCodeBlockInterceptor,
  14. RestoreCodeBlockInterceptor,
  15. } from '../util/interceptor/detach-code-blocks';
  16. import {
  17. DrawioInterceptor,
  18. } from '../util/interceptor/drawio-interceptor';
  19. import i18nFactory from '../util/i18n';
  20. import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
  21. /**
  22. * Service container related to options for Application
  23. * @extends {Container} unstated Container
  24. */
  25. export default class AppContainer extends Container {
  26. constructor() {
  27. super();
  28. this.state = {
  29. preferDarkModeByMediaQuery: false,
  30. // stetes for contents
  31. recentlyUpdatedPages: [],
  32. };
  33. const body = document.querySelector('body');
  34. this.csrfToken = body.dataset.csrftoken;
  35. this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
  36. const isSharedPageElem = document.getElementById('is-shared-page');
  37. this.isSharedUser = (isSharedPageElem != null);
  38. const userAgent = window.navigator.userAgent.toLowerCase();
  39. this.isMobile = /iphone|ipad|android/.test(userAgent);
  40. const userlang = body.dataset.userlang;
  41. this.i18n = i18nFactory(userlang);
  42. this.containerInstances = {};
  43. this.componentInstances = {};
  44. this.rendererInstances = {};
  45. this.apiGet = this.apiGet.bind(this);
  46. this.apiPost = this.apiPost.bind(this);
  47. this.apiDelete = this.apiDelete.bind(this);
  48. this.apiRequest = this.apiRequest.bind(this);
  49. this.apiv3Root = '/_api/v3';
  50. this.apiv3 = {
  51. get: this.apiv3Get.bind(this),
  52. post: this.apiv3Post.bind(this),
  53. put: this.apiv3Put.bind(this),
  54. delete: this.apiv3Delete.bind(this),
  55. };
  56. }
  57. /**
  58. * Workaround for the mangling in production build to break constructor.name
  59. */
  60. static getClassName() {
  61. return 'AppContainer';
  62. }
  63. initApp() {
  64. this.initMediaQueryForColorScheme();
  65. this.injectToWindow();
  66. }
  67. initContents() {
  68. const body = document.querySelector('body');
  69. const currentUserElem = document.getElementById('growi-current-user');
  70. if (currentUserElem != null) {
  71. this.currentUser = JSON.parse(currentUserElem.textContent);
  72. }
  73. this.isAdmin = body.dataset.isAdmin === 'true';
  74. this.isDocSaved = true;
  75. this.originRenderer = new GrowiRenderer(this);
  76. this.interceptorManager = new InterceptorManager();
  77. this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
  78. this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
  79. this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
  80. if (this.currentUser != null) {
  81. // remove old user cache
  82. this.removeOldUserCache();
  83. }
  84. const isPluginEnabled = body.dataset.pluginEnabled === 'true';
  85. if (isPluginEnabled) {
  86. this.initPlugins();
  87. }
  88. this.injectToWindow();
  89. }
  90. async initMediaQueryForColorScheme() {
  91. const switchStateByMediaQuery = async(mql) => {
  92. const preferDarkMode = mql.matches;
  93. this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
  94. applyColorScheme();
  95. };
  96. // add event listener
  97. mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
  98. }
  99. initPlugins() {
  100. const growiPlugin = window.growiPlugin;
  101. growiPlugin.installAll(this, this.originRenderer);
  102. }
  103. injectToWindow() {
  104. window.appContainer = this;
  105. const originRenderer = this.getOriginRenderer();
  106. window.growiRenderer = originRenderer;
  107. // backward compatibility
  108. window.crowi = this;
  109. window.crowiRenderer = originRenderer;
  110. window.crowiPlugin = window.growiPlugin;
  111. }
  112. get currentUserId() {
  113. if (this.currentUser == null) {
  114. return null;
  115. }
  116. return this.currentUser._id;
  117. }
  118. get currentUsername() {
  119. if (this.currentUser == null) {
  120. return null;
  121. }
  122. return this.currentUser.username;
  123. }
  124. /**
  125. * @return {Object} window.Crowi (js/legacy/crowi.js)
  126. */
  127. getCrowiForJquery() {
  128. return window.Crowi;
  129. }
  130. getConfig() {
  131. return this.config;
  132. }
  133. /**
  134. * Register unstated container instance
  135. * @param {object} instance unstated container instance
  136. */
  137. registerContainer(instance) {
  138. if (instance == null) {
  139. throw new Error('The specified instance must not be null');
  140. }
  141. const className = instance.constructor.getClassName();
  142. if (this.containerInstances[className] != null) {
  143. throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
  144. }
  145. this.containerInstances[className] = instance;
  146. }
  147. /**
  148. * Get registered unstated container instance
  149. * !! THIS METHOD SHOULD ONLY BE USED FROM unstated CONTAINERS !!
  150. * !! From component instances, inject containers with `import { Subscribe } from 'unstated'` !!
  151. *
  152. * @param {string} className
  153. */
  154. getContainer(className) {
  155. return this.containerInstances[className];
  156. }
  157. /**
  158. * Register React component instance
  159. * @param {string} id
  160. * @param {object} instance React component instance
  161. */
  162. registerComponentInstance(id, instance) {
  163. if (instance == null) {
  164. throw new Error('The specified instance must not be null');
  165. }
  166. if (this.componentInstances[id] != null) {
  167. throw new Error('The specified instance couldn\'t register because the same id has already been registered');
  168. }
  169. this.componentInstances[id] = instance;
  170. }
  171. /**
  172. * Get registered React component instance
  173. * @param {string} id
  174. */
  175. getComponentInstance(id) {
  176. return this.componentInstances[id];
  177. }
  178. /**
  179. *
  180. * @param {string} breakpoint id of breakpoint
  181. * @param {function} handler event handler for media query
  182. * @param {boolean} invokeOnInit invoke handler after the initialization if true
  183. */
  184. addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
  185. document.addEventListener('DOMContentLoaded', () => {
  186. // get the value of '--breakpoint-*'
  187. const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
  188. const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
  189. // add event listener
  190. mediaQuery.addListener(handler);
  191. // initialize
  192. if (invokeOnInit) {
  193. handler(mediaQuery);
  194. }
  195. });
  196. }
  197. getOriginRenderer() {
  198. return this.originRenderer;
  199. }
  200. /**
  201. * factory method
  202. */
  203. getRenderer(mode) {
  204. if (this.rendererInstances[mode] != null) {
  205. return this.rendererInstances[mode];
  206. }
  207. const renderer = new GrowiRenderer(this, this.originRenderer);
  208. // setup
  209. renderer.initMarkdownItConfigurers(mode);
  210. renderer.setup(mode);
  211. // register
  212. this.rendererInstances[mode] = renderer;
  213. return renderer;
  214. }
  215. getEmojiStrategy() {
  216. return emojiStrategy;
  217. }
  218. removeOldUserCache() {
  219. if (window.localStorage.userByName == null) {
  220. return;
  221. }
  222. const keys = ['userByName', 'userById', 'users', 'lastFetched'];
  223. keys.forEach((key) => {
  224. window.localStorage.removeItem(key);
  225. });
  226. }
  227. async retrieveRecentlyUpdated() {
  228. const { data } = await this.apiv3Get('/pages/recent');
  229. this.setState({ recentlyUpdatedPages: data.pages });
  230. }
  231. launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
  232. let targetComponent;
  233. switch (componentKind) {
  234. case 'page':
  235. targetComponent = this.getComponentInstance('Page');
  236. break;
  237. }
  238. targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
  239. }
  240. launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
  241. let targetComponent;
  242. switch (componentKind) {
  243. case 'page':
  244. targetComponent = this.getComponentInstance('Page');
  245. break;
  246. }
  247. targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
  248. }
  249. async apiGet(path, params) {
  250. return this.apiRequest('get', path, { params });
  251. }
  252. async apiPost(path, params) {
  253. if (!params._csrf) {
  254. params._csrf = this.csrfToken;
  255. }
  256. return this.apiRequest('post', path, params);
  257. }
  258. async apiDelete(path, params) {
  259. if (!params._csrf) {
  260. params._csrf = this.csrfToken;
  261. }
  262. return this.apiRequest('delete', path, { data: params });
  263. }
  264. async apiRequest(method, path, params) {
  265. const res = await axios[method](`/_api${path}`, params);
  266. if (res.data.ok) {
  267. return res.data;
  268. }
  269. // Return error code if code is exist
  270. if (res.data.code != null) {
  271. const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
  272. throw error;
  273. }
  274. throw new Error(res.data.error);
  275. }
  276. async apiv3Request(method, path, params) {
  277. try {
  278. const res = await axios[method](urljoin(this.apiv3Root, path), params);
  279. return res.data;
  280. }
  281. catch (err) {
  282. const errors = apiv3ErrorHandler(err);
  283. throw errors;
  284. }
  285. }
  286. async apiv3Get(path, params) {
  287. return this.apiv3Request('get', path, { params });
  288. }
  289. async apiv3Post(path, params = {}) {
  290. if (!params._csrf) {
  291. params._csrf = this.csrfToken;
  292. }
  293. return this.apiv3Request('post', path, params);
  294. }
  295. async apiv3Put(path, params = {}) {
  296. if (!params._csrf) {
  297. params._csrf = this.csrfToken;
  298. }
  299. return this.apiv3Request('put', path, params);
  300. }
  301. async apiv3Delete(path, params = {}) {
  302. if (!params._csrf) {
  303. params._csrf = this.csrfToken;
  304. }
  305. return this.apiv3Request('delete', path, { params });
  306. }
  307. }