| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- import { Container } from 'unstated';
- import axios from 'axios';
- import urljoin from 'url-join';
- import InterceptorManager from '@commons/service/interceptor-manager';
- import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
- import GrowiRenderer from '../util/GrowiRenderer';
- import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
- import {
- DetachCodeBlockInterceptor,
- RestoreCodeBlockInterceptor,
- } from '../util/interceptor/detach-code-blocks';
- import {
- DrawioInterceptor,
- } from '../util/interceptor/drawio-interceptor';
- import i18nFactory from '../util/i18n';
- import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
- /**
- * Service container related to options for Application
- * @extends {Container} unstated Container
- */
- export default class AppContainer extends Container {
- constructor() {
- super();
- this.state = {
- editorMode: null,
- preferDarkModeByMediaQuery: false,
- preferDarkModeByUser: null,
- breakpoint: 'xs',
- isDrawerOpened: false,
- isPageCreateModalShown: false,
- recentlyUpdatedPages: [],
- };
- const body = document.querySelector('body');
- this.isAdmin = body.dataset.isAdmin === 'true';
- this.csrfToken = body.dataset.csrftoken;
- this.isPluginEnabled = body.dataset.pluginEnabled === 'true';
- this.isLoggedin = document.querySelector('body.nologin') == null;
- this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
- const currentUserElem = document.getElementById('growi-current-user');
- if (currentUserElem != null) {
- this.currentUser = JSON.parse(currentUserElem.textContent);
- }
- const userAgent = window.navigator.userAgent.toLowerCase();
- this.isMobile = /iphone|ipad|android/.test(userAgent);
- this.isDocSaved = true;
- this.originRenderer = new GrowiRenderer(this);
- this.interceptorManager = new InterceptorManager();
- this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
- this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
- this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
- const userlang = body.dataset.userlang;
- this.i18n = i18nFactory(userlang);
- this.users = [];
- this.userByName = {};
- this.userById = {};
- this.recoverData();
- if (this.isLoggedin) {
- this.fetchUsers();
- }
- this.containerInstances = {};
- this.componentInstances = {};
- this.rendererInstances = {};
- this.fetchUsers = this.fetchUsers.bind(this);
- this.apiGet = this.apiGet.bind(this);
- this.apiPost = this.apiPost.bind(this);
- this.apiDelete = this.apiDelete.bind(this);
- this.apiRequest = this.apiRequest.bind(this);
- this.apiv3Root = '/_api/v3';
- this.apiv3 = {
- get: this.apiv3Get.bind(this),
- post: this.apiv3Post.bind(this),
- put: this.apiv3Put.bind(this),
- delete: this.apiv3Delete.bind(this),
- };
- this.openPageCreateModal = this.openPageCreateModal.bind(this);
- this.closePageCreateModal = this.closePageCreateModal.bind(this);
- }
- /**
- * Workaround for the mangling in production build to break constructor.name
- */
- static getClassName() {
- return 'AppContainer';
- }
- init() {
- this.initColorScheme();
- this.initPlugins();
- }
- async initColorScheme() {
- const switchStateByMediaQuery = (mql) => {
- const preferDarkMode = mql.matches;
- this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
- this.applyColorScheme();
- };
- const mqlForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
- // add event listener
- mqlForDarkMode.addListener(switchStateByMediaQuery);
- // restore settings from localStorage
- const { localStorage } = window;
- if (localStorage.preferDarkModeByUser != null) {
- await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
- }
- // initialize
- switchStateByMediaQuery(mqlForDarkMode);
- }
- initPlugins() {
- if (this.isPluginEnabled) {
- const growiPlugin = window.growiPlugin;
- growiPlugin.installAll(this, this.originRenderer);
- }
- }
- injectToWindow() {
- window.appContainer = this;
- const originRenderer = this.getOriginRenderer();
- window.growiRenderer = originRenderer;
- // backward compatibility
- window.crowi = this;
- window.crowiRenderer = originRenderer;
- window.crowiPlugin = window.growiPlugin;
- }
- get currentUserId() {
- if (this.currentUser == null) {
- return null;
- }
- return this.currentUser._id;
- }
- get currentUsername() {
- if (this.currentUser == null) {
- return null;
- }
- return this.currentUser.username;
- }
- /**
- * @return {Object} window.Crowi (js/legacy/crowi.js)
- */
- getCrowiForJquery() {
- return window.Crowi;
- }
- getConfig() {
- return this.config;
- }
- /**
- * Register unstated container instance
- * @param {object} instance unstated container instance
- */
- registerContainer(instance) {
- if (instance == null) {
- throw new Error('The specified instance must not be null');
- }
- const className = instance.constructor.getClassName();
- if (this.containerInstances[className] != null) {
- throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
- }
- this.containerInstances[className] = instance;
- }
- /**
- * Get registered unstated container instance
- * !! THIS METHOD SHOULD ONLY BE USED FROM unstated CONTAINERS !!
- * !! From component instances, inject containers with `import { Subscribe } from 'unstated'` !!
- *
- * @param {string} className
- */
- getContainer(className) {
- return this.containerInstances[className];
- }
- /**
- * Register React component instance
- * @param {string} id
- * @param {object} instance React component instance
- */
- registerComponentInstance(id, instance) {
- if (instance == null) {
- throw new Error('The specified instance must not be null');
- }
- if (this.componentInstances[id] != null) {
- throw new Error('The specified instance couldn\'t register because the same id has already been registered');
- }
- this.componentInstances[id] = instance;
- }
- /**
- * Get registered React component instance
- * @param {string} id
- */
- getComponentInstance(id) {
- return this.componentInstances[id];
- }
- /**
- *
- * @param {string} breakpoint id of breakpoint
- * @param {function} handler event handler for media query
- * @param {boolean} invokeOnInit invoke handler after the initialization if true
- */
- addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
- document.addEventListener('DOMContentLoaded', () => {
- // get the value of '--breakpoint-*'
- const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
- const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
- // add event listener
- mediaQuery.addListener(handler);
- // initialize
- if (invokeOnInit) {
- handler(mediaQuery);
- }
- });
- }
- getOriginRenderer() {
- return this.originRenderer;
- }
- /**
- * factory method
- */
- getRenderer(mode) {
- if (this.rendererInstances[mode] != null) {
- return this.rendererInstances[mode];
- }
- const renderer = new GrowiRenderer(this, this.originRenderer);
- // setup
- renderer.initMarkdownItConfigurers(mode);
- renderer.setup(mode);
- // register
- this.rendererInstances[mode] = renderer;
- return renderer;
- }
- getEmojiStrategy() {
- return emojiStrategy;
- }
- recoverData() {
- const keys = [
- 'userByName',
- 'userById',
- 'users',
- ];
- keys.forEach((key) => {
- const keyContent = window.localStorage[key];
- if (keyContent) {
- try {
- this[key] = JSON.parse(keyContent);
- }
- catch (e) {
- window.localStorage.removeItem(key);
- }
- }
- });
- }
- async retrieveRecentlyUpdated() {
- const { data } = await this.apiv3Get('/pages/recent');
- this.setState({ recentlyUpdatedPages: data.pages });
- }
- fetchUsers() {
- const interval = 1000 * 60 * 15; // 15min
- const currentTime = new Date();
- if (window.localStorage.lastFetched && interval > currentTime - new Date(window.localStorage.lastFetched)) {
- return;
- }
- this.apiGet('/users.list', {})
- .then((data) => {
- this.users = data.users;
- window.localStorage.users = JSON.stringify(data.users);
- const userByName = {};
- const userById = {};
- for (let i = 0; i < data.users.length; i++) {
- const user = data.users[i];
- userByName[user.username] = user;
- userById[user._id] = user;
- }
- this.userByName = userByName;
- window.localStorage.userByName = JSON.stringify(userByName);
- this.userById = userById;
- window.localStorage.userById = JSON.stringify(userById);
- window.localStorage.lastFetched = new Date();
- })
- .catch((err) => {
- window.localStorage.removeItem('lastFetched');
- // ignore errors
- });
- }
- findUserById(userId) {
- if (this.userById && this.userById[userId]) {
- return this.userById[userId];
- }
- return null;
- }
- findUserByIds(userIds) {
- const users = [];
- for (const userId of userIds) {
- const user = this.findUserById(userId);
- if (user) {
- users.push(user);
- }
- }
- return users;
- }
- toggleDrawer() {
- const { isDrawerOpened } = this.state;
- this.setState({ isDrawerOpened: !isDrawerOpened });
- }
- launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
- let targetComponent;
- switch (componentKind) {
- case 'page':
- targetComponent = this.getComponentInstance('Page');
- break;
- }
- targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
- }
- launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
- let targetComponent;
- switch (componentKind) {
- case 'page':
- targetComponent = this.getComponentInstance('Page');
- break;
- }
- targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
- }
- /**
- * Set color scheme preference by user
- * @param {boolean} isDarkMode
- */
- async setColorSchemePreference(isDarkMode) {
- await this.setState({ preferDarkModeByUser: isDarkMode });
- // store settings to localStorage
- const { localStorage } = window;
- if (isDarkMode == null) {
- delete localStorage.removeItem('preferDarkModeByUser');
- }
- else {
- localStorage.preferDarkModeByUser = isDarkMode;
- }
- this.applyColorScheme();
- }
- /**
- * Apply color scheme as 'dark' attribute of <html></html>
- */
- applyColorScheme() {
- const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
- let isDarkMode = preferDarkModeByMediaQuery;
- if (preferDarkModeByUser != null) {
- isDarkMode = preferDarkModeByUser;
- }
- // switch to dark mode
- if (isDarkMode) {
- document.documentElement.removeAttribute('light');
- document.documentElement.setAttribute('dark', 'true');
- }
- // switch to light mode
- else {
- document.documentElement.setAttribute('light', 'true');
- document.documentElement.removeAttribute('dark');
- }
- }
- async apiGet(path, params) {
- return this.apiRequest('get', path, { params });
- }
- async apiPost(path, params) {
- if (!params._csrf) {
- params._csrf = this.csrfToken;
- }
- return this.apiRequest('post', path, params);
- }
- async apiDelete(path, params) {
- if (!params._csrf) {
- params._csrf = this.csrfToken;
- }
- return this.apiRequest('delete', path, { data: params });
- }
- async apiRequest(method, path, params) {
- const res = await axios[method](`/_api${path}`, params);
- if (res.data.ok) {
- return res.data;
- }
- // Return error code if code is exist
- if (res.data.code != null) {
- const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
- throw error;
- }
- throw new Error(res.data.error);
- }
- async apiv3Request(method, path, params) {
- try {
- const res = await axios[method](urljoin(this.apiv3Root, path), params);
- return res.data;
- }
- catch (err) {
- const errors = apiv3ErrorHandler(err);
- throw errors;
- }
- }
- async apiv3Get(path, params) {
- return this.apiv3Request('get', path, { params });
- }
- async apiv3Post(path, params = {}) {
- if (!params._csrf) {
- params._csrf = this.csrfToken;
- }
- return this.apiv3Request('post', path, params);
- }
- async apiv3Put(path, params = {}) {
- if (!params._csrf) {
- params._csrf = this.csrfToken;
- }
- return this.apiv3Request('put', path, params);
- }
- async apiv3Delete(path, params = {}) {
- if (!params._csrf) {
- params._csrf = this.csrfToken;
- }
- return this.apiv3Request('delete', path, { params });
- }
- openPageCreateModal() {
- this.setState({ isPageCreateModalShown: true });
- }
- closePageCreateModal() {
- this.setState({ isPageCreateModalShown: false });
- }
- }
|