Source: index.js

/**
 * @namespace qui
 */

import * as RequireJSCompat from '$qui/base/require-js-compat.js'
import $                    from '$qui/lib/jquery.module.js'
import Logger               from '$qui/lib/logger.module.js'

import '$qui/lib/jquery-ui.js'
import '$qui/lib/jquery.mousewheel.js'
import '$qui/lib/jquery.longpress.js'
import '$qui/lib/pep.js'

import {globalize}               from '$qui/base/base.js'
import ConditionVariable         from '$qui/base/condition-variable.js'
import {CancelledError}          from '$qui/base/errors.js'
import {gettext}                 from '$qui/base/i18n.js'
import Config                    from '$qui/config.js'
import * as Forms                from '$qui/forms/forms.js'
import * as GlobalGlass          from '$qui/global-glass.js'
import * as Icons                from '$qui/icons/icons.js'
import * as MainUI               from '$qui/main-ui/main-ui.js'
import * as Status               from '$qui/main-ui/status.js'
import {StickySimpleMessageForm} from '$qui/messages/common-message-forms/common-message-forms.js'
import * as Messages             from '$qui/messages/messages.js'
import * as Navigation           from '$qui/navigation.js'
import * as Pages                from '$qui/pages/pages.js'
import * as PWA                  from '$qui/pwa.js'
import * as Sections             from '$qui/sections/sections.js'
import * as Theme                from '$qui/theme.js'
import * as ArrayUtils           from '$qui/utils/array.js'
import * as DateUtils            from '$qui/utils/date.js'
import * as ObjectUtils          from '$qui/utils/object.js'
import * as Widgets              from '$qui/widgets/widgets.js'
import * as Window               from '$qui/window.js'


const logger = Logger.get('qui')

/**
 * A condition that is fulfilled as soon as the app is fully initialized and ready to be used.
 * @alias qui.whenReady
 * @type {qui.base.ConditionVariable}
 */
export let whenReady = new ConditionVariable()


function initConfig() {
    /* Look for the main script name */
    let error = new Error()
    let names = error.stack.match(new RegExp('[A-Za-z0-9_-]+\\.js', 'g'))
    if (!names) {
        throw new Error('Cannot find main script: empty stack')
    }

    let scripts = document.getElementsByTagName('script')

    /* Find the reference to the main app DOM script */
    let mainAppScriptName = names[names.length - 1]
    let mainAppScript = Array.from(scripts).find(s => s.src.split('?')[0].endsWith(mainAppScriptName))
    if (!mainAppScript) {
        throw new Error(`Cannot find main script: no such script ${mainAppScriptName}`)
    }

    /* Deduce app name from main app script name */
    let appName = mainAppScriptName.slice(0, -3) /* remove .js */
    if (appName.endsWith('-bundle')) {
        appName = appName.slice(0, -7)
    }
    if (appName === 'index') { /* development mode */
        appName = 'qui-app'
    }
    Config.appName = appName

    /* Copy all data-* attributes from script tag to Config */
    ObjectUtils.forEach(mainAppScript.dataset, (n, v) => Config.set(n, v))

    /* Find the URL to the QUI index */
    let urls = error.stack.match(/http.*?\.js/gi)
    if (!urls || !urls.length) {
        throw new Error('Cannot find QUI index script: no script URLs found in stack')
    }

    /* Detect QUI static URL - we know it's the first script in stack */
    let quiIndexScript = urls[0]
    let m = quiIndexScript.match(new RegExp('[.a-z0-9_-]+\\.js'))
    if (!m) {
        throw new Error('Cannot find QUI index script: no JS file found in stack')
    }

    Config.quiIndexName = quiIndexScript.split('/').slice(-1)[0]
    Config.quiIndexName = Config.quiIndexName.split(':')[0] /* Remove :row:col */
    Config.quiStaticURL = quiIndexScript.slice(0, m.index - 1)
    /* In debug mode, we have an extra "/js" dir */
    if (Config.debug) {
        Config.quiStaticURL = Config.quiStaticURL.split('/').slice(0, -1).join('/')
    }

    /* Detect app static URL - we know it's the last script in stack */
    let appIndexScript = urls.slice(-1)[0]
    m = appIndexScript.match(new RegExp('[.a-z0-9_-]+\\.js'))

    Config.appIndexName = appIndexScript.split('/').slice(-1)[0]
    Config.appIndexName = Config.appIndexName.split(':')[0] /* Remove :row:col */
    Config.appStaticURL = appIndexScript.slice(0, m.index - 1)
    /* In debug mode, we have an extra "/js" dir */
    if (Config.debug) {
        Config.appStaticURL = Config.appStaticURL.split('/').slice(0, -1).join('/')
    }

    /* Use details supplied by webpack at build time */
    if (window.__quiAppVersion) {
        Config.appCurrentVersion = window.__quiAppVersion
    }
}

function initJQuery() {
    function passivizeEvent(eventName) {
        $.event.special[eventName] = {
            setup: function (_, ns, handle) {
                if (ns.includes('noPreventDefault')) {
                    this.addEventListener(eventName, handle, {passive: false})
                }
                else {
                    this.addEventListener(eventName, handle, {passive: true})
                }
            }
        }
    }

    passivizeEvent('touchstart')
    passivizeEvent('touchmove')
}

function configureLogging() {
    Logger.setHandler(Logger.createDefaultHandler({
        formatter: function (messages, context) {
            messages.unshift(`[${context.name}]`)
            messages.unshift(`${context.level.name}:`)
            messages.unshift(DateUtils.formatPercent(new Date(), '%Y-%m-%d %H:%M:%S:'))
        }
    }))

    Logger.setLevel(Config.debug ? Logger.DEBUG : Logger.INFO)

    /* Inject errorStack logger method */
    Object.getPrototypeOf(logger).errorStack = function (msg, error) {
        this.error(`${msg}: ${error ? error.toString() : '(no error supplied)'}`)
        if (error && error.stack) {
            this.error(error.stack)
        }
    }

    /* Unlimited stack trace */
    Error.stackTraceLimit = Infinity

    /* Export Logger to global scope  */
    let qui = (window.qui = window.qui || {})
    qui.Logger = Logger
}

function logCurrentConfig() {
    ArrayUtils.sortKey(Object.entries(Config), e => e[0]).forEach(function ([key, value]) {
        if (typeof value === 'function') {
            return
        }

        logger.debug(`using Config.${key} = ${JSON.stringify(value)}`)
    })
}

function configureGlobalErrorHandling() {
    window.addEventListener('unhandledrejection', function (e) {
        /* Uncaught/unhandled cancelled errors are silently ignored */
        if (e.reason instanceof CancelledError) {
            e.preventDefault()
            return
        }

        logger.error(`unhandled promise rejection: ${e.reason || '<unspecified reason>'}`)
        if (e.reason != null) {
            logger.error(e.reason)
        }
        logger.error(e.promise)

        let msg = gettext('An unexpected error occurred.')
        msg += '<br>'
        msg += gettext('Application reloading is recommended.')
        new StickySimpleMessageForm({type: 'error', message: msg}).show()
        e.preventDefault()
    })
}


/**
 * Initialize the QUI library.
 * @alias qui.init
 * @returns {Promise} a promise that resolves when QUI is initialized and ready
 */
export function init() {
    RequireJSCompat.cleanup()
    initConfig()
    initJQuery()
    configureLogging()
    logCurrentConfig()

    return Promise.resolve()
    .then(() => Window.init())
    .then(() => PWA.init())
    .then(() => Theme.init())
    .then(() => Icons.init())
    .then(() => MainUI.init())
    .then(() => Status.init())
    .then(() => Widgets.init())
    .then(() => Messages.init())
    .then(() => GlobalGlass.init())
    .then(() => Sections.init())
    .then(() => Pages.init())
    .then(() => Navigation.init())
    .then(() => Forms.init())
    .then(() => configureGlobalErrorHandling())
    .then(function () {
        whenReady.fulfill()
        logger.debug('QUI is ready')
    })
}

/* Make some modules accessible globally, via window */
import('$qui/config.js').then(globalize('qui.Config'))
import('$qui/lib/logger.module.js').then(globalize('qui.Logger'))
import('$qui/navigation.js').then(globalize('qui.navigation'))
import('$qui/sections/sections.js').then(globalize('qui.sections'))